Removed edge snapping in favour of single layer fog system

This commit is contained in:
Mitchell McCaffrey 2021-02-16 08:27:39 +11:00
parent 1a2260515d
commit 7639297510
12 changed files with 187 additions and 255 deletions

View File

@ -92,12 +92,12 @@ function Map({
onMapDraw(new RemoveShapeAction(shapeIds));
}
function handleFogShapeAdd(shape) {
onFogDraw(new AddShapeAction([shape]));
function handleFogShapesAdd(shapes) {
onFogDraw(new AddShapeAction(shapes));
}
function handleFogShapeCut(shape) {
onFogDraw(new CutShapeAction([shape]));
function handleFogShapesCut(shapes) {
onFogDraw(new CutShapeAction(shapes));
}
function handleFogShapesRemove(shapeIds) {
@ -228,8 +228,8 @@ function Map({
<MapFog
map={map}
shapes={fogShapes}
onShapeAdd={handleFogShapeAdd}
onShapeCut={handleFogShapeCut}
onShapesAdd={handleFogShapesAdd}
onShapesCut={handleFogShapesCut}
onShapesRemove={handleFogShapesRemove}
onShapesEdit={handleFogShapesEdit}
active={selectedToolId === "fog"}

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import shortid from "shortid";
import { Group, Rect, Line, Circle } from "react-konva";
import { Group, Rect, Line } from "react-konva";
import useImage from "use-image";
import diagonalPattern from "../../images/DiagonalPattern.png";
@ -18,7 +18,6 @@ import {
getGuidesFromBoundingBoxes,
getGuidesFromGridCell,
findBestGuides,
getSnappingVertex,
} from "../../helpers/drawing";
import colors from "../../helpers/colors";
import {
@ -27,13 +26,15 @@ import {
getRelativePointerPosition,
} from "../../helpers/konva";
import SubtractShapeAction from "../../actions/SubtractShapeAction";
import useSetting from "../../hooks/useSetting";
function MapFog({
map,
shapes,
onShapeAdd,
onShapeCut,
onShapesAdd,
onShapesCut,
onShapesRemove,
onShapesEdit,
active,
@ -66,7 +67,6 @@ function MapFog({
// Bounding boxes for guides
const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]);
const [guides, setGuides] = useState([]);
const [vertexSnapping, setVertexSnapping] = useState();
const shouldHover =
active &&
@ -76,16 +76,7 @@ function MapFog({
const shouldRenderGuides =
active &&
editable &&
(toolSettings.type === "rectangle" || toolSettings.type === "polygon") &&
!vertexSnapping;
const shouldRenderVertexSnapping =
active &&
editable &&
(toolSettings.type === "rectangle" ||
toolSettings.type === "polygon" ||
toolSettings.type === "brush") &&
toolSettings.useEdgeSnapping &&
vertexSnapping;
(toolSettings.type === "rectangle" || toolSettings.type === "polygon");
const [patternImage] = useImage(diagonalPattern);
@ -99,20 +90,13 @@ function MapFog({
function getBrushPosition(snapping = true) {
const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage);
if (snapping) {
if (shouldRenderVertexSnapping) {
position = Vector2.multiply(vertexSnapping, {
x: mapWidth,
y: mapHeight,
});
} else if (shouldRenderGuides) {
for (let guide of guides) {
if (guide.orientation === "vertical") {
position.x = guide.start.x * mapWidth;
}
if (guide.orientation === "horizontal") {
position.y = guide.start.y * mapHeight;
}
if (snapping && shouldRenderGuides) {
for (let guide of guides) {
if (guide.orientation === "vertical") {
position.x = guide.start.x * mapWidth;
}
if (guide.orientation === "horizontal") {
position.y = guide.start.y * mapHeight;
}
}
}
@ -204,33 +188,53 @@ function MapFog({
function handleBrushUp() {
if (
toolSettings.type === "brush" ||
(toolSettings.type === "rectangle" && drawingShape)
(toolSettings.type === "brush" || toolSettings.type === "rectangle") &&
drawingShape
) {
const cut = toolSettings.useFogCut;
if (drawingShape.data.points.length > 1) {
let shapeData = {};
let drawingShapes = [drawingShape];
if (!toolSettings.multilayer) {
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
);
const subtractAction = new SubtractShapeAction(
mergeFogShapes(shapesToSubtract, !cut)
);
const state = subtractAction.execute({
[drawingShape.id]: drawingShape,
});
drawingShapes = Object.values(state)
.filter((shape) => shape.data.points.length > 2)
.map((shape) => ({ ...shape, id: shortid.generate() }));
}
if (drawingShapes.length > 0) {
drawingShapes = drawingShapes.map((shape) => {
let shapeData = {};
if (cut) {
shapeData = { id: shape.id, type: shape.type };
} else {
shapeData = { ...shape, color: "black" };
}
return {
...shapeData,
data: {
...shape.data,
points: simplifyPoints(
shape.data.points,
gridCellNormalizedSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
Math.max(stageScale, 1) / 2
),
},
};
});
if (cut) {
shapeData = { id: drawingShape.id, type: drawingShape.type };
onShapesCut(drawingShapes);
} else {
shapeData = { ...drawingShape, color: "black" };
}
const shape = {
...shapeData,
data: {
...drawingShape.data,
points: simplifyPoints(
drawingShape.data.points,
gridCellNormalizedSize,
// Downscale fog as smoothing doesn't currently work with edge snapping
stageScale / 2
),
},
};
if (cut) {
onShapeCut(shape);
} else {
onShapeAdd(shape);
onShapesAdd(drawingShapes);
}
}
setDrawingShape(null);
@ -273,9 +277,7 @@ function MapFog({
function handlePolygonMove() {
if (
active &&
(toolSettings.type === "polygon" ||
toolSettings.type === "rectangle") &&
!shouldRenderVertexSnapping
(toolSettings.type === "polygon" || toolSettings.type === "rectangle")
) {
let guides = [];
const brushPosition = getBrushPosition(false);
@ -308,24 +310,6 @@ function MapFog({
setGuides(findBestGuides(brushPosition, guides));
}
if (
active &&
toolSettings.useEdgeSnapping &&
(toolSettings.type === "polygon" ||
toolSettings.type === "rectangle" ||
toolSettings.type === "brush")
) {
const brushPosition = getBrushPosition(false);
setVertexSnapping(
getSnappingVertex(
brushPosition,
fogShapes,
fogShapeBoundingBoxes,
gridCellNormalizedSize,
Math.min(0.4 / stageScale, 0.4)
)
);
}
if (toolSettings.type === "polygon") {
const brushPosition = getBrushPosition();
if (toolSettings.type === "polygon" && drawingShape) {
@ -365,23 +349,50 @@ function MapFog({
const finishDrawingPolygon = useCallback(() => {
const cut = toolSettings.useFogCut;
const data = {
...drawingShape.data,
// Remove the last point as it hasn't been placed yet
points: drawingShape.data.points.slice(0, -1),
let polygonShape = {
id: drawingShape.id,
type: drawingShape.type,
data: {
...drawingShape.data,
// Remove the last point as it hasn't been placed yet
points: drawingShape.data.points.slice(0, -1),
},
};
if (cut) {
onShapeCut({
id: drawingShape.id,
type: drawingShape.type,
data: data,
let polygonShapes = [polygonShape];
if (!toolSettings.multilayer) {
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
);
const subtractAction = new SubtractShapeAction(
mergeFogShapes(shapesToSubtract, !cut)
);
const state = subtractAction.execute({
[polygonShape.id]: polygonShape,
});
} else {
onShapeAdd({ ...drawingShape, data: data, color: "black" });
polygonShapes = Object.values(state)
.filter((shape) => shape.data.points.length > 2)
.map((shape) => ({ ...shape, id: shortid.generate() }));
}
if (polygonShapes.length > 0) {
if (cut) {
onShapesCut(polygonShapes);
} else {
onShapesAdd(
polygonShapes.map((shape) => ({
...drawingShape,
data: shape.data,
id: shape.id,
color: "black",
}))
);
}
}
setDrawingShape(null);
}, [toolSettings, drawingShape, onShapeCut, onShapeAdd]);
}, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]);
// Add keyboard shortcuts
function handleKeyDown({ key }) {
@ -467,9 +478,6 @@ function MapFog({
// Disable collision if the fog is transparent and we're not editing it
// This allows tokens to be moved under the fog
hitFunc={editable && !active ? () => {} : undefined}
// shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(34, 34, 34, 1)"}
// shadowOffset={{ x: 0, y: 5 }}
// shadowBlur={10}
/>
);
}
@ -523,18 +531,6 @@ function MapFog({
));
}
function renderSnappingVertex() {
return (
<Circle
x={vertexSnapping.x * mapWidth}
y={vertexSnapping.y * mapHeight}
radius={gridStrokeWidth}
stroke="hsl(260, 100%, 80%)"
strokeWidth={gridStrokeWidth * 0.25}
/>
);
}
useEffect(() => {
function shapeVisible(shape) {
return (active && !toolSettings.preview) || shape.visible;
@ -559,7 +555,6 @@ function MapFog({
{fogShapes.map(renderShape)}
</Group>
{shouldRenderGuides && renderGuides()}
{shouldRenderVertexSnapping && renderSnappingVertex()}
{drawingShape && renderShape(drawingShape)}
{drawingShape &&
toolSettings &&

View File

@ -1,32 +0,0 @@
import React from "react";
import { IconButton } from "theme-ui";
import SnappingOnIcon from "../../../icons/SnappingOnIcon";
import SnappingOffIcon from "../../../icons/SnappingOffIcon";
function EdgeSnappingToggle({
useEdgeSnapping,
onEdgeSnappingChange,
disabled,
}) {
return (
<IconButton
aria-label={
useEdgeSnapping
? "Disable Edge Snapping (S)"
: "Enable Edge Snapping (S)"
}
title={
useEdgeSnapping
? "Disable Edge Snapping (S)"
: "Enable Edge Snapping (S)"
}
onClick={() => onEdgeSnappingChange(!useEdgeSnapping)}
disabled={disabled}
>
{useEdgeSnapping ? <SnappingOnIcon /> : <SnappingOffIcon />}
</IconButton>
);
}
export default EdgeSnappingToggle;

View File

@ -4,7 +4,7 @@ import { useMedia } from "react-media";
import RadioIconButton from "../../RadioIconButton";
import EdgeSnappingToggle from "./EdgeSnappingToggle";
import MultilayerToggle from "./MultilayerToggle";
import FogPreviewToggle from "./FogPreviewToggle";
import FogCutToggle from "./FogCutToggle";
@ -40,8 +40,8 @@ function BrushToolSettings({
onSettingChange({ type: "toggle" });
} else if (key === "e") {
onSettingChange({ type: "remove" });
} else if (key === "s") {
onSettingChange({ useEdgeSnapping: !settings.useEdgeSnapping });
} else if (key === "l") {
onSettingChange({ multilayer: !settings.multilayer });
} else if (key === "f") {
onSettingChange({ preview: !settings.preview });
} else if (key === "c") {
@ -128,11 +128,9 @@ function BrushToolSettings({
onFogCutChange={(useFogCut) => onSettingChange({ useFogCut })}
disabled={settings.preview}
/>
<EdgeSnappingToggle
useEdgeSnapping={settings.useEdgeSnapping}
onEdgeSnappingChange={(useEdgeSnapping) =>
onSettingChange({ useEdgeSnapping })
}
<MultilayerToggle
multilayer={settings.multilayer}
onMultilayerChange={(multilayer) => onSettingChange({ multilayer })}
disabled={settings.preview}
/>
<FogPreviewToggle

View File

@ -0,0 +1,22 @@
import React from "react";
import { IconButton } from "theme-ui";
import MultilayerOnIcon from "../../../icons/FogMultilayerOnIcon";
import MultilayerOffIcon from "../../../icons/FogMultilayerOffIcon";
function MultilayerToggle({ multilayer, onMultilayerChange, disabled }) {
return (
<IconButton
aria-label={
multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"
}
title={multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"}
onClick={() => onMultilayerChange(!multilayer)}
disabled={disabled}
>
{multilayer ? <MultilayerOnIcon /> : <MultilayerOffIcon />}
</IconButton>
);
}
export default MultilayerToggle;

View File

@ -9,11 +9,13 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) {
}
}
const points = difference[i][0].map(([x, y]) => ({ x, y }));
shapes[newId] = {
...shape,
id: newId,
data: {
points: difference[i][0].map(([x, y]) => ({ x, y })),
points,
holes,
},
};
@ -23,11 +25,14 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) {
export function addPolygonIntersectionToShapes(shape, intersection, shapes) {
for (let i = 0; i < intersection.length; i++) {
let newId = `${shape.id}-int-${i}`;
const points = intersection[i][0].map(([x, y]) => ({ x, y }));
shapes[newId] = {
...shape,
id: newId,
data: {
points: intersection[i][0].map(([x, y]) => ({ x, y })),
points,
holes: [],
},
// Default intersection visibility to false

View File

@ -210,15 +210,16 @@ export function simplifyPoints(points, gridCellSize, scale) {
/**
* Merges overlapping fog shapes
* @param {Fog[]} shapes
* @param {boolean} ignoreHidden
* @returns {Fog[]}
*/
export function mergeFogShapes(shapes) {
export function mergeFogShapes(shapes, ignoreHidden = true) {
if (shapes.length === 0) {
return shapes;
}
let geometries = [];
for (let shape of shapes) {
if (!shape.visible) {
if (ignoreHidden && !shape.visible) {
continue;
}
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]);
@ -243,7 +244,7 @@ export function mergeFogShapes(shapes) {
}
merged.push({
// Use the data of the first visible shape as the merge
...shapes.find((shape) => shape.visible),
...shapes.find((shape) => ignoreHidden || shape.visible),
id: `merged-${i}`,
data: {
points: union[i][0].map(([x, y]) => ({ x, y })),
@ -253,7 +254,7 @@ export function mergeFogShapes(shapes) {
}
return merged;
} catch {
logError(new Error(`Unable to merge shapes ${JSON.stringify(shapes)}`));
console.error("Unable to merge shapes");
return shapes;
}
}
@ -464,69 +465,3 @@ export function findBestGuides(brushPosition, guides) {
}
return bestGuides;
}
/**
* @param {Vector2} brushPosition
* @param {Fog[]} shapes
* @param {Vector2.BoundingBox} boundingBoxes
* @param {Vector2} gridCellSize
* @param {number} snappingSensitivity
*/
export function getSnappingVertex(
brushPosition,
shapes,
boundingBoxes,
gridCellSize,
snappingSensitivity
) {
const minGrid = Vector2.min(gridCellSize);
const snappingDistance = minGrid * snappingSensitivity;
let closestDistance = Number.MAX_VALUE;
let closestPosition;
for (let i = 0; i < shapes.length; i++) {
// Check bounds before checking all points
const bounds = boundingBoxes[i];
const offsetMin = Vector2.subtract(bounds.min, gridCellSize);
const offsetMax = Vector2.add(bounds.max, gridCellSize);
if (
brushPosition.x < offsetMin.x ||
brushPosition.x > offsetMax.x ||
brushPosition.y < offsetMin.y ||
brushPosition.y > offsetMax.y
) {
continue;
}
const shape = shapes[i];
// Include shape points and holes
let pointArray = [shape.data.points, ...shape.data.holes];
for (let points of pointArray) {
// Find the closest point to each edge of the shape
for (let i = 0; i < points.length; i++) {
const a = points[i];
// Wrap around points to the start to account for closed shape
const b = points[(i + 1) % points.length];
let { distance, point } = Vector2.distanceToLine(brushPosition, a, b);
// Bias towards vertices
distance += snappingDistance / 2;
const isCloseToShape = distance < snappingDistance;
if (isCloseToShape && distance < closestDistance) {
closestPosition = point;
closestDistance = distance;
}
}
// Find cloest vertex
for (let point of points) {
const distance = Vector2.distance(point, brushPosition);
const isCloseToShape = distance < snappingDistance;
if (isCloseToShape && distance < closestDistance) {
closestPosition = point;
closestDistance = distance;
}
}
}
}
return closestPosition;
}

View File

@ -0,0 +1,20 @@
import React from "react";
function FogMultilayerOffIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<rect fill="none" height="24" width="24" y="0" />
<path d="M19.97,13.2c-0.36-0.28-0.86-0.28-1.22,0l-1.23,0.96l1.43,1.43l1.03-0.8c0.51-0.4,0.51-1.17,0-1.57L19.97,13.2z" />
<path d="M19.99,9.71c0.51-0.4,0.51-1.18,0-1.58l-6.76-5.26c-0.72-0.56-1.73-0.56-2.46,0L8.23,4.86l7.88,7.88L19.99,9.71z" />
<path d="M20.48,19.94L3.51,2.97c-0.39-0.39-1.02-0.39-1.41,0c-0.39,0.39-0.39,1.02,0,1.41l2.95,2.95L4.01,8.14 c-0.51,0.4-0.51,1.18,0,1.58l6.76,5.26c0.61,0.48,1.43,0.52,2.11,0.18l1.47,1.47l-2.36,1.84l-6.77-5.26 c-0.36-0.28-0.86-0.28-1.22,0c-0.51,0.4-0.51,1.17,0,1.57l6.76,5.26c0.72,0.56,1.73,0.56,2.46,0l2.55-1.98l3.3,3.3 c0.39,0.39,1.02,0.39,1.41,0C20.88,20.97,20.88,20.33,20.48,19.94z" />
</svg>
);
}
export default FogMultilayerOffIcon;

View File

@ -0,0 +1,18 @@
import React from "react";
function FogMultilayerOnIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<rect fill="none" height="24" width="24" y="0" />
<path d="M11.99,18.47l-6.77-5.26c-0.36-0.28-0.86-0.28-1.22,0l0,0c-0.51,0.4-0.51,1.17,0,1.57l6.76,5.26 c0.72,0.56,1.73,0.56,2.46,0l6.76-5.26c0.51-0.4,0.51-1.17,0-1.57l-0.01-0.01c-0.36-0.28-0.86-0.28-1.22,0L11.99,18.47z M13.23,14.97l6.76-5.26c0.51-0.4,0.51-1.18,0-1.58l-6.76-5.26c-0.72-0.56-1.73-0.56-2.46,0L4.01,8.14c-0.51,0.4-0.51,1.18,0,1.58 l6.76,5.26C11.49,15.54,12.51,15.54,13.23,14.97z" />
</svg>
);
}
export default FogMultilayerOnIcon;

View File

@ -1,18 +0,0 @@
import React from "react";
function SnappingOffIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<path d="M24 24H0V0h24z" fill="none" />
<path d="M10 5.59L6.62 2.2a1 1 0 00-.2-.15c.14-.03.27-.05.4-.05L7 2h.48c1.06 0 2.4.83 2.51 1.86L10 4v1.59zm10.88 10.88L16 11.59V4a2 2 0 011.85-2H19a3 3 0 013 2.82v6.87c0 1.69-.37 3.34-1.12 4.78zM13 21c-5.36 0-8.87-3.7-9-8.72V4.83l-.9-.9A1 1 0 014.51 2.5l.37.37a3 3 0 01.04-.04v.08l16.15 16.15a1 1 0 01-1.41 1.42l-1.17-1.17A8.98 8.98 0 0113 21zm5-12.97h.28c.55 0 1.72-.01 1.72-.03V5a1 1 0 00-.88-1H18v4.03zM6 8h.06L7.17 8 6 6.83V8zM13 15a3 3 0 00.98-.18L10 10.83V12c0 1.98 1.72 2.99 3 2.99z" />
</svg>
);
}
export default SnappingOffIcon;

View File

@ -1,18 +0,0 @@
import React from "react";
function SnappingOnIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
fill="currentcolor"
>
<path d="M24 24H0V0h24z" fill="none" />
<path d="M13 21c-5.46 0-9-3.83-9-9V5a3 3 0 013-3h.48C8.58 2 10 2.9 10 4v8c0 1.98 1.72 2.99 3 2.99S16 14 16 12V4c0-1.1.9-2 2-2h1a3 3 0 013 3v6.69c0 4.79-3 9.31-9 9.31zm5-12.97h.28c.55 0 1.72-.01 1.72-.03V5a1 1 0 00-.88-1H18v4.03zM6 8h.06L8 8V4.2h-.01l-.02-.01c-.13-.1-.3-.17-.41-.2H7a1 1 0 00-1 .88v3.13z" />
</svg>
);
}
export default SnappingOnIcon;

View File

@ -47,6 +47,13 @@ function loadVersions(settings) {
delete newSettings.measure;
return newSettings;
});
// v1.8.0 - Removed edge snapping for multilayer
settings.version(5, (prev) => {
let newSettings = { ...prev };
delete newSettings.fog.useEdgeSnapping;
newSettings.fog.multilayer = false;
return newSettings;
});
}
export function getSettings() {