diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 181163d..0c38c40 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -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({ 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 ( - - ); - } - useEffect(() => { function shapeVisible(shape) { return (active && !toolSettings.preview) || shape.visible; @@ -559,7 +555,6 @@ function MapFog({ {fogShapes.map(renderShape)} {shouldRenderGuides && renderGuides()} - {shouldRenderVertexSnapping && renderSnappingVertex()} {drawingShape && renderShape(drawingShape)} {drawingShape && toolSettings && diff --git a/src/components/map/controls/EdgeSnappingToggle.js b/src/components/map/controls/EdgeSnappingToggle.js deleted file mode 100644 index 44f7a10..0000000 --- a/src/components/map/controls/EdgeSnappingToggle.js +++ /dev/null @@ -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 ( - onEdgeSnappingChange(!useEdgeSnapping)} - disabled={disabled} - > - {useEdgeSnapping ? : } - - ); -} - -export default EdgeSnappingToggle; diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js index acdf51f..81b8384 100644 --- a/src/components/map/controls/FogToolSettings.js +++ b/src/components/map/controls/FogToolSettings.js @@ -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} /> - - onSettingChange({ useEdgeSnapping }) - } + onSettingChange({ multilayer })} disabled={settings.preview} /> onMultilayerChange(!multilayer)} + disabled={disabled} + > + {multilayer ? : } + + ); +} + +export default MultilayerToggle; diff --git a/src/helpers/actions.js b/src/helpers/actions.js index 8847937..58fb2e9 100644 --- a/src/helpers/actions.js +++ b/src/helpers/actions.js @@ -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 diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index d94ccd7..d122679 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -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; -} diff --git a/src/icons/FogMultilayerOffIcon.js b/src/icons/FogMultilayerOffIcon.js new file mode 100644 index 0000000..f71da9b --- /dev/null +++ b/src/icons/FogMultilayerOffIcon.js @@ -0,0 +1,20 @@ +import React from "react"; + +function FogMultilayerOffIcon() { + return ( + + + + + + + ); +} + +export default FogMultilayerOffIcon; diff --git a/src/icons/FogMultilayerOnIcon.js b/src/icons/FogMultilayerOnIcon.js new file mode 100644 index 0000000..361e770 --- /dev/null +++ b/src/icons/FogMultilayerOnIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function FogMultilayerOnIcon() { + return ( + + + + + ); +} + +export default FogMultilayerOnIcon; diff --git a/src/icons/SnappingOffIcon.js b/src/icons/SnappingOffIcon.js deleted file mode 100644 index ef0bcf5..0000000 --- a/src/icons/SnappingOffIcon.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -function SnappingOffIcon() { - return ( - - - - - ); -} - -export default SnappingOffIcon; diff --git a/src/icons/SnappingOnIcon.js b/src/icons/SnappingOnIcon.js deleted file mode 100644 index d78c12f..0000000 --- a/src/icons/SnappingOnIcon.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -function SnappingOnIcon() { - return ( - - - - - ); -} - -export default SnappingOnIcon; diff --git a/src/settings.js b/src/settings.js index e59cfaf..513c84b 100644 --- a/src/settings.js +++ b/src/settings.js @@ -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() {