diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index fc28d1e..da42ae1 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -6,6 +6,7 @@ import ReactResizeDetector from "react-resize-detector"; import useMapImage from "../../helpers/useMapImage"; import usePreventOverscroll from "../../helpers/usePreventOverscroll"; import useStageInteraction from "../../helpers/useStageInteraction"; +import { getMapDefaultInset } from "../../helpers/map"; import { MapInteractionProvider } from "../../contexts/MapInteractionContext"; @@ -36,6 +37,13 @@ function MapEditor({ map, onSettingsChange }) { mapHeight = map ? stageWidth * (map.height / map.width) : stageHeight; } + const defaultInset = getMapDefaultInset( + map.width, + map.height, + map.grid.size.x, + map.grid.size.y + ); + const stageTranslateRef = useRef({ x: 0, y: 0 }); const mapLayerRef = useRef(); const [preventMapInteraction, setPreventMapInteraction] = useState(false); @@ -94,7 +102,7 @@ function MapEditor({ map, onSettingsChange }) { function handleMapReset() { onSettingsChange("grid", { ...map.grid, - inset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } }, + inset: defaultInset, }); } @@ -110,10 +118,10 @@ function MapEditor({ map, onSettingsChange }) { }; const gridChanged = - map.grid.inset.topLeft.x !== 0 || - map.grid.inset.topLeft.y !== 0 || - map.grid.inset.bottomRight.x !== 1 || - map.grid.inset.bottomRight.y !== 1; + map.grid.inset.topLeft.x !== defaultInset.topLeft.x || + map.grid.inset.topLeft.y !== defaultInset.topLeft.y || + map.grid.inset.bottomRight.x !== defaultInset.bottomRight.x || + map.grid.inset.bottomRight.y !== defaultInset.bottomRight.y; return ( handleGridSizeChange(e, "x")} + onChange={handleGridSizeXChange} disabled={mapEmpty || map.type === "default"} min={1} my={1} @@ -81,7 +110,7 @@ function MapSettings({ type="number" name="gridY" value={`${(map && map.grid.size.y) || 0}`} - onChange={(e) => handleGridSizeChange(e, "y")} + onChange={handleGridSizeYChange} disabled={mapEmpty || map.type === "default"} min={1} my={1} diff --git a/src/helpers/map.js b/src/helpers/map.js new file mode 100644 index 0000000..2250b50 --- /dev/null +++ b/src/helpers/map.js @@ -0,0 +1,53 @@ +export function getMapDefaultInset(width, height, gridX, gridY) { + // Max the width + const gridScale = width / gridX; + const y = gridY * gridScale; + const yNorm = y / height; + return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: yNorm } }; +} + +// Get all factors of a number +function factors(n) { + const numbers = Array.from(Array(n + 1), (_, i) => i); + return numbers.filter((i) => n % i === 0); +} + +// Greatest common divisor +// Euclidean algorithm https://en.wikipedia.org/wiki/Euclidean_algorithm +function gcd(a, b) { + while (b !== 0) { + const t = b; + b = a % b; + a = t; + } + return a; +} + +// Find all dividers that fit into two numbers +function dividers(a, b) { + const d = gcd(a, b); + return factors(d); +} + +const commonGridScales = [70, 111, 140, 300]; + +export function gridSizeHeuristic(width, height) { + const div = dividers(width, height); + if (div.length > 0) { + // default to middle divider + let scale = div[Math.floor(div.length / 2)]; + for (let common of commonGridScales) { + // Check common but make sure the grid size is above 10 + if (div.includes(common) && width / common > 10 && height / common > 10) { + scale = common; + } + } + const x = Math.floor(width / scale); + const y = Math.floor(height / scale); + // Check grid size is below 100 + if (x < 100 && y < 100) { + return { x, y }; + } + } + return { x: 22, y: 22 }; +} diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 8e1b03b..de5069d 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -14,11 +14,11 @@ import blobToBuffer from "../helpers/blobToBuffer"; import useKeyboard from "../helpers/useKeyboard"; import { resizeImage } from "../helpers/image"; import { useSearch, useGroup, handleItemSelect } from "../helpers/select"; +import { getMapDefaultInset, gridSizeHeuristic } from "../helpers/map"; import MapDataContext from "../contexts/MapDataContext"; import AuthContext from "../contexts/AuthContext"; -const defaultMapSize = 22; const defaultMapProps = { // Grid type showGrid: false, @@ -99,34 +99,6 @@ function SelectMapModal({ if (!file) { return Promise.reject(); } - let fileGridX = defaultMapSize; - let fileGridY = defaultMapSize; - let name = "Unknown Map"; - if (file.name) { - // TODO: match all not supported on safari, find alternative - if (file.name.matchAll) { - // Match against a regex to find the grid size in the file name - // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]] - const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)]; - if (gridMatches.length > 0) { - const lastMatch = gridMatches[gridMatches.length - 1]; - const matchX = parseInt(lastMatch[1]); - const matchY = parseInt(lastMatch[3]); - if (!isNaN(matchX) && !isNaN(matchY)) { - fileGridX = matchX; - fileGridY = matchY; - } - } - } - - // Remove file extension - name = file.name.replace(/\.[^/.]+$/, ""); - // Removed grid size expression - name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, ""); - // Clean string - name = name.replace(/ +/g, " "); - name = name.trim(); - } let image = new Image(); setImageLoading(true); @@ -138,6 +110,37 @@ function SelectMapModal({ return new Promise((resolve, reject) => { image.onload = async function () { + // Find name and grid size + let gridSize; + let name = "Unknown Map"; + if (file.name) { + if (file.name.matchAll) { + // Match against a regex to find the grid size in the file name + // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]] + const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)]; + if (gridMatches.length > 0) { + const lastMatch = gridMatches[gridMatches.length - 1]; + const matchX = parseInt(lastMatch[1]); + const matchY = parseInt(lastMatch[3]); + if (!isNaN(matchX) && !isNaN(matchY)) { + gridSize = { x: matchX, y: matchY }; + } + } + } + + if (!gridSize) { + gridSize = gridSizeHeuristic(image.width, image.height); + } + + // Remove file extension + name = file.name.replace(/\.[^/.]+$/, ""); + // Removed grid size expression + name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, ""); + // Clean string + name = name.replace(/ +/g, " "); + name = name.trim(); + } + // Create resolutions const resolutions = {}; for (let resolution of mapResolutions) { @@ -166,8 +169,13 @@ function SelectMapModal({ name, type: "file", grid: { - size: { x: fileGridX, y: fileGridY }, - inset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } }, + size: gridSize, + inset: getMapDefaultInset( + image.width, + image.height, + gridSize.x, + gridSize.y + ), type: "square", }, width: image.width,