typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-16 14:55:33 +10:00
parent 68c1c6db0c
commit d80bfa2f1e
103 changed files with 1402 additions and 1336 deletions

View File

@ -1,40 +1,31 @@
// Load Diff for auto complete
// eslint-disable-next-line no-unused-vars
import { Diff } from "deep-diff";
import { diff, revertChanges } from "../helpers/diff";
import cloneDeep from "lodash.clonedeep";
/**
* @callback ActionUpdate
* @param {any} state
*/
/**
* Implementation of the Command Pattern
* Wraps an update function with internal state to support undo
*/
class Action {
class Action<State> {
/**
* The update function called with the current state and should return the updated state
* This is implemented in the child class
*
* @type {ActionUpdate}
*/
update;
update(state: State): State {
return state;
}
/**
* The changes caused by the last state update
* @type {Diff}
*/
changes;
changes: Diff<State, State>[] | undefined;
/**
* Executes the action update on the state
* @param {any} state The current state to update
* @returns {any} The updated state
* @param {State} state The current state to update
*/
execute(state) {
execute(state: State): State {
if (state && this.update) {
let newState = this.update(cloneDeep(state));
this.changes = diff(state, newState);
@ -45,10 +36,10 @@ class Action {
/**
* Reverts the changes caused by the last call of `execute`
* @param {any} state The current state to perform the undo on
* @returns {any} The state with the last changes reverted
* @param {State} state The current state to perform the undo on
* @returns {State} The state with the last changes reverted
*/
undo(state) {
undo(state: State): State {
if (state && this.changes) {
let revertedState = cloneDeep(state);
revertChanges(revertedState, this.changes);

View File

@ -1,15 +0,0 @@
import Action from "./Action";
class AddShapeAction extends Action {
constructor(shapes) {
super();
this.update = (shapesById) => {
for (let shape of shapes) {
shapesById[shape.id] = shape;
}
return shapesById;
};
}
}
export default AddShapeAction;

View File

@ -0,0 +1,21 @@
import Action from "./Action";
import { ID } from "../types/Action";
class AddStatesAction<State extends ID> extends Action<Record<string, State>> {
states: State[];
constructor(states: State[]) {
super();
this.states = states;
}
update(statesById: Record<string, State>) {
for (let state of this.states) {
statesById[state.id] = state;
}
return statesById;
}
}
export default AddStatesAction;

View File

@ -0,0 +1,41 @@
import polygonClipping from "polygon-clipping";
import Action from "./Action";
import {
addPolygonDifferenceToFog,
addPolygonIntersectionToFog,
fogToGeometry,
} from "../helpers/actions";
import { Fog, FogState } from "../types/Fog";
class CutFogAction extends Action<FogState> {
fogs: Fog[];
constructor(fog: Fog[]) {
super();
this.fogs = fog;
}
update(fogsById: FogState): FogState {
let actionGeom = this.fogs.map(fogToGeometry);
let cutFogs: FogState = {};
for (let fog of Object.values(fogsById)) {
const fogGeom = fogToGeometry(fog);
try {
const difference = polygonClipping.difference(fogGeom, ...actionGeom);
const intersection = polygonClipping.intersection(
fogGeom,
...actionGeom
);
addPolygonDifferenceToFog(fog, difference, cutFogs);
addPolygonIntersectionToFog(fog, intersection, cutFogs);
} catch {
console.error("Unable to find intersection for fogs");
}
}
return cutFogs;
}
}
export default CutFogAction;

View File

@ -1,38 +0,0 @@
import polygonClipping from "polygon-clipping";
import Action from "./Action";
import {
addPolygonDifferenceToShapes,
addPolygonIntersectionToShapes,
shapeToGeometry,
} from "../helpers/actions";
class CutShapeAction extends Action {
constructor(shapes) {
super();
this.update = (shapesById) => {
let actionGeom = shapes.map(shapeToGeometry);
let cutShapes = {};
for (let shape of Object.values(shapesById)) {
const shapeGeom = shapeToGeometry(shape);
try {
const difference = polygonClipping.difference(
shapeGeom,
...actionGeom
);
const intersection = polygonClipping.intersection(
shapeGeom,
...actionGeom
);
addPolygonDifferenceToShapes(shape, difference, cutShapes);
addPolygonIntersectionToShapes(shape, intersection, cutShapes);
} catch {
console.error("Unable to find intersection for shapes");
}
}
return cutShapes;
};
}
}
export default CutShapeAction;

View File

@ -1,17 +0,0 @@
import Action from "./Action";
class EditShapeAction extends Action {
constructor(shapes) {
super();
this.update = (shapesById) => {
for (let edit of shapes) {
if (edit.id in shapesById) {
shapesById[edit.id] = { ...shapesById[edit.id], ...edit };
}
}
return shapesById;
};
}
}
export default EditShapeAction;

View File

@ -0,0 +1,23 @@
import Action from "./Action";
import { ID } from "../types/Action";
class EditStatesAction<State extends ID> extends Action<Record<string, State>> {
edits: Partial<State>[];
constructor(edits: Partial<State>[]) {
super();
this.edits = edits;
}
update(statesById: Record<string, State>) {
for (let edit of this.edits) {
if (edit.id !== undefined && edit.id in statesById) {
statesById[edit.id] = { ...statesById[edit.id], ...edit };
}
}
return statesById;
}
}
export default EditStatesAction;

View File

@ -1,13 +0,0 @@
import Action from "./Action";
import { omit } from "../helpers/shared";
class RemoveShapeAction extends Action {
constructor(shapeIds) {
super();
this.update = (shapesById) => {
return omit(shapesById, shapeIds);
};
}
}
export default RemoveShapeAction;

View File

@ -0,0 +1,21 @@
import Action from "./Action";
import { omit } from "../helpers/shared";
import { ID } from "../types/Action";
class RemoveStatesAction<State extends ID> extends Action<
Record<string, State>
> {
stateIds: string[];
constructor(stateIds: string[]) {
super();
this.stateIds = stateIds;
}
update(statesById: Record<string, State>) {
return omit(statesById, this.stateIds);
}
}
export default RemoveStatesAction;

View File

@ -0,0 +1,32 @@
import polygonClipping from "polygon-clipping";
import Action from "./Action";
import { addPolygonDifferenceToFog, fogToGeometry } from "../helpers/actions";
import { Fog, FogState } from "../types/Fog";
class SubtractFogAction extends Action<FogState> {
fogs: Fog[];
constructor(fogs: Fog[]) {
super();
this.fogs = fogs;
}
update(fogsById: FogState): FogState {
const actionGeom = this.fogs.map(fogToGeometry);
let subtractedFogs: FogState = {};
for (let fog of Object.values(fogsById)) {
const fogGeom = fogToGeometry(fog);
try {
const difference = polygonClipping.difference(fogGeom, ...actionGeom);
addPolygonDifferenceToFog(fog, difference, subtractedFogs);
} catch {
console.error("Unable to find difference for fogs");
}
}
return subtractedFogs;
}
}
export default SubtractFogAction;

View File

@ -1,32 +0,0 @@
import polygonClipping from "polygon-clipping";
import Action from "./Action";
import {
addPolygonDifferenceToShapes,
shapeToGeometry,
} from "../helpers/actions";
class SubtractShapeAction extends Action {
constructor(shapes) {
super();
this.update = (shapesById) => {
const actionGeom = shapes.map(shapeToGeometry);
let subtractedShapes = {};
for (let shape of Object.values(shapesById)) {
const shapeGeom = shapeToGeometry(shape);
try {
const difference = polygonClipping.difference(
shapeGeom,
...actionGeom
);
addPolygonDifferenceToShapes(shape, difference, subtractedShapes);
} catch {
console.error("Unable to find difference for shapes");
}
}
return subtractedShapes;
};
}
}
export default SubtractShapeAction;

View File

@ -1,13 +0,0 @@
import AddShapeAction from "./AddShapeAction";
import CutShapeAction from "./CutShapeAction";
import EditShapeAction from "./EditShapeAction";
import RemoveShapeAction from "./RemoveShapeAction";
import SubtractShapeAction from "./SubtractShapeAction";
export {
AddShapeAction,
CutShapeAction,
EditShapeAction,
RemoveShapeAction,
SubtractShapeAction,
};

13
src/actions/index.ts Normal file
View File

@ -0,0 +1,13 @@
import AddStatesAction from "./AddStatesAction";
import CutFogAction from "./CutFogAction";
import EditStatesAction from "./EditStatesAction";
import RemoveStatesAction from "./RemoveStatesAction";
import SubtractFogAction from "./SubtractFogAction";
export {
AddStatesAction,
CutFogAction,
EditStatesAction,
RemoveStatesAction,
SubtractFogAction,
};

View File

@ -78,7 +78,7 @@ Slider.defaultProps = {
value: 0,
ml: 0,
mr: 0,
labelFunc: (value: any) => value,
labelFunc: (value: number) => value,
};
export default Slider;

View File

@ -1,5 +1,7 @@
import Modal from "react-modal";
import { useThemeUI, Close } from "theme-ui";
import { RequestCloseEventHandler } from "../../types/Events";
import CSS from "csstype";
function Banner({
isOpen,
@ -8,11 +10,11 @@ function Banner({
allowClose,
backgroundColor,
}: {
isOpen: boolean,
onRequestClose: any,
children: any,
allowClose: boolean,
backgroundColor?: any
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
children: React.ReactNode;
allowClose: boolean;
backgroundColor?: CSS.Property.Color;
}) {
const { theme } = useThemeUI();
@ -23,7 +25,8 @@ function Banner({
style={{
overlay: { bottom: "0", top: "initial", zIndex: 2000 },
content: {
backgroundColor: backgroundColor || theme.colors?.highlight,
backgroundColor:
backgroundColor || (theme.colors?.highlight as CSS.Property.Color),
color: "hsl(210, 50%, 96%)",
top: "initial",
left: "50%",

View File

@ -2,7 +2,13 @@ import { Box, Text } from "theme-ui";
import Banner from "./Banner";
function ErrorBanner({ error, onRequestClose }: { error: Error | undefined, onRequestClose: any }) {
function ErrorBanner({
error,
onRequestClose,
}: {
error: Error | undefined;
onRequestClose;
}) {
return (
<Banner isOpen={!!error} onRequestClose={onRequestClose}>
<Box p={1}>

View File

@ -35,7 +35,7 @@ type DiceInteractionProps = {
canvas: HTMLCanvasElement | WebGLRenderingContext;
}) => void;
onPointerDown: () => void;
onPointerUp: () => any;
onPointerUp: () => void;
};
function DiceInteraction({

View File

@ -21,12 +21,21 @@ import NoteMenu from "../note/NoteMenu";
import NoteDragOverlay from "../note/NoteDragOverlay";
import {
AddShapeAction,
CutShapeAction,
EditShapeAction,
RemoveShapeAction,
AddStatesAction,
CutFogAction,
EditStatesAction,
RemoveStatesAction,
} from "../../actions";
import Session from "../../network/Session";
import { Drawing } from "../../types/Drawing";
import { Fog } from "../../types/Fog";
import { Map, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState";
import { Settings } from "../../types/Settings";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
function Map({
map,
@ -51,43 +60,39 @@ function Map({
disabledTokens,
session,
}: {
map: any;
map: Map;
mapState: MapState;
mapActions: any;
onMapTokenStateChange: any;
onMapTokenStateRemove: any;
onMapChange: any;
onMapReset: any;
onMapDraw: any;
onMapDrawUndo: any;
onMapDrawRedo: any;
onFogDraw: any;
onFogDrawUndo: any;
onFogDrawRedo: any;
onMapNoteChange: any;
onMapNoteRemove: any;
mapActions: ;
onMapTokenStateChange: ;
onMapTokenStateRemove: ;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
onMapDraw: ;
onMapDrawUndo: ;
onMapDrawRedo: ;
onFogDraw: ;
onFogDrawUndo: ;
onFogDrawRedo: ;
onMapNoteChange: ;
onMapNoteRemove: ;
allowMapDrawing: boolean;
allowFogDrawing: boolean;
allowMapChange: boolean;
allowNoteEditing: boolean;
disabledTokens: any;
disabledTokens: ;
session: Session;
}) {
const { addToast } = useToasts();
const { tokensById } = useTokenData();
const [selectedToolId, setSelectedToolId] = useState("move");
const { settings, setSettings }: { settings: any; setSettings: any } =
useSettings();
const [selectedToolId, setSelectedToolId] = useState<MapToolId>("move");
const { settings, setSettings } = useSettings();
function handleToolSettingChange(tool: any, change: any) {
setSettings((prevSettings: any) => ({
function handleToolSettingChange(change: Partial<Settings>) {
setSettings((prevSettings) => ({
...prevSettings,
[tool]: {
...prevSettings[tool],
...change,
},
...change,
}));
}
@ -96,7 +101,7 @@ function Map({
function handleToolAction(action: string) {
if (action === "eraseAll") {
onMapDraw(new RemoveShapeAction(drawShapes.map((s) => s.id)));
onMapDraw(new RemoveStatesAction(drawShapes.map((s) => s.id)));
}
if (action === "mapUndo") {
onMapDrawUndo();
@ -112,28 +117,28 @@ function Map({
}
}
function handleMapShapeAdd(shape: Shape) {
onMapDraw(new AddShapeAction([shape]));
function handleMapShapeAdd(shape: Drawing) {
onMapDraw(new AddStatesAction([shape]));
}
function handleMapShapesRemove(shapeIds: string[]) {
onMapDraw(new RemoveShapeAction(shapeIds));
onMapDraw(new RemoveStatesAction(shapeIds));
}
function handleFogShapesAdd(shapes: Shape[]) {
onFogDraw(new AddShapeAction(shapes));
function handleFogShapesAdd(shapes: Fog[]) {
onFogDraw(new AddStatesAction(shapes));
}
function handleFogShapesCut(shapes: Shape[]) {
onFogDraw(new CutShapeAction(shapes));
function handleFogShapesCut(shapes: Fog[]) {
onFogDraw(new CutFogAction(shapes));
}
function handleFogShapesRemove(shapeIds: string[]) {
onFogDraw(new RemoveShapeAction(shapeIds));
onFogDraw(new RemoveStatesAction(shapeIds));
}
function handleFogShapesEdit(shapes: Shape[]) {
onFogDraw(new EditShapeAction(shapes));
function handleFogShapesEdit(shapes: Partial<Fog>[]) {
onFogDraw(new EditStatesAction(shapes));
}
const disabledControls = [];
@ -155,7 +160,10 @@ function Map({
disabledControls.push("note");
}
const disabledSettings: { fog: any[]; drawing: any[] } = {
const disabledSettings: {
fog: string[];
drawing: string[];
} = {
fog: [],
drawing: [],
};
@ -197,19 +205,10 @@ function Map({
/>
);
const [isTokenMenuOpen, setIsTokenMenuOpen]: [
isTokenMenuOpen: boolean,
setIsTokenMenuOpen: React.Dispatch<React.SetStateAction<boolean>>
] = useState<boolean>(false);
const [tokenMenuOptions, setTokenMenuOptions]: [
tokenMenuOptions: any,
setTokenMenuOptions: any
] = useState({});
const [tokenDraggingOptions, setTokenDraggingOptions]: [
tokenDraggingOptions: any,
setTokenDragginOptions: any
] = useState();
function handleTokenMenuOpen(tokenStateId: string, tokenImage: any) {
const [isTokenMenuOpen, setIsTokenMenuOpen] = useState<boolean>(false);
const [tokenMenuOptions, setTokenMenuOptions] = useState({});
const [tokenDraggingOptions, setTokenDraggingOptions] = useState();
function handleTokenMenuOpen(tokenStateId: string, tokenImage) {
setTokenMenuOptions({ tokenStateId, tokenImage });
setIsTokenMenuOpen(true);
}
@ -240,7 +239,7 @@ function Map({
const tokenDragOverlay = tokenDraggingOptions && (
<TokenDragOverlay
onTokenStateRemove={(state: any) => {
onTokenStateRemove={(state) => {
onMapTokenStateRemove(state);
setTokenDraggingOptions(null);
}}
@ -292,14 +291,14 @@ function Map({
);
const [isNoteMenuOpen, setIsNoteMenuOpen] = useState<boolean>(false);
const [noteMenuOptions, setNoteMenuOptions] = useState<any>({});
const [noteDraggingOptions, setNoteDraggingOptions] = useState<any>();
function handleNoteMenuOpen(noteId: string, noteNode: any) {
const [noteMenuOptions, setNoteMenuOptions] = useState({});
const [noteDraggingOptions, setNoteDraggingOptions] = useState();
function handleNoteMenuOpen(noteId: string, noteNode) {
setNoteMenuOptions({ noteId, noteNode });
setIsNoteMenuOpen(true);
}
function sortNotes(a: any, b: any, noteDraggingOptions: any) {
function sortNotes(a, b, noteDraggingOptions) {
if (
noteDraggingOptions &&
noteDraggingOptions.dragging &&
@ -338,7 +337,7 @@ function Map({
allowNoteEditing &&
(selectedToolId === "note" || selectedToolId === "move")
}
onNoteDragStart={(e: any, noteId: any) =>
onNoteDragStart={(e, noteId) =>
setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target })
}
onNoteDragEnd={() =>
@ -364,7 +363,7 @@ function Map({
dragging={!!(noteDraggingOptions && noteDraggingOptions.dragging)}
noteGroup={noteDraggingOptions && noteDraggingOptions.noteGroup}
noteId={noteDraggingOptions && noteDraggingOptions.noteId}
onNoteRemove={(noteId: any) => {
onNoteRemove={(noteId) => {
onMapNoteRemove(noteId);
setNoteDraggingOptions(null);
}}

View File

@ -1,4 +1,4 @@
import React, { useState, Fragment } from "react";
import { useState, Fragment } from "react";
import { IconButton, Flex, Box } from "theme-ui";
import RadioIconButton from "../RadioIconButton";
@ -21,21 +21,26 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon";
import NoteToolIcon from "../../icons/NoteToolIcon";
import useSetting from "../../hooks/useSetting";
import { Map } from "../../types/Map";
import { Map, MapTool, MapToolId } from "../../types/Map";
import { MapState } from "../../types/MapState";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
import { Settings } from "../../types/Settings";
type MapControlsProps = {
onMapChange: () => void;
onMapReset: () => void;
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
selectedToolId: string;
onSelectedToolChange: () => void;
toolSettings: any;
onToolSettingChange: () => void;
onToolAction: () => void;
selectedToolId: MapToolId;
onSelectedToolChange: (toolId: MapToolId) => void;
toolSettings: Settings;
onToolSettingChange: (change: Partial<Settings>) => void;
onToolAction: (actionId: string) => void;
disabledControls: string[];
disabledSettings: string[];
disabledSettings: Partial<Record<keyof Settings, string[]>>;
};
function MapContols({
@ -54,7 +59,7 @@ function MapContols({
const [isExpanded, setIsExpanded] = useState(true);
const [fullScreen, setFullScreen] = useSetting("map.fullScreen");
const toolsById = {
const toolsById: Record<string, MapTool> = {
move: {
id: "move",
icon: <MoveToolIcon />,
@ -89,7 +94,14 @@ function MapContols({
title: "Note Tool (N)",
},
};
const tools = ["move", "fog", "drawing", "measure", "pointer", "note"];
const tools: MapToolId[] = [
"move",
"fog",
"drawing",
"measure",
"pointer",
"note",
];
const sections = [
{
@ -174,32 +186,41 @@ function MapContols({
function getToolSettings() {
const Settings = toolsById[selectedToolId].SettingsComponent;
if (Settings) {
return (
<Box
sx={{
position: "absolute",
top: "4px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "overlay",
borderRadius: "4px",
}}
p={1}
>
<Settings
settings={toolSettings[selectedToolId]}
onSettingChange={(change) =>
onToolSettingChange(selectedToolId, change)
}
onToolAction={onToolAction}
disabledActions={disabledSettings[selectedToolId]}
/>
</Box>
);
} else {
if (
!Settings ||
selectedToolId === "move" ||
selectedToolId === "measure" ||
selectedToolId === "note"
) {
return null;
}
return (
<Box
sx={{
position: "absolute",
top: "4px",
left: "50%",
transform: "translateX(-50%)",
backgroundColor: "overlay",
borderRadius: "4px",
}}
p={1}
>
<Settings
settings={toolSettings[selectedToolId]}
onSettingChange={(change) =>
onToolSettingChange({
[selectedToolId]: {
...toolSettings[selectedToolId],
...change,
},
})
}
onToolAction={onToolAction}
disabledActions={disabledSettings[selectedToolId]}
/>
</Box>
);
}
return (

View File

@ -21,8 +21,17 @@ import GridOffIcon from "../../icons/GridOffIcon";
import MapGrid from "./MapGrid";
import MapGridEditor from "./MapGridEditor";
import { Map } from "../../types/Map";
import { GridInset } from "../../types/Grid";
function MapEditor({ map, onSettingsChange }) {
type MapSettingsChangeEventHandler = (change: Partial<Map>) => void;
type MapEditorProps = {
map: Map;
onSettingsChange: MapSettingsChangeEventHandler;
};
function MapEditor({ map, onSettingsChange }: MapEditorProps) {
const [mapImage] = useMapImage(map);
const [stageWidth, setStageWidth] = useState(1);
@ -36,12 +45,17 @@ function MapEditor({ map, onSettingsChange }) {
const mapLayerRef = useRef();
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
function handleResize(width?: number, height?: number): void {
if (width) {
setStageWidth(width);
}
if (height) {
setStageHeight(height);
}
}
const containerRef = useRef();
const containerRef = useRef(null);
usePreventOverscroll(containerRef);
const [mapWidth, mapHeight] = useImageCenter(
@ -67,17 +81,21 @@ function MapEditor({ map, onSettingsChange }) {
preventMapInteraction
);
function handleGridChange(inset) {
onSettingsChange("grid", {
...map.grid,
inset,
function handleGridChange(inset: GridInset) {
onSettingsChange({
grid: {
...map.grid,
inset,
},
});
}
function handleMapReset() {
onSettingsChange("grid", {
...map.grid,
inset: defaultInset,
onSettingsChange({
grid: {
...map.grid,
inset: defaultInset,
},
});
}
@ -120,8 +138,9 @@ function MapEditor({ map, onSettingsChange }) {
>
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
<KonvaBridge
stageRender={(children) => (
stageRender={(children: React.ReactNode) => (
<Stage
// @ts-ignore https://github.com/konvajs/react-konva/issues/342
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}

View File

@ -31,6 +31,7 @@ import {
getGuidesFromBoundingBoxes,
getGuidesFromGridCell,
findBestGuides,
Guide,
} from "../../helpers/drawing";
import colors from "../../helpers/colors";
import {
@ -40,13 +41,35 @@ import {
} from "../../helpers/konva";
import { keyBy } from "../../helpers/shared";
import SubtractShapeAction from "../../actions/SubtractShapeAction";
import CutShapeAction from "../../actions/CutShapeAction";
import SubtractFogAction from "../../actions/SubtractFogAction";
import CutFogAction from "../../actions/CutFogAction";
import useSetting from "../../hooks/useSetting";
import shortcuts from "../../shortcuts";
import { Map } from "../../types/Map";
import { Fog, FogToolSettings } from "../../types/Fog";
type FogAddEventHandler = (fog: Fog[]) => void;
type FogCutEventHandler = (fog: Fog[]) => void;
type FogRemoveEventHandler = (fogId: string[]) => void;
type FogEditEventHandler = (edit: Partial<Fog>[]) => void;
type FogErrorEventHandler = (message: string) => void;
type MapFogProps = {
map: Map;
shapes: Fog[];
onShapesAdd: FogAddEventHandler;
onShapesCut: FogCutEventHandler;
onShapesRemove: FogRemoveEventHandler;
onShapesEdit: FogEditEventHandler;
onShapeError: FogErrorEventHandler;
active: boolean;
toolSettings: FogToolSettings;
editable: boolean;
};
function MapFog({
map,
shapes,
@ -58,7 +81,7 @@ function MapFog({
active,
toolSettings,
editable,
}) {
}: MapFogProps) {
const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
@ -76,7 +99,7 @@ function MapFog({
const [editOpacity] = useSetting("fog.editOpacity");
const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState(null);
const [drawingShape, setDrawingShape] = useState<Fog | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]);
@ -84,7 +107,7 @@ function MapFog({
const [fogShapes, setFogShapes] = useState(shapes);
// Bounding boxes for guides
const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]);
const [guides, setGuides] = useState([]);
const [guides, setGuides] = useState<Guide[]>([]);
const shouldHover =
active &&
@ -108,8 +131,14 @@ function MapFog({
const mapStage = mapStageRef.current;
function getBrushPosition(snapping = true) {
if (!mapStage) {
return;
}
const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage);
if (!position) {
return;
}
if (shouldUseGuides && snapping) {
for (let guide of guides) {
if (guide.orientation === "vertical") {
@ -129,6 +158,9 @@ function MapFog({
function handleBrushDown() {
if (toolSettings.type === "brush") {
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape({
type: "fog",
data: {
@ -143,6 +175,9 @@ function MapFog({
}
if (toolSettings.type === "rectangle") {
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape({
type: "fog",
data: {
@ -166,7 +201,13 @@ function MapFog({
function handleBrushMove() {
if (toolSettings.type === "brush" && isBrushDown && drawingShape) {
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape((prevShape) => {
if (!prevShape) {
return prevShape;
}
const prevPoints = prevShape.data.points;
if (
Vector2.compare(
@ -193,7 +234,13 @@ function MapFog({
if (toolSettings.type === "rectangle" && isBrushDown && drawingShape) {
const prevPoints = drawingShape.data.points;
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
}
setDrawingShape((prevShape) => {
if (!prevShape) {
return prevShape;
}
return {
...prevShape,
data: {
@ -223,7 +270,7 @@ function MapFog({
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
);
const subtractAction = new SubtractShapeAction(shapesToSubtract);
const subtractAction = new SubtractFogAction(shapesToSubtract);
const state = subtractAction.execute({
[drawingShape.id]: drawingShape,
});
@ -235,7 +282,7 @@ function MapFog({
if (drawingShapes.length > 0) {
if (cut) {
// Run a pre-emptive cut action to check whether we've cut anything
const cutAction = new CutShapeAction(drawingShapes);
const cutAction = new CutFogAction(drawingShapes);
const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) {
@ -300,7 +347,7 @@ function MapFog({
function handlePointerMove() {
if (shouldUseGuides) {
let guides = [];
let guides: Guide[] = [];
const brushPosition = getBrushPosition(false);
const absoluteBrushPosition = Vector2.multiply(brushPosition, {
x: mapWidth,
@ -393,7 +440,7 @@ function MapFog({
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
);
const subtractAction = new SubtractShapeAction(shapesToSubtract);
const subtractAction = new SubtractFogAction(shapesToSubtract);
const state = subtractAction.execute({
[polygonShape.id]: polygonShape,
});
@ -405,7 +452,7 @@ function MapFog({
if (polygonShapes.length > 0) {
if (cut) {
// Run a pre-emptive cut action to check whether we've cut anything
const cutAction = new CutShapeAction(polygonShapes);
const cutAction = new CutFogAction(polygonShapes);
const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) {

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import useImage from "use-image";
import { useDataURL } from "../../contexts/AssetsContext";
@ -8,8 +8,9 @@ import { mapSources as defaultMapSources } from "../../maps";
import { getImageLightness } from "../../helpers/image";
import Grid from "../Grid";
import { Map } from "../../types/Map";
function MapGrid({ map }) {
function MapGrid({ map }: { map: Map }) {
let mapSourceMap = map;
const mapURL = useDataURL(
mapSourceMap,
@ -17,13 +18,14 @@ function MapGrid({ map }) {
undefined,
map.type === "file"
);
const [mapImage, mapLoadingStatus] = useImage(mapURL);
const [mapImage, mapLoadingStatus] = useImage(mapURL || "");
const [isImageLight, setIsImageLight] = useState(true);
// When the map changes find the average lightness of its pixels
useEffect(() => {
if (mapLoadingStatus === "loaded") {
if (mapLoadingStatus === "loaded" && mapImage) {
setIsImageLight(getImageLightness(mapImage));
}
}, [mapImage, mapLoadingStatus]);

View File

@ -1,5 +1,6 @@
import React, { useRef } from "react";
import { useRef } from "react";
import { Group, Circle, Rect } from "react-konva";
import { KonvaEventObject, Node } from "konva/types/Node";
import {
useDebouncedStageScale,
@ -12,8 +13,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2";
import shortcuts from "../../shortcuts";
import { Map } from "../../types/Map";
import { GridInset } from "../../types/Grid";
function MapGridEditor({ map, onGridChange }) {
type MapGridEditorProps = {
map: Map;
onGridChange: (inset: GridInset) => void;
};
function MapGridEditor({ map, onGridChange }: MapGridEditorProps) {
const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
@ -39,21 +47,21 @@ function MapGridEditor({ map, onGridChange }) {
}
const handlePositions = getHandlePositions();
const handlePreviousPositionRef = useRef();
const handlePreviousPositionRef = useRef<Vector2>();
function handleScaleCircleDragStart(event) {
function handleScaleCircleDragStart(event: KonvaEventObject<MouseEvent>) {
const handle = event.target;
const position = getHandleNormalizedPosition(handle);
handlePreviousPositionRef.current = position;
}
function handleScaleCircleDragMove(event) {
function handleScaleCircleDragMove(event: KonvaEventObject<MouseEvent>) {
const handle = event.target;
onGridChange(getHandleInset(handle));
handlePreviousPositionRef.current = getHandleNormalizedPosition(handle);
}
function handleScaleCircleDragEnd(event) {
function handleScaleCircleDragEnd(event: KonvaEventObject<MouseEvent>) {
onGridChange(getHandleInset(event.target));
setPreventMapInteraction(false);
}
@ -66,11 +74,14 @@ function MapGridEditor({ map, onGridChange }) {
setPreventMapInteraction(false);
}
function getHandleInset(handle) {
function getHandleInset(handle: Node): GridInset {
const name = handle.name();
// Find distance and direction of dragging
const previousPosition = handlePreviousPositionRef.current;
if (!previousPosition) {
return map.grid.inset;
}
const position = getHandleNormalizedPosition(handle);
const distance = Vector2.distance(previousPosition, position);
const direction = Vector2.normalize(
@ -154,7 +165,7 @@ function MapGridEditor({ map, onGridChange }) {
}
}
function nudgeGrid(direction, scale) {
function nudgeGrid(direction: Vector2, scale: number) {
const inset = map.grid.inset;
const gridSizeNormalized = Vector2.divide(
Vector2.subtract(inset.bottomRight, inset.topLeft),
@ -170,7 +181,7 @@ function MapGridEditor({ map, onGridChange }) {
});
}
function handleKeyDown(event) {
function handleKeyDown(event: KeyboardEvent) {
const nudgeAmount = event.shiftKey ? 2 : 0.5;
if (shortcuts.gridNudgeUp(event)) {
// Stop arrow up/down scrolling if overflowing
@ -191,7 +202,7 @@ function MapGridEditor({ map, onGridChange }) {
useKeyboard(handleKeyDown);
function getHandleNormalizedPosition(handle) {
function getHandleNormalizedPosition(handle: Node) {
return Vector2.divide({ x: handle.x(), y: handle.y() }, mapSize);
}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { IconButton } from "theme-ui";
import SelectMapModal from "../../modals/SelectMapModal";
@ -6,6 +6,20 @@ import SelectMapIcon from "../../icons/SelectMapIcon";
import { useMapData } from "../../contexts/MapDataContext";
import { useUserId } from "../../contexts/UserIdContext";
import {
MapChangeEventHandler,
MapResetEventHandler,
} from "../../types/Events";
import { Map } from "../../types/Map";
import { MapState } from "../../types/MapState";
type SelectMapButtonProps = {
onMapChange: MapChangeEventHandler;
onMapReset: MapResetEventHandler;
currentMap?: Map;
currentMapState?: MapState;
disabled: boolean;
};
function SelectMapButton({
onMapChange,
@ -13,7 +27,7 @@ function SelectMapButton({
currentMap,
currentMapState,
disabled,
}) {
}: SelectMapButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const { updateMapState } = useMapData();

View File

@ -4,7 +4,13 @@ import { IconButton } from "theme-ui";
import ChangeNicknameModal from "../../modals/ChangeNicknameModal";
import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon";
function ChangeNicknameButton({ nickname, onChange }: { nickname: string, onChange: any}) {
function ChangeNicknameButton({
nickname,
onChange,
}: {
nickname: string;
onChange;
}) {
const [isChangeModalOpen, setIsChangeModalOpen] = useState(false);
function openModal() {
setIsChangeModalOpen(true);

View File

@ -1,12 +1,20 @@
import { Flex, Box, Text } from "theme-ui";
function DiceRoll({ rolls, type, children }: { rolls: any, type: string, children: any}) {
function DiceRoll({
rolls,
type,
children,
}: {
rolls;
type: string;
children;
}) {
return (
<Flex sx={{ flexWrap: "wrap" }}>
<Box sx={{ transform: "scale(0.8)" }}>{children}</Box>
{rolls
.filter((d: any) => d.type === type && d.roll !== "unknown")
.map((dice: any, index: string | number) => (
.filter((d) => d.type === type && d.roll !== "unknown")
.map((dice, index: string | number) => (
<Text as="p" my={1} variant="caption" mx={1} key={index}>
{dice.roll}
</Text>

View File

@ -24,14 +24,14 @@ const diceIcons = [
{ type: "d100", Icon: D100Icon },
];
function DiceRolls({ rolls }: { rolls: any }) {
function DiceRolls({ rolls }: { rolls }) {
const total = getDiceRollTotal(rolls);
const [expanded, setExpanded] = useState<boolean>(false);
let expandedRolls = [];
for (let icon of diceIcons) {
if (rolls.some((roll: any) => roll.type === icon.type)) {
if (rolls.some((roll) => roll.type === icon.type)) {
expandedRolls.push(
<DiceRoll rolls={rolls} type={icon.type} key={icon.type}>
<icon.Icon />
@ -45,29 +45,29 @@ function DiceRolls({ rolls }: { rolls: any }) {