diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..d0920b6f Binary files /dev/null and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 00000000..127299e8 Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.yarn/cache/@emotion-is-prop-valid-npm-0.8.6-4f7ef72d35-9c9b4b2d85.zip b/.yarn/cache/@emotion-is-prop-valid-npm-0.8.6-4f7ef72d35-9c9b4b2d85.zip deleted file mode 100644 index 439b95fb..00000000 Binary files a/.yarn/cache/@emotion-is-prop-valid-npm-0.8.6-4f7ef72d35-9c9b4b2d85.zip and /dev/null differ diff --git a/.yarn/cache/@emotion-memoize-npm-0.7.4-5648cf11b8-4e3920d4ec.zip b/.yarn/cache/@emotion-memoize-npm-0.7.4-5648cf11b8-4e3920d4ec.zip deleted file mode 100644 index b4720df6..00000000 Binary files a/.yarn/cache/@emotion-memoize-npm-0.7.4-5648cf11b8-4e3920d4ec.zip and /dev/null differ diff --git a/.yarn/cache/@floating-ui-core-npm-1.6.8-496cdfbb6e-82faa6ea9d.zip b/.yarn/cache/@floating-ui-core-npm-1.6.8-496cdfbb6e-82faa6ea9d.zip new file mode 100644 index 00000000..c8c009dc Binary files /dev/null and b/.yarn/cache/@floating-ui-core-npm-1.6.8-496cdfbb6e-82faa6ea9d.zip differ diff --git a/.yarn/cache/@floating-ui-dom-npm-1.6.11-b81155e63e-d6413759ab.zip b/.yarn/cache/@floating-ui-dom-npm-1.6.11-b81155e63e-d6413759ab.zip new file mode 100644 index 00000000..5399d9d8 Binary files /dev/null and b/.yarn/cache/@floating-ui-dom-npm-1.6.11-b81155e63e-d6413759ab.zip differ diff --git a/.yarn/cache/@floating-ui-react-dom-npm-2.1.2-9e283fcbfa-25bb031686.zip b/.yarn/cache/@floating-ui-react-dom-npm-2.1.2-9e283fcbfa-25bb031686.zip new file mode 100644 index 00000000..7449ee62 Binary files /dev/null and b/.yarn/cache/@floating-ui-react-dom-npm-2.1.2-9e283fcbfa-25bb031686.zip differ diff --git a/.yarn/cache/@floating-ui-react-npm-0.24.8-39038a6688-b345d3bbb7.zip b/.yarn/cache/@floating-ui-react-npm-0.24.8-39038a6688-b345d3bbb7.zip deleted file mode 100644 index 3b471343..00000000 Binary files a/.yarn/cache/@floating-ui-react-npm-0.24.8-39038a6688-b345d3bbb7.zip and /dev/null differ diff --git a/.yarn/cache/@floating-ui-react-npm-0.26.24-4412013655-c49fc0040d.zip b/.yarn/cache/@floating-ui-react-npm-0.26.24-4412013655-c49fc0040d.zip new file mode 100644 index 00000000..a49804ae Binary files /dev/null and b/.yarn/cache/@floating-ui-react-npm-0.26.24-4412013655-c49fc0040d.zip differ diff --git a/.yarn/cache/@floating-ui-utils-npm-0.2.8-01a00634a5-deb98bba01.zip b/.yarn/cache/@floating-ui-utils-npm-0.2.8-01a00634a5-deb98bba01.zip new file mode 100644 index 00000000..fd466226 Binary files /dev/null and b/.yarn/cache/@floating-ui-utils-npm-0.2.8-01a00634a5-deb98bba01.zip differ diff --git a/.yarn/cache/@types-lodash-npm-4.17.10-033d752d27-4600f2f252.zip b/.yarn/cache/@types-lodash-npm-4.17.10-033d752d27-4600f2f252.zip new file mode 100644 index 00000000..5abaada7 Binary files /dev/null and b/.yarn/cache/@types-lodash-npm-4.17.10-033d752d27-4600f2f252.zip differ diff --git a/.yarn/cache/@types-uuid-npm-10.0.0-9ac1066765-e3958f8b0f.zip b/.yarn/cache/@types-uuid-npm-10.0.0-9ac1066765-e3958f8b0f.zip new file mode 100644 index 00000000..f31647f5 Binary files /dev/null and b/.yarn/cache/@types-uuid-npm-10.0.0-9ac1066765-e3958f8b0f.zip differ diff --git a/.yarn/cache/@types-uuid-npm-9.0.7-c380bb8654-c7321194ae.zip b/.yarn/cache/@types-uuid-npm-9.0.7-c380bb8654-c7321194ae.zip deleted file mode 100644 index a051baf4..00000000 Binary files a/.yarn/cache/@types-uuid-npm-9.0.7-c380bb8654-c7321194ae.zip and /dev/null differ diff --git a/.yarn/cache/aria-hidden-npm-1.2.3-02d72be80c-7d7d211629.zip b/.yarn/cache/aria-hidden-npm-1.2.3-02d72be80c-7d7d211629.zip deleted file mode 100644 index f73359af..00000000 Binary files a/.yarn/cache/aria-hidden-npm-1.2.3-02d72be80c-7d7d211629.zip and /dev/null differ diff --git a/.yarn/cache/classnames-npm-2.2.5-0eaec5c33f-cf6bc29a8a.zip b/.yarn/cache/classnames-npm-2.2.5-0eaec5c33f-cf6bc29a8a.zip deleted file mode 100644 index d37bbc5e..00000000 Binary files a/.yarn/cache/classnames-npm-2.2.5-0eaec5c33f-cf6bc29a8a.zip and /dev/null differ diff --git a/.yarn/cache/classnames-npm-2.5.1-c7273f3423-da424a8a6f.zip b/.yarn/cache/classnames-npm-2.5.1-c7273f3423-da424a8a6f.zip new file mode 100644 index 00000000..748647bb Binary files /dev/null and b/.yarn/cache/classnames-npm-2.5.1-c7273f3423-da424a8a6f.zip differ diff --git a/.yarn/cache/framer-motion-npm-10.16.16-01419cd9ac-992d755faa.zip b/.yarn/cache/framer-motion-npm-10.16.16-01419cd9ac-992d755faa.zip deleted file mode 100644 index 344dec30..00000000 Binary files a/.yarn/cache/framer-motion-npm-10.16.16-01419cd9ac-992d755faa.zip and /dev/null differ diff --git a/.yarn/cache/framer-motion-npm-11.5.6-f6ace36519-b37bc0856e.zip b/.yarn/cache/framer-motion-npm-11.5.6-f6ace36519-b37bc0856e.zip new file mode 100644 index 00000000..d3c95fca Binary files /dev/null and b/.yarn/cache/framer-motion-npm-11.5.6-f6ace36519-b37bc0856e.zip differ diff --git a/.yarn/cache/lucide-react-npm-0.291.0-3ab92b0f70-7c15106d25.zip b/.yarn/cache/lucide-react-npm-0.291.0-3ab92b0f70-7c15106d25.zip deleted file mode 100644 index 4542e638..00000000 Binary files a/.yarn/cache/lucide-react-npm-0.291.0-3ab92b0f70-7c15106d25.zip and /dev/null differ diff --git a/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-d8e7416032.zip b/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-d8e7416032.zip deleted file mode 100644 index 7f1008f4..00000000 Binary files a/.yarn/cache/lucide-react-npm-0.294.0-86f24c8068-d8e7416032.zip and /dev/null differ diff --git a/.yarn/cache/lucide-react-npm-0.439.0-f219c67bf3-3de85588b6.zip b/.yarn/cache/lucide-react-npm-0.445.0-8c9bb71600-848f382ead.zip similarity index 54% rename from .yarn/cache/lucide-react-npm-0.439.0-f219c67bf3-3de85588b6.zip rename to .yarn/cache/lucide-react-npm-0.445.0-8c9bb71600-848f382ead.zip index de5bad3a..165abb4e 100644 Binary files a/.yarn/cache/lucide-react-npm-0.439.0-f219c67bf3-3de85588b6.zip and b/.yarn/cache/lucide-react-npm-0.445.0-8c9bb71600-848f382ead.zip differ diff --git a/.yarn/cache/lucide-react-npm-0.447.0-3b33a909f1-c634a6d6fa.zip b/.yarn/cache/lucide-react-npm-0.447.0-3b33a909f1-c634a6d6fa.zip new file mode 100644 index 00000000..ec698033 Binary files /dev/null and b/.yarn/cache/lucide-react-npm-0.447.0-3b33a909f1-c634a6d6fa.zip differ diff --git a/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-f290fe5788.zip b/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-f290fe5788.zip new file mode 100644 index 00000000..dee0b7e2 Binary files /dev/null and b/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-f290fe5788.zip differ diff --git a/.yarn/cache/open-color-npm-1.9.1-84a56b0f77-8ab89bb950.zip b/.yarn/cache/open-color-npm-1.9.1-84a56b0f77-8ab89bb950.zip new file mode 100644 index 00000000..7993689a Binary files /dev/null and b/.yarn/cache/open-color-npm-1.9.1-84a56b0f77-8ab89bb950.zip differ diff --git a/.yarn/cache/preshape-npm-19.1.0-641a60f534-7716ddbc6b.zip b/.yarn/cache/preshape-npm-19.1.0-641a60f534-7716ddbc6b.zip deleted file mode 100644 index 531e41cb..00000000 Binary files a/.yarn/cache/preshape-npm-19.1.0-641a60f534-7716ddbc6b.zip and /dev/null differ diff --git a/.yarn/cache/preshape-npm-19.1.5-e1c25eb766-4601ca65cf.zip b/.yarn/cache/preshape-npm-19.1.5-e1c25eb766-4601ca65cf.zip new file mode 100644 index 00000000..e7725da6 Binary files /dev/null and b/.yarn/cache/preshape-npm-19.1.5-e1c25eb766-4601ca65cf.zip differ diff --git a/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-39931f6da7.zip b/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-39931f6da7.zip deleted file mode 100644 index 9a64a742..00000000 Binary files a/.yarn/cache/uuid-npm-9.0.1-39a8442bc6-39931f6da7.zip and /dev/null differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index b08e3f8b..afb6bf42 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/Cargo.lock b/Cargo.lock index ce1065ec..e92c40fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,7 +441,7 @@ dependencies = [ ] [[package]] -name = "circular-sequence" +name = "circular_sequence" version = "0.0.0" dependencies = [ "pretty_assertions", @@ -1962,6 +1962,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.210" @@ -1999,9 +2008,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64", "chrono", @@ -2017,9 +2026,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", "proc-macro2", @@ -2133,6 +2142,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spatial_grid_map" +version = "0.0.0" +dependencies = [ + "insta", + "log", + "rand", + "serde", + "serde_arrays", + "serde_json", + "typeshare", +] + [[package]] name = "spin" version = "0.5.2" @@ -2482,7 +2504,7 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", - "circular-sequence", + "circular_sequence", "console_error_panic_hook", "console_log", "insta", @@ -2493,6 +2515,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_with", + "spatial_grid_map", "thiserror", "tracing", "typeshare", @@ -2544,6 +2567,7 @@ name = "tiling-renderer" version = "0.0.0" dependencies = [ "anyhow", + "circular_sequence", "colorgrad", "console_error_panic_hook", "console_log", @@ -2553,6 +2577,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", + "spatial_grid_map", "thiserror", "tiling", "typeshare", @@ -2582,22 +2607,6 @@ dependencies = [ "typeshare", ] -[[package]] -name = "tiling-wasm" -version = "0.0.0" -dependencies = [ - "console_error_panic_hook", - "console_log", - "log", - "serde", - "serde-wasm-bindgen", - "serde_json", - "tiling", - "tiling-renderer", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "time" version = "0.3.36" @@ -2868,7 +2877,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" name = "wasm" version = "0.0.0" dependencies = [ - "circular-sequence", + "circular_sequence", "console_error_panic_hook", "console_log", "line_segment_extending", @@ -2877,6 +2886,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", + "spatial_grid_map", "tiling", "tiling-renderer", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 8f2da00d..0520c673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ strip = true members = [ "workspaces/circular-sequence", "workspaces/line-segment-extending", + "workspaces/spatial-grid-map", "workspaces/tilings/src-rust/*", "workspaces/wasm", ] @@ -29,10 +30,12 @@ futures-util = "0.3.27" insta = { version = "1.40.0", features = ["json"] } log = "0.4.22" pretty_assertions = "1.3.0" +rand = "0.8.4" serde = { version = "1.0.210", features = ["derive"] } +serde_arrays = "0.1.0" serde_json = "1.0.128" serde-wasm-bindgen = "0.6.5" -serde_with = "3.9.0" +serde_with = "3.11.0" sqlx = { version = "0.8.2", features = [ "chrono", "macros", diff --git a/package.json b/package.json index 96f72373..a08cc6a2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,6 @@ "typescript": "^5.0.4" }, "dependencies": { - "preshape": "^19.1.0" + "preshape": "^19.1.5" } } diff --git a/tsconfig.json b/tsconfig.json index 9106bfdb..f9683b78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ "noFallthroughCasesInSwitch": true, /* Experimental */ - "types": ["vite/client"] + "types": [] } } diff --git a/workspaces/.DS_Store b/workspaces/.DS_Store new file mode 100644 index 00000000..6469d34b Binary files /dev/null and b/workspaces/.DS_Store differ diff --git a/workspaces/circle-art/package.json b/workspaces/circle-art/package.json index ac937b0f..2010626e 100644 --- a/workspaces/circle-art/package.json +++ b/workspaces/circle-art/package.json @@ -8,7 +8,7 @@ "dependencies": { "@hogg/circle-intersections": "workspace:^", "@hogg/common": "workspace:^", - "preshape": "^19.1.0", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/workspaces/circle-art/src/Presentation/index.tsx b/workspaces/circle-art/src/Presentation/index.tsx index b74cbc38..216d01d0 100644 --- a/workspaces/circle-art/src/Presentation/index.tsx +++ b/workspaces/circle-art/src/Presentation/index.tsx @@ -16,7 +16,6 @@ const Presentation = ({ } - controlsPosition="top" padding="x0" tabs={ diff --git a/workspaces/circle-intersections/package.json b/workspaces/circle-intersections/package.json index d117666b..7c014f8c 100644 --- a/workspaces/circle-intersections/package.json +++ b/workspaces/circle-intersections/package.json @@ -15,13 +15,13 @@ "bitset": "^5.1.1", "classnames": "^2.3.2", "file-saver": "^2.0.5", - "framer-motion": "^10.16.16", - "lucide-react": "^0.294.0", - "preshape": "^19.1.0", + "framer-motion": "11.5.6", + "lucide-react": "0.445.0", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "sat": "^0.9.0", - "uuid": "^9.0.1" + "uuid": "10.0.0" }, "devDependencies": { "@svgr/cli": "^8.1.0", @@ -29,6 +29,6 @@ "@swc/core": "^1.4.2", "@types/file-saver": "^2.0.7", "@types/sat": "^0.0.35", - "@types/uuid": "^9.0.7" + "@types/uuid": "10.0.0" } } diff --git a/workspaces/circular-sequence/Cargo.toml b/workspaces/circular-sequence/Cargo.toml index 5c097af6..8e039432 100644 --- a/workspaces/circular-sequence/Cargo.toml +++ b/workspaces/circular-sequence/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "circular-sequence" +name = "circular_sequence" version = "0.0.0" edition = "2021" authors = ["Harry Hogg "] diff --git a/workspaces/circular-sequence/package.json b/workspaces/circular-sequence/package.json index f62bd7fe..469f98e3 100644 --- a/workspaces/circular-sequence/package.json +++ b/workspaces/circular-sequence/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@hogg/common": "workspace:^", - "lucide-react": "0.439.0", - "preshape": "^19.1.0", + "lucide-react": "0.445.0", + "preshape": "^19.1.5", "react": "^18.2.0", "uuid": "10.0.0" }, diff --git a/workspaces/circular-sequence/src-rust/lib.rs b/workspaces/circular-sequence/src-rust/lib.rs index 14988980..bfbf829a 100644 --- a/workspaces/circular-sequence/src-rust/lib.rs +++ b/workspaces/circular-sequence/src-rust/lib.rs @@ -11,4 +11,4 @@ pub use min_permutation::{get_min_permutation, reverse}; pub use sequence::{get_length, get_symmetry_index, insert, is_symmetrical, Sequence}; pub use sequence_store::SequenceStore; pub use sort::{compare, sort}; -pub use to_string::to_string; +pub use to_string::{to_string, to_string_one}; diff --git a/workspaces/circular-sequence/src-rust/sequence.rs b/workspaces/circular-sequence/src-rust/sequence.rs index 4da131c9..1d0470a4 100644 --- a/workspaces/circular-sequence/src-rust/sequence.rs +++ b/workspaces/circular-sequence/src-rust/sequence.rs @@ -1,7 +1,10 @@ +use typeshare::typeshare; + #[path = "./sequence_tests.rs"] #[cfg(test)] mod tests; +#[typeshare] pub type Sequence = [u8; 12]; /// Returns the length of a sequence. diff --git a/workspaces/circular-sequence/src-rust/sequence_store.rs b/workspaces/circular-sequence/src-rust/sequence_store.rs index 1339edcf..93932af0 100644 --- a/workspaces/circular-sequence/src-rust/sequence_store.rs +++ b/workspaces/circular-sequence/src-rust/sequence_store.rs @@ -20,6 +20,13 @@ impl SequenceStore { self.sequences.get(index as usize) } + pub fn get_index(&self, sequence: &Sequence) -> Option { + match self.get_match(sequence) { + Match::Exact(index) => Some(index), + _ => None, + } + } + pub fn get_match(&self, sequence: &Sequence) -> Match { get_match(sequence, &self.sequences) } diff --git a/workspaces/common/.DS_Store b/workspaces/common/.DS_Store new file mode 100644 index 00000000..dd40a3a0 Binary files /dev/null and b/workspaces/common/.DS_Store differ diff --git a/workspaces/common/package.json b/workspaces/common/package.json index 1bca12f6..8c38ede6 100644 --- a/workspaces/common/package.json +++ b/workspaces/common/package.json @@ -6,9 +6,10 @@ "type": "module", "main": "./src/index.ts", "dependencies": { - "framer-motion": "^10.16.16", - "lucide-react": "^0.294.0", - "preshape": "^19.1.0", + "framer-motion": "11.5.6", + "lucide-react": "0.445.0", + "luxon": "3.5.0", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", diff --git a/workspaces/common/src/Article/ArticleFigCodeBlock.tsx b/workspaces/common/src/Article/ArticleFigCodeBlock.tsx index cdce4df7..1d4f02b6 100644 --- a/workspaces/common/src/Article/ArticleFigCodeBlock.tsx +++ b/workspaces/common/src/Article/ArticleFigCodeBlock.tsx @@ -52,6 +52,7 @@ export default function ArticleFigCodeBlock({ borderSize="x1" flex="vertical" grow + padding="x6" minWidth="0px" > {presentation} @@ -69,13 +70,10 @@ export default function ArticleFigCodeBlock({ {contents} diff --git a/workspaces/common/src/ProjectPage/ProjectPageHeader.tsx b/workspaces/common/src/ProjectPage/ProjectPageHeader.tsx index 506f0ed2..cd14fe2f 100644 --- a/workspaces/common/src/ProjectPage/ProjectPageHeader.tsx +++ b/workspaces/common/src/ProjectPage/ProjectPageHeader.tsx @@ -1,8 +1,12 @@ import { Box, BoxProps, Text } from 'preshape'; +import { formateDate } from '../utils'; import { useProjectPageContext } from './useProjectPageContext'; export default function ProjectPageHeader(props: BoxProps) { - const { name, description, wip } = useProjectPageContext(); + const { name, description, created, updated, wip } = useProjectPageContext(); + const createdString = formateDate(created); + const updatedString = formateDate(updated); + const showUpdated = createdString !== updatedString; return ( @@ -14,7 +18,7 @@ export default function ProjectPageHeader(props: BoxProps) { {description} - {wip && ( + {wip ? ( - WIP + Work In Progress + + ) : ( + + Written {createdString} {showUpdated && `ยท Updated ${updatedString}`} )} diff --git a/workspaces/common/src/ProjectWindow/ProjectHeader.tsx b/workspaces/common/src/ProjectWindow/ProjectHeader.tsx new file mode 100644 index 00000000..54c438d3 --- /dev/null +++ b/workspaces/common/src/ProjectWindow/ProjectHeader.tsx @@ -0,0 +1,15 @@ +import { Box, BoxProps } from 'preshape'; + +export default function ProjectHeader(props: BoxProps) { + return ( + + ); +} diff --git a/workspaces/common/src/ProjectWindow/ProjectHeaderGroup.tsx b/workspaces/common/src/ProjectWindow/ProjectHeaderGroup.tsx new file mode 100644 index 00000000..c1d50edd --- /dev/null +++ b/workspaces/common/src/ProjectWindow/ProjectHeaderGroup.tsx @@ -0,0 +1,5 @@ +import { Box, BoxProps } from 'preshape'; + +export default function ProjectHeaderGroup(props: BoxProps) { + return ; +} diff --git a/workspaces/common/src/ProjectWindow/ProjectWindowContents.tsx b/workspaces/common/src/ProjectWindow/ProjectWindowContents.tsx index f3d03cd7..e4b1b523 100644 --- a/workspaces/common/src/ProjectWindow/ProjectWindowContents.tsx +++ b/workspaces/common/src/ProjectWindow/ProjectWindowContents.tsx @@ -1,4 +1,4 @@ -import { Appear, Box, BoxProps } from 'preshape'; +import { Box, BoxProps } from 'preshape'; import { useProjectWindowContext } from '..'; import PatternBackground, { PatternBackgroundProps } from './PatternBackground'; @@ -7,8 +7,7 @@ export type ProjectWindowContentsProps = { backgroundPatternGap?: PatternBackgroundProps['patternGap']; backgroundPatternSize?: PatternBackgroundProps['patternSize']; controls?: JSX.Element; - controlsVisible?: boolean; - controlsPosition?: 'top' | 'bottom'; + header?: JSX.Element; tabs?: JSX.Element; shadow?: boolean; }; @@ -23,11 +22,10 @@ export default function ProjectWindowContents({ backgroundPatternSize, children, controls, - controlsVisible, - controlsPosition = 'bottom', gap, gapHorizontal, gapVertical, + header, onClick, padding = 'x6', paddingBottom, @@ -59,15 +57,10 @@ export default function ProjectWindowContents({ ref={refWindow} theme={theme} > - {controls && controlsPosition === 'top' && ( - - {controls} - + {header && ( + + {header} + )} - {controls && controlsPosition === 'bottom' && ( - + {controls && ( + {controls} - + )} ); diff --git a/workspaces/common/src/SvgLabels/SvgLabel.tsx b/workspaces/common/src/SvgLabels/SvgLabel.tsx index b8820fcd..0a0df106 100644 --- a/workspaces/common/src/SvgLabels/SvgLabel.tsx +++ b/workspaces/common/src/SvgLabels/SvgLabel.tsx @@ -20,7 +20,7 @@ type SvgLabelProps = Omit< paddingHorizontal?: number; paddingVertical?: number; margin?: number; - text: string; + text: JSX.Element | string; offsetX?: number; offsetY?: number; targetX: number; @@ -127,7 +127,7 @@ export default function SvgLabel({ y={-1} width={width} height={height} - fill={`var(--color-${backgroundColor}`} + fill={backgroundColor ? `var(--color-${backgroundColor}` : 'none'} stroke={stroke} strokeDasharray="4 4" strokeWidth="1" @@ -135,19 +135,19 @@ export default function SvgLabel({ ry={borderRadius} /> - - {text} - + + + {text} + + diff --git a/workspaces/common/src/SvgLabels/SvgLabelsProvider.tsx b/workspaces/common/src/SvgLabels/SvgLabelsProvider.tsx index 57838194..9ab735be 100644 --- a/workspaces/common/src/SvgLabels/SvgLabelsProvider.tsx +++ b/workspaces/common/src/SvgLabels/SvgLabelsProvider.tsx @@ -14,24 +14,34 @@ import { getLabelShifts, } from './utils/getLabelShifts'; +const defaultGetPoints = ( + count: number, + width: number, + height: number +): Point[] => { + return getArchimedesSpiral(count, [width * -1, width], [height * -1, height]); +}; + type SvgLabelsProviderProps = { width: number; height: number; + getPoints?: typeof defaultGetPoints; }; export default function SvgLabelsProvider({ width, height, + getPoints = defaultGetPoints, ...props }: PropsWithChildren) { - const refTimeout = useRef(null); + const refTimeout = useRef(null); const refLabels = useRef([]); const refObstacles = useRef([]); const [shifts, setShifts] = useState([]); const points = useMemo( - () => getArchimedesSpiral(1000, [width * -1, width], [height * -1, height]), - [height, width] + () => getPoints(1000, width, height), + [getPoints, height, width] ); const refreshLabelShifts = useCallback(() => { diff --git a/workspaces/common/src/index.ts b/workspaces/common/src/index.ts index 7fe482c1..29580f17 100644 --- a/workspaces/common/src/index.ts +++ b/workspaces/common/src/index.ts @@ -22,6 +22,7 @@ export { default as Lines } from './Lines/Lines'; export { default as SvgLabel } from './SvgLabels/SvgLabel'; export { default as SvgLabelsProvider } from './SvgLabels/SvgLabelsProvider'; export { default as useSvgLabelsContext } from './SvgLabels/useSvgLabelsContext'; +export { type Point } from './SvgLabels/types'; export { default as PatternBackground } from './ProjectWindow/PatternBackground'; @@ -36,6 +37,8 @@ export { useProjectPageContext } from './ProjectPage/useProjectPageContext'; export { default as ProjectControl } from './ProjectWindow/ProjectControl'; export { default as ProjectControlGroup } from './ProjectWindow/ProjectControlGroup'; export { default as ProjectControls } from './ProjectWindow/ProjectControls'; +export { default as ProjectHeader } from './ProjectWindow/ProjectHeader'; +export { default as ProjectHeaderGroup } from './ProjectWindow/ProjectHeaderGroup'; export { default as ProjectProgressBar } from './ProjectWindow/ProjectProgressBar'; export { default as ProjectTab } from './ProjectWindow/ProjectTab'; export { default as ProjectTabs } from './ProjectWindow/ProjectTabs'; diff --git a/workspaces/common/src/types.ts b/workspaces/common/src/types.ts index 6fed7095..726ead20 100644 --- a/workspaces/common/src/types.ts +++ b/workspaces/common/src/types.ts @@ -8,6 +8,7 @@ export enum ProjectKey { evolution = 'evolution', line_segment_extending = 'line-segment-extending', snake = 'snake', + spatial_grid_map = 'spatial-grid-map', spirals = 'spirals', tilings = 'tilings', wasm = 'wasm', @@ -19,7 +20,7 @@ export type Project = { description: string; created: string; updated: string; - image: string; + image?: string; tags: string[]; href?: string; deploy?: boolean; diff --git a/workspaces/common/src/utils.ts b/workspaces/common/src/utils.ts index a00306bf..9ad9707e 100644 --- a/workspaces/common/src/utils.ts +++ b/workspaces/common/src/utils.ts @@ -1,9 +1,14 @@ +import { DateTime } from 'luxon'; import { Project } from './types'; export function getProjectRoutePath(project: Project): string { return `/projects/${project.id}`; } +export function formateDate(date: string): string { + return DateTime.fromISO(date).toLocaleString(DateTime.DATE_FULL); +} + export function createLinearScale( domain: [number, number], range: [number, number] diff --git a/workspaces/evolution/package.json b/workspaces/evolution/package.json index 4216df33..a3d85365 100644 --- a/workspaces/evolution/package.json +++ b/workspaces/evolution/package.json @@ -13,13 +13,14 @@ }, "dependencies": { "@hogg/common": "workspace:^", - "lucide-react": "^0.294.0", - "preshape": "^19.1.0", + "lucide-react": "0.445.0", + "preshape": "^19.1.5", "react": "^18.2.0", - "uuid": "^9.0.1" + "uuid": "10.0.0" }, "devDependencies": { "@types/react": "^18.0.28", + "@types/uuid": "10.0.0", "typescript": "^5.0.4" } } diff --git a/workspaces/evolution/src/Presentation/index.tsx b/workspaces/evolution/src/Presentation/index.tsx index 40d72d33..be9d0c42 100644 --- a/workspaces/evolution/src/Presentation/index.tsx +++ b/workspaces/evolution/src/Presentation/index.tsx @@ -5,7 +5,6 @@ const Presentation = ({}: {}) => { return ( } - controlsPosition="top" padding="x0" tabs={ diff --git a/workspaces/line-segment-extending/package.json b/workspaces/line-segment-extending/package.json index 76beb71d..93ab4ec8 100644 --- a/workspaces/line-segment-extending/package.json +++ b/workspaces/line-segment-extending/package.json @@ -14,8 +14,8 @@ "@hogg/tilings": "workspace:^", "@hogg/wasm": "workspace:^", "framer-motion": "11.5.4", - "lucide-react": "0.439.0", - "preshape": "^19.1.0", + "lucide-react": "0.445.0", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/workspaces/line-segment-extending/src-rust/extend_line_segment.rs b/workspaces/line-segment-extending/src-rust/extend_line_segment.rs index 86087b35..92bea0c8 100644 --- a/workspaces/line-segment-extending/src-rust/extend_line_segment.rs +++ b/workspaces/line-segment-extending/src-rust/extend_line_segment.rs @@ -14,8 +14,8 @@ pub fn extend_line_segment( let dx = x2 - x1; let dy = y2 - y1; - let is_horizontal = dx.abs() < f64::EPSILON; - let is_vertical = dy.abs() < f64::EPSILON; + let is_horizontal = dy.abs() < f64::EPSILON; + let is_vertical = dx.abs() < f64::EPSILON; // For cases where the line is horizontal or vertical, // we can just use the min/max values of the bbox, and diff --git a/workspaces/line-segment-extending/src/Article.tsx b/workspaces/line-segment-extending/src/Article.tsx index 8354a84e..1ba7dc98 100644 --- a/workspaces/line-segment-extending/src/Article.tsx +++ b/workspaces/line-segment-extending/src/Article.tsx @@ -7,7 +7,12 @@ import { ArticlePage, ProjectPageLink, } from '@hogg/common'; -import { Annotation, TilingRenderer, meta as tilingsMeta } from '@hogg/tilings'; +import { + Layer, + Options, + TilingRenderer, + meta as tilingsMeta, +} from '@hogg/tilings'; import { ArticleSection, ArticleHeading, @@ -21,12 +26,18 @@ import { type Props = {}; -const tilingRendererOptions = { - expansionPhases: 1, - showAnnotations: { - [Annotation.AxisOrigin]: false, - [Annotation.Transform]: true, - [Annotation.VertexType]: false, +const tilingRendererOptions: Partial = { + showTransformIndex: 1, + showLayers: { + [Layer.Axis]: false, + [Layer.BoundingBoxes]: false, + [Layer.GridLineSegment]: false, + [Layer.GridPolygon]: false, + [Layer.PlaneOutline]: false, + [Layer.ShapeBorder]: true, + [Layer.ShapeFill]: true, + [Layer.Transform]: true, + [Layer.TransformPoints]: false, }, }; @@ -54,6 +65,7 @@ const Article = ({}: Props) => { @@ -337,6 +349,7 @@ let intercepts_max_x = y_for_max_x >= min_y && y_for_max_x <= max_y; diff --git a/workspaces/line-segment-extending/src/Presentation/Controls.tsx b/workspaces/line-segment-extending/src/Presentation/Controls.tsx index 4586c3cd..cfc3705e 100644 --- a/workspaces/line-segment-extending/src/Presentation/Controls.tsx +++ b/workspaces/line-segment-extending/src/Presentation/Controls.tsx @@ -3,6 +3,7 @@ import { ProjectControlGroup, ProjectControl, } from '@hogg/common'; +import { WasmWorkerLabel } from '@hogg/wasm'; import { SettingsIcon } from 'lucide-react'; import { PointerEvent } from 'react'; import { useLineSegmentContext } from './useLineSegmentContext'; @@ -16,7 +17,11 @@ export default function Controls() { }; return ( - + + + + + "] +description = "A custom data structure for dividing a 2D space into a grid of cells, and bucketing value for efficient lookup." +license = "MIT/Apache-2.0" +repository = "https://github.com/hhogg/hogg.io" + +[lib] +name = "spatial_grid_map" +path = "src-rust/lib.rs" + +[dependencies] +log.workspace = true +rand.workspace = true +serde.workspace = true +serde_arrays.workspace = true +serde_json.workspace = true +typeshare.workspace = true + +[dev-dependencies] +insta = { workspace = true } diff --git a/workspaces/spatial-grid-map/package.json b/workspaces/spatial-grid-map/package.json new file mode 100644 index 00000000..90752fdd --- /dev/null +++ b/workspaces/spatial-grid-map/package.json @@ -0,0 +1,25 @@ +{ + "name": "@hogg/spatial-grid-map", + "version": "0.0.0", + "author": "Harry Hogg ", + "private": true, + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types.ts" + }, + "dependencies": { + "@hogg/common": "workspace:^", + "lucide-react": "0.445.0", + "preshape": "^19.1.5", + "react": "^18.2.0", + "uuid": "10.0.0" + }, + "devDependencies": { + "@svgr/cli": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0", + "@types/react": "^18.0.28", + "typescript": "^5.0.4" + } +} diff --git a/workspaces/spatial-grid-map/src-rust/bucket.rs b/workspaces/spatial-grid-map/src-rust/bucket.rs new file mode 100644 index 00000000..83b1c0a6 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/bucket.rs @@ -0,0 +1,208 @@ +use std::{ + cmp::Ordering, + collections::{BTreeSet, HashMap}, + ops::{Deref, DerefMut}, +}; + +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::utils::{compare_coordinate, compare_radians, coordinate_equals, normalize_radian}; + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[typeshare] +pub struct Bucket { + #[typeshare(serialized_as = "Vec>")] + entries: BTreeSet>, +} + +impl Bucket { + pub fn new(point: (f64, f64), value: TEntryValue, size: f32) -> Self { + Bucket { + entries: BTreeSet::from([BucketEntry { + point, + value, + size, + counters: HashMap::new(), + }]), + } + } + + pub fn size(&self) -> usize { + self.entries.len() + } + + pub fn iter(&self) -> impl Iterator> { + self.entries.iter() + } + + pub fn iter_points(&self) -> impl Iterator { + self.entries.iter().map(|entry| &entry.point) + } + + pub fn iter_values(&self) -> impl Iterator { + self.entries.iter().map(|entry| &entry.value) + } + pub fn get_entry_index(&self, point: &(f64, f64)) -> Option { + self + .entries + .iter() + .position(|BucketEntry { point: (x, y), .. }| { + coordinate_equals(*x, point.0) && coordinate_equals(*y, point.1) + }) + } + + pub fn get_entry(&self, point: &(f64, f64)) -> Option<&BucketEntry> { + self + .get_entry_index(point) + .and_then(|index| self.entries.iter().nth(index)) + } + + pub fn get_entry_mut(&mut self, point: &(f64, f64)) -> Option> { + self.remove(point).map(|entry| MutBucketEntry { + item: entry, + parent: self, + }) + } + + pub fn take_entry(&mut self, point: &(f64, f64)) -> Option> { + self + .get_entry_index(point) + .and_then(|index| self.entries.iter().nth(index).cloned()) + .and_then(|entry| self.entries.take(&entry)) + } + + pub fn get_value(&self, point: &(f64, f64)) -> Option<&TEntryValue> { + self.get_entry(point).map(|entry| &entry.value) + } + + pub fn contains(&self, point: &(f64, f64)) -> bool { + self.get_value(point).is_some() + } + + pub fn insert(&mut self, entry: BucketEntry) -> bool { + if !self.contains(&entry.point) { + return self.entries.insert(entry); + } + + false + } + + pub fn remove(&mut self, point: &(f64, f64)) -> Option> { + self + .get_entry(point) + .cloned() + .and_then(|entry| self.entries.take(&entry)) + } + + pub fn increment_counter(&mut self, point: &(f64, f64), counter: &str) { + let mut entry = self + .get_entry_mut(point) + .expect("No entry found to increment."); + let counter = entry.counters.entry(counter.to_string()).or_insert(0); + + *counter += 1; + } + + pub fn get_counter(&self, point: &(f64, f64), counter: &str) -> Option<&u32> { + self + .get_entry(point) + .and_then(|entry| entry.counters.get(counter)) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[typeshare] +pub struct BucketEntry { + #[typeshare(serialized_as = "Vec")] + pub point: (f64, f64), + pub size: f32, + pub value: TEntryValue, + pub counters: HashMap, +} + +impl BucketEntry { + pub fn with_point(mut self, point: (f64, f64)) -> Self { + self.point = point; + self + } + + pub fn with_size(mut self, size: f32) -> Self { + self.size = size; + self + } + + pub fn with_value(mut self, value: TEntryValue) -> Self { + self.value = value; + self + } + + pub fn with_counters(mut self, counters: HashMap) -> Self { + self.counters = counters; + self + } + + pub fn distance_to_center(&self) -> f64 { + let (x, y) = self.point; + (x * x + y * y).sqrt() + } + + pub fn theta(&self) -> f64 { + let (x, y) = self.point; + normalize_radian(y.atan2(x)) + } +} + +impl Eq for BucketEntry {} + +impl PartialEq for BucketEntry { + fn eq(&self, other: &Self) -> bool { + coordinate_equals(self.point.0, other.point.0) && coordinate_equals(self.point.0, other.point.0) + } +} + +impl Ord for BucketEntry { + fn cmp(&self, other: &Self) -> Ordering { + let theta_comparison = compare_radians(self.theta(), other.theta()); + + if theta_comparison != Ordering::Equal { + return theta_comparison; + } + + compare_coordinate(self.distance_to_center(), other.distance_to_center()) + } +} + +impl PartialOrd for BucketEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// Helper struct to mimic a mutable reference +pub struct MutBucketEntry<'a, TEntryValue: Clone + Default> { + item: BucketEntry, + parent: &'a mut Bucket, +} + +impl<'a, TEntryValue: Clone + Default> Deref for MutBucketEntry<'a, TEntryValue> { + type Target = BucketEntry; + + fn deref(&self) -> &Self::Target { + &self.item + } +} + +impl<'a, TEntryValue: Clone + Default> DerefMut for MutBucketEntry<'a, TEntryValue> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.item + } +} + +// Reinserts the item into the BTreeSet when `MutEntry` is dropped +impl<'a, TEntryValue: Clone + Default> Drop for MutBucketEntry<'a, TEntryValue> { + fn drop(&mut self) { + let item = std::mem::take(&mut self.item); + self.parent.entries.insert(item); + } +} diff --git a/workspaces/spatial-grid-map/src-rust/grid.rs b/workspaces/spatial-grid-map/src-rust/grid.rs new file mode 100644 index 00000000..bce75684 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/grid.rs @@ -0,0 +1,307 @@ +#[path = "./grid_tests.rs"] +#[cfg(test)] +mod grid_tests; + +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use core::f64; +use std::collections::{BTreeSet, HashMap}; +use std::mem; + +use crate::bucket::{Bucket, BucketEntry, MutBucketEntry}; +use crate::location::{self, Location}; +use crate::visitor::Visitor; + +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +#[typeshare] +pub enum ResizeMethod { + First, + Fixed, + Maximum, + Minimum, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[typeshare] +pub struct SpatialGridMap { + /// Default of "2" as 2 * 2 * 64 = 256 bits or a 16x16 grid. + /// The reason we start with 4 blocks as opposed to 1 block + /// is to avoid the case of shifting a single block over the center + /// of 4 blocks which would require some block splitting. + pub blocks_dimension: u32, + /// Sorted set of all occupied locations in the grid. + #[typeshare(serialized_as = "Vec")] + pub locations: BTreeSet, + /// Resizing + pub resize_method: ResizeMethod, + /// Bucket store. + #[typeshare(serialized_as = "Map, Bucket>")] + pub store: HashMap>, + /// The amount of space each bit represents in the grid. + spacing: Option, +} + +impl SpatialGridMap { + pub fn with_resize_method(mut self, resize_method: ResizeMethod) -> Self { + self.resize_method = resize_method; + self + } + + pub fn with_spacing(mut self, spacing: f64) -> Self { + self.spacing = Some(spacing as f32); + self + } + + pub fn get_spacing(&self) -> f64 { + self.spacing.unwrap_or(1.0) as f64 + } + + pub fn get_grid_size(&self) -> u64 { + Location::get_grid_size(self.blocks_dimension) + } + + /// Increases the grid size by making it 4 times larger (a square) + fn increase_size(&mut self) { + self.blocks_dimension *= 2; + } + + fn get_location(&self, point: &(f64, f64)) -> Option { + let location = self.get_location_unchecked(point); + + if location.contained { + Some(location) + } else { + None + } + } + + fn get_location_unchecked(&self, point: &(f64, f64)) -> Location { + Location::new(self.blocks_dimension, self.get_spacing(), *point) + } + + fn get_bucket_by_location(&self, location: &Location) -> Option<&Bucket> { + self.store.get(&location.key) + } + + fn get_bucket_by_location_mut( + &mut self, + location: &Location, + ) -> Option<&mut Bucket> { + self.store.get_mut(&location.key) + } + + fn get_bucket_by_point(&self, point: &(f64, f64)) -> Option<&Bucket> { + self + .get_location(point) + .and_then(|location| self.get_bucket_by_location(&location)) + } + + fn get_bucket_by_point_mut(&mut self, point: &(f64, f64)) -> Option<&mut Bucket> { + self + .get_location(point) + .and_then(|location| self.store.get_mut(&location.key)) + } + + pub fn get_value(&self, point: &(f64, f64)) -> Option<&TEntryValue> { + self + .get_bucket_by_point(point) + .and_then(|bucket| bucket.get_value(point)) + } + + pub fn get_value_mut(&mut self, point: &(f64, f64)) -> Option> { + self + .get_bucket_by_point_mut(point) + .and_then(|bucket| bucket.get_entry_mut(point)) + } + + fn get_value_by_location(&self, location: &Location) -> Option<&TEntryValue> { + self + .get_bucket_by_location(location) + .and_then(|bucket| bucket.get_value(&location.point)) + } + + pub fn iter_points(&self) -> impl Iterator { + self.locations.iter().map(|location| &location.point) + } + + pub fn iter_values(&self) -> impl Iterator { + self + .locations + .iter() + .map(|location| self.get_value_by_location(location).unwrap()) + } + + pub fn iter_values_around( + &self, + point: &(f64, f64), + radius: u8, + ) -> impl Iterator { + let location = self.get_location_unchecked(point); + + Visitor::new(location.key, radius) + .filter_map(move |key| self.store.get(&key)) + .flat_map(|bucket| bucket.iter_values()) + } + + pub fn size(&self) -> usize { + self.store.values().map(|bucket| bucket.size()).sum() + } + + pub fn is_empty(&self) -> bool { + self.iter_values().next().is_none() + } + + /// Checks if the grid contains the given centroid. + pub fn contains(&self, point: &(f64, f64)) -> bool { + self.get_value(point).is_some() + } + + fn insert_entry(&mut self, entry: BucketEntry) -> &mut Self { + match self.get_location(&entry.point) { + None => { + self.increase_size(); + self.insert_entry(entry); + } + Some(location) => { + let size = entry.size; + + if self.store.entry(location.key).or_default().insert(entry) { + self.locations.insert(location); + self.update_spacing(size); + } + } + }; + + self + } + + /// Inserts a point into the grid, returning false if it's already present. + /// If the grid is too small, it will be increased to fit the new centroid. + pub fn insert(&mut self, point: (f64, f64), size: f64, value: TEntryValue) -> &mut Self { + self.insert_entry( + BucketEntry::default() + .with_point(point) + .with_value(value) + .with_size(size as f32), + ) + } + + pub fn get_counter(&self, point: &(f64, f64), counter: &str) -> Option<&u32> { + self + .get_bucket_by_point(point) + .and_then(|bucket| bucket.get_counter(point, counter)) + } + + pub fn increment_counter(&mut self, point: &(f64, f64), counter: &str) { + if let Some(bucket) = self.get_bucket_by_point_mut(point) { + bucket.increment_counter(point, counter) + } + } + + pub fn remove(&mut self, point: &(f64, f64)) { + if let Some(location) = self.get_location(point) { + self + .get_bucket_by_location_mut(&location) + .map(|bucket| bucket.remove(point)); + + self.locations.remove(&location); + } + + self + .get_bucket_by_point_mut(point) + .map(|bucket| bucket.remove(point)); + + self.locations.remove(&self.get_location(point).unwrap()); + } + + pub fn filter(&self, predicate: impl Fn(&TEntryValue) -> bool) -> Self { + let mut locations = BTreeSet::new(); + let mut store: HashMap> = HashMap::new(); + + for (key, bucket) in &self.store { + for entry in bucket.iter() { + if predicate(&entry.value) { + store.entry(*key).or_default().insert( + BucketEntry::default() + .with_point(entry.point) + .with_value(entry.value.clone()) + .with_counters(entry.counters.clone()), + ); + + locations.insert(self.get_location(&entry.point).unwrap()); + } + } + } + + SpatialGridMap { + blocks_dimension: self.blocks_dimension, + locations, + resize_method: self.resize_method, + spacing: self.spacing, + store, + } + } + + fn update_spacing(&mut self, new_spacing: f32) { + match self.resize_method { + ResizeMethod::First => { + if self.spacing.is_none() { + self.spacing = Some(new_spacing); + } else { + return; + } + } + ResizeMethod::Fixed => { + return; + } + ResizeMethod::Maximum => { + if new_spacing > self.get_spacing() as f32 { + self.spacing = Some(new_spacing); + } else { + return; + } + } + ResizeMethod::Minimum => { + if new_spacing < self.get_spacing() as f32 { + self.spacing = Some(new_spacing); + } else { + return; + } + } + } + + let old_locations = mem::take(&mut self.locations); + let mut old_store = mem::take(&mut self.store); + + self.blocks_dimension = 2; + + for location in old_locations { + let bucket = old_store + .get_mut(&location.key) + .expect("Bucket not found while updating spacing"); + let bucket_entry = bucket + .take_entry(&location.point) + .expect("Bucket entry not found while updating spacing"); + + self.insert_entry( + BucketEntry::default() + .with_point(location.point) + .with_value(bucket_entry.value) + .with_counters(bucket_entry.counters), + ); + } + } +} + +impl Default for SpatialGridMap { + fn default() -> Self { + SpatialGridMap { + blocks_dimension: 2, + locations: BTreeSet::new(), + resize_method: ResizeMethod::Fixed, + spacing: None, + store: HashMap::new(), + } + } +} diff --git a/workspaces/spatial-grid-map/src-rust/grid_tests.rs b/workspaces/spatial-grid-map/src-rust/grid_tests.rs new file mode 100644 index 00000000..467751cd --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/grid_tests.rs @@ -0,0 +1,136 @@ +use super::*; + +#[test] +fn get_grid_size() { + assert_eq!(SpatialGridMap::::default().get_grid_size(), 16); +} + +#[test] +fn insert_top_left() { + let mut grid = SpatialGridMap::::default(); + let point = (-7.5, -7.5); + + grid.insert(point, 1.0, true); + + assert!(grid.contains(&point)); + assert_eq!(grid.get_value(&point), Some(&true)); +} + +#[test] +fn insert_top_right() { + let mut grid = SpatialGridMap::::default(); + let point = (7.5, -7.5); + + grid.insert(point, 1.0, true); + + assert!(grid.contains(&point)); + assert_eq!(grid.get_value(&point), Some(&true)); +} + +#[test] +fn insert_bottom_left() { + let mut grid = SpatialGridMap::::default(); + let point = (-7.5, 7.5); + + grid.insert(point, 1.0, true); + + assert!(grid.contains(&point)); + assert_eq!(grid.get_value(&point), Some(&true)); +} + +#[test] +fn insert_bottom_right() { + let mut grid = SpatialGridMap::::default(); + let point = (7.5, 7.5); + grid.insert(point, 1.0, true); + + assert!(grid.contains(&point)); + assert_eq!(grid.get_value(&point), Some(&true)); +} + +#[test] +fn insert_duplicate() { + let point = (-7.5, -7.5); + let mut grid = SpatialGridMap::::default(); + grid.insert(point, 1.0, true); + grid.insert(point, 1.0, false); + + assert!(grid.contains(&point)); + assert_eq!(grid.get_value(&point), Some(&true)); +} + +#[test] +fn insert_and_rescale() { + let mut grid = SpatialGridMap::::default(); + + assert_eq!(grid.get_grid_size(), 16); + grid.insert((-8.0, -8.0), 1.0, true); + assert_eq!(grid.get_grid_size(), 16); + grid.insert((-9.0, -9.0), 1.0, true); + assert_eq!(grid.get_grid_size(), 32); + assert!(grid.contains(&(-8.0, -8.0))); + assert!(grid.contains(&(-9.0, -9.0))); +} + +#[test] +fn contains_true() { + let mut grid = SpatialGridMap::::default(); + grid.insert((-7.5, -7.5), 1.0, true); + + assert!(grid.contains(&(-7.5, -7.5))); +} + +#[test] +fn contains_false() { + let grid = SpatialGridMap::::default(); + + assert!(!grid.contains(&(-7.5, -7.5))); +} + +#[test] +fn rescales_top_left() { + let mut grid = SpatialGridMap::::default(); + grid.insert((-7.5, -7.5), 1.0, true); + + assert_eq!(grid.get_grid_size(), 16); + grid.increase_size(); + assert_eq!(grid.get_grid_size(), 32); + + assert_eq!(grid.get_value(&(-7.5, -7.5)), Some(&true)); +} + +#[test] +fn rescales_top_right() { + let mut grid = SpatialGridMap::::default(); + grid.insert((7.5, -7.5), 1.0, true); + + assert_eq!(grid.get_grid_size(), 16); + grid.increase_size(); + assert_eq!(grid.get_grid_size(), 32); + + assert_eq!(grid.get_value(&(7.5, -7.5)), Some(&true)); +} + +#[test] +fn rescales_bottom_left() { + let mut grid = SpatialGridMap::::default(); + grid.insert((-7.5, 7.5), 1.0, true); + + assert_eq!(grid.get_grid_size(), 16); + grid.increase_size(); + assert_eq!(grid.get_grid_size(), 32); + + assert_eq!(grid.get_value(&(-7.5, 7.5)), Some(&true)); +} + +#[test] +fn rescales_bottom_right() { + let mut grid = SpatialGridMap::::default(); + grid.insert((7.5, 7.5), 1.0, true); + + assert_eq!(grid.get_grid_size(), 16); + grid.increase_size(); + assert_eq!(grid.get_grid_size(), 32); + + assert_eq!(grid.get_value(&(7.5, 7.5)), Some(&true)); +} diff --git a/workspaces/spatial-grid-map/src-rust/lib.rs b/workspaces/spatial-grid-map/src-rust/lib.rs new file mode 100644 index 00000000..c5ecef0c --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/lib.rs @@ -0,0 +1,7 @@ +mod bucket; +mod grid; +mod location; +pub mod utils; +mod visitor; + +pub use grid::{ResizeMethod, SpatialGridMap}; diff --git a/workspaces/spatial-grid-map/src-rust/location.rs b/workspaces/spatial-grid-map/src-rust/location.rs new file mode 100644 index 00000000..cf8abb6e --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/location.rs @@ -0,0 +1,147 @@ +#[path = "./location_tests.rs"] +#[cfg(test)] +mod tests; + +use std::cmp::Ordering; + +use serde::{Deserialize, Serialize}; + +use crate::utils::{compare_coordinate, compare_radians, coordinate_equals, get_radians_for_x_y}; + +const BLOCK_SIZE: u64 = 8; // Sqrt(64) +pub(super) const TOLERANCE: f64 = 0.0001525; + +pub type Key = (i64, i64); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Location { + pub key: Key, + pub block_index: u64, + pub bit_index: u64, + pub point: (f64, f64), + pub distance: f64, + pub radians: f64, + pub contained: bool, +} + +impl Location { + pub fn get_grid_size(blocks_dimension: u32) -> u64 { + (blocks_dimension as u64) * BLOCK_SIZE + } + + pub fn new(blocks_dimension: u32, spacing: f64, point: (f64, f64)) -> Self { + let (x, y) = point; + let grid_size = Self::get_grid_size(blocks_dimension); + let grid_size_div2 = grid_size / 2; + + // Scale the coordinates according to the bit size + let mut scaled_x = x / spacing; + let mut scaled_y = y / spacing; + + // We need to snap the coordinates to the grid. + // To handle precision errors, anything that is less + // than a whole number by some threshold we will snap + // up to the whole number. + let dx = (scaled_x - scaled_x.round()).abs(); + let dy = (scaled_y - scaled_y.round()).abs(); + + if dx <= TOLERANCE { + scaled_x = scaled_x.round(); + } else { + scaled_x = scaled_x.floor(); + } + + if dy <= TOLERANCE { + scaled_y = scaled_y.round(); + } else { + scaled_y = scaled_y.floor(); + } + + // Adjust the coordinates relative to the grid center + let adjusted_x = scaled_x + grid_size_div2 as f64; + let adjusted_y = scaled_y + grid_size_div2 as f64; + + // Ensure the coordinates are within the grid + let contained = adjusted_x >= 0.0 + && adjusted_y >= 0.0 + && adjusted_x < grid_size as f64 + && adjusted_y < grid_size as f64; + + let adjusted_x = adjusted_x as u64; + let adjusted_y = adjusted_y as u64; + + // Determine the number of blocks along one dimension + let num_blocks_per_row = grid_size / BLOCK_SIZE; + + // Find the x and y block indices + let block_x = adjusted_x / BLOCK_SIZE; + let block_y = adjusted_y / BLOCK_SIZE; + + // Calculate the block index + let mut block_index = block_y * num_blocks_per_row + block_x; + + // Find the local coordinates within the block (0-63) + let bit_x = adjusted_x % BLOCK_SIZE; + let bit_y = adjusted_y % BLOCK_SIZE; + + // Calculate the bit index within the block + let mut bit_index = bit_y * BLOCK_SIZE + bit_x; + + // Calculate the x and y offset from the center + let absolute_bit_x = block_x * BLOCK_SIZE + bit_x; + let absolute_bit_y = block_y * BLOCK_SIZE + bit_y; + let bit_dx = absolute_bit_x as i64 - grid_size_div2 as i64; + let bit_dy = absolute_bit_y as i64 - grid_size_div2 as i64; + + let key = (bit_dx, bit_dy); + + // The block and bit indexes above are calculated as if the blocks + // were made up of 64 bits. We need to adjust the block index and + // bit index to account for the fact they are stored as 32 bits. + block_index *= 2; + + if bit_index >= 32 { + block_index += 1; + bit_index -= 32; + } + + let distance = (x * x + y * y).sqrt(); + let radians = get_radians_for_x_y(x, y); + + Location { + point, + key, + block_index, + bit_index, + distance, + radians, + contained, + } + } +} + +impl Eq for Location {} + +impl PartialEq for Location { + fn eq(&self, other: &Self) -> bool { + coordinate_equals(self.point.0, other.point.0) && coordinate_equals(self.point.0, other.point.0) + } +} + +impl Ord for Location { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let theta_comparison = compare_radians(self.radians, other.radians); + + if theta_comparison != Ordering::Equal { + return theta_comparison; + } + + compare_coordinate(self.distance, other.distance) + } +} + +impl PartialOrd for Location { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/workspaces/spatial-grid-map/src-rust/location_tests.rs b/workspaces/spatial-grid-map/src-rust/location_tests.rs new file mode 100644 index 00000000..f4942226 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/location_tests.rs @@ -0,0 +1,128 @@ +use std::collections::HashMap; + +use super::*; + +use insta::assert_debug_snapshot; +use rand::seq::SliceRandom; + +#[test] +fn center() { + assert_debug_snapshot!(Location::new(2, 1.0, (0.0, 0.0))); +} + +#[test] +fn top() { + assert_debug_snapshot!(Location::new(2, 1.0, (0.0, -7.5))); +} + +#[test] +fn top_right() { + assert_debug_snapshot!(Location::new(2, 1.0, (7.5, -7.5))); +} + +#[test] +fn right() { + assert_debug_snapshot!(Location::new(2, 1.0, (7.5, 0.0))); +} + +#[test] +fn bottom_right() { + assert_debug_snapshot!(Location::new(2, 1.0, (7.5, 7.5))); +} + +#[test] +fn bottom() { + assert_debug_snapshot!(Location::new(2, 1.0, (0.0, 7.5))); +} + +#[test] +fn bottom_left() { + assert_debug_snapshot!(Location::new(2, 1.0, (-7.5, 7.5))); +} + +#[test] +fn left() { + assert_debug_snapshot!(Location::new(2, 1.0, (-7.5, 0.0))); +} + +#[test] +fn top_left() { + assert_debug_snapshot!(Location::new(2, 1.0, (-7.5, -7.5))); +} + +#[test] +fn snap_x_just_under() { + assert_debug_snapshot!(Location::new(2, 1.0, (7.0 - TOLERANCE * 0.5, 7.0))); +} + +#[test] +fn snap_y_just_under() { + assert_debug_snapshot!(Location::new(2, 1.0, (7.0, 7.0 - TOLERANCE * 0.5))); +} + +#[test] +fn top_oob() { + assert_debug_snapshot!(Location::new(2, 1.0, (0.0, -8.1))); +} + +#[test] +fn right_oob() { + assert_debug_snapshot!(Location::new(2, 1.0, (8.0, 0.0))); +} + +#[test] +fn bottom_oob() { + assert_debug_snapshot!(Location::new(2, 1.0, (0.0, 8.0))); +} + +#[test] +fn left_oob() { + assert_debug_snapshot!(Location::new(2, 1.0, (-8.1, 0.0))); +} + +fn get_sorted_location_ids(locations: Vec<(&str, Location)>) -> Vec<&str> { + let locations_map_by_id: HashMap<&str, Location> = locations.iter().cloned().collect(); + + let locations_map_by_key: HashMap<(i64, i64), &str> = locations_map_by_id + .iter() + .map(|(id, location)| (location.key, *id)) + .collect(); + + let mut locations = locations_map_by_id.values().collect::>(); + + // Lets just make sure it's not in order first. + locations.shuffle(&mut rand::thread_rng()); + + // The actual logic we're testing here. + locations.sort(); + + let sorted_locations_ids: Vec<_> = locations + .iter() + .map(|location| locations_map_by_key[&location.key]) + .collect(); + + sorted_locations_ids +} + +#[test] +fn order_radians() { + assert_debug_snapshot!(get_sorted_location_ids(vec![ + ("top", Location::new(2, 1.0, (0.0, -7.5))), + ("top-right", Location::new(2, 1.0, (7.5, -7.5))), + ("right", Location::new(2, 1.0, (7.5, 0.0))), + ("bottom-right", Location::new(2, 1.0, (7.5, 7.5))), + ("bottom", Location::new(2, 1.0, (0.0, 7.5))), + ("bottom-left", Location::new(2, 1.0, (-7.5, 7.5))), + ("left", Location::new(2, 1.0, (-7.5, 0.0))), + ("top-left", Location::new(2, 1.0, (-7.5, -7.5))), + ])); +} + +#[test] +fn order_distance() { + assert_debug_snapshot!(get_sorted_location_ids(vec![ + ("center", Location::new(2, 1.0, (0.0, 0.0))), + ("top", Location::new(2, 1.0, (0.0, -7.5))), + ("top-right", Location::new(2, 1.0, (7.5, -7.5))), + ])); +} diff --git a/workspaces/spatial-grid-map/src-rust/math.rs b/workspaces/spatial-grid-map/src-rust/math.rs new file mode 100644 index 00000000..67227968 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/math.rs @@ -0,0 +1,59 @@ +#[path = "./math_tests.rs"] +#[cfg(test)] +mod tests; + +use std::cmp::Ordering; +use std::f64::consts::PI; + +const PRECISION_RADIAN: f64 = 0.0001; +const PRECISION_COORDINATE: f64 = 0.001; + +pub fn compare_f64(a: f64, b: f64, precision: f64) -> Ordering { + if (a - b).abs() <= precision + f64::EPSILON { + return Ordering::Equal; + } + + a.partial_cmp(&b).unwrap() +} + +pub fn compare_radians(a: f64, b: f64) -> std::cmp::Ordering { + compare_f64(a, b, PRECISION_RADIAN) +} + +pub fn compare_coordinate(a: f64, b: f64) -> std::cmp::Ordering { + compare_f64(a, b, PRECISION_COORDINATE) +} + +pub fn coordinate_equals(a: f64, b: f64) -> bool { + compare_coordinate(a, b) == Ordering::Equal +} + +pub fn round_coordinate(coordinate: f64) -> f64 { + let rounded = ((coordinate + f64::EPSILON) / PRECISION_COORDINATE).round() * PRECISION_COORDINATE; + + // Even though -0.0 == 0.0, we want to return 0.0 + // to avoid issues with hashing + if rounded == -0.0 { + return 0.0; + } + + rounded +} + +pub fn radian_to_degrees(radian: f64) -> u16 { + (((radian * 180.0 / PI) * 10.0).round() / 10.0) as u16 +} + +pub fn degrees_to_radian(degrees: u16) -> f64 { + degrees as f64 * PI / 180.0 +} + +pub fn normalize_radian(mut radian: f64) -> f64 { + radian += PI * 0.5; + + if radian < -PRECISION_RADIAN { + radian += PI * 2.0; + } + + radian +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom.snap new file mode 100644 index 00000000..6e160def --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (0.0, 7.5))" +--- +Location { + key: ( + 0, + 7, + ), + block_index: 7, + bit_index: 24, + point: ( + 0.0, + 7.5, + ), + distance: 7.5, + radians: 3.141592653589793, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_left.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_left.snap new file mode 100644 index 00000000..d0a221d7 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_left.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (-7.5, 7.5))" +--- +Location { + key: ( + -8, + 7, + ), + block_index: 5, + bit_index: 24, + point: ( + -7.5, + 7.5, + ), + distance: 10.606601717798213, + radians: 3.9269908169872414, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_oob.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_oob.snap new file mode 100644 index 00000000..f6402384 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_oob.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (0.0, 8.0))" +--- +Location { + key: ( + 0, + 8, + ), + block_index: 10, + bit_index: 0, + point: ( + 0.0, + 8.0, + ), + distance: 8.0, + radians: 3.141592653589793, + contained: false, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_right.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_right.snap new file mode 100644 index 00000000..493fcfab --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__bottom_right.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (7.5, 7.5))" +--- +Location { + key: ( + 7, + 7, + ), + block_index: 7, + bit_index: 31, + point: ( + 7.5, + 7.5, + ), + distance: 10.606601717798213, + radians: 2.356194490192345, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__center.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__center.snap new file mode 100644 index 00000000..3d25b8d9 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__center.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (0.0, 0.0))" +--- +Location { + key: ( + 0, + 0, + ), + block_index: 6, + bit_index: 0, + point: ( + 0.0, + 0.0, + ), + distance: 0.0, + radians: 0.0, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left.snap new file mode 100644 index 00000000..8e065108 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (-7.5, 0.0))" +--- +Location { + key: ( + -8, + 0, + ), + block_index: 4, + bit_index: 0, + point: ( + -7.5, + 0.0, + ), + distance: 7.5, + radians: 4.71238898038469, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left_oob.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left_oob.snap new file mode 100644 index 00000000..d991f264 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__left_oob.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (-8.1, 0.0))" +--- +Location { + key: ( + -8, + 0, + ), + block_index: 4, + bit_index: 0, + point: ( + -8.1, + 0.0, + ), + distance: 8.1, + radians: 4.71238898038469, + contained: false, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_distance.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_distance.snap new file mode 100644 index 00000000..54ab657d --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_distance.snap @@ -0,0 +1,9 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: sorted_locations_ids +--- +[ + "center", + "top", + "top-right", +] diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_radians.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_radians.snap new file mode 100644 index 00000000..396e4271 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__order_radians.snap @@ -0,0 +1,14 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: sorted_locations_ids +--- +[ + "top", + "top-right", + "right", + "bottom-right", + "bottom", + "bottom-left", + "left", + "top-left", +] diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right.snap new file mode 100644 index 00000000..c9d506a8 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (7.5, 0.0))" +--- +Location { + key: ( + 7, + 0, + ), + block_index: 6, + bit_index: 7, + point: ( + 7.5, + 0.0, + ), + distance: 7.5, + radians: 1.5707963267948966, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right_oob.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right_oob.snap new file mode 100644 index 00000000..586a2290 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__right_oob.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (8.0, 0.0))" +--- +Location { + key: ( + 8, + 0, + ), + block_index: 8, + bit_index: 0, + point: ( + 8.0, + 0.0, + ), + distance: 8.0, + radians: 1.5707963267948966, + contained: false, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_x_just_under.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_x_just_under.snap new file mode 100644 index 00000000..98981a01 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_x_just_under.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (7.0 - TOLERANCE * 0.5, 7.0))" +--- +Location { + key: ( + 7, + 7, + ), + block_index: 7, + bit_index: 31, + point: ( + 6.99992375, + 7.0, + ), + distance: 9.899441019866428, + radians: 2.35619993665058, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_y_just_under.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_y_just_under.snap new file mode 100644 index 00000000..eb729acf --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__snap_y_just_under.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (7.0, 7.0 - TOLERANCE * 0.5))" +--- +Location { + key: ( + 7, + 7, + ), + block_index: 7, + bit_index: 31, + point: ( + 7.0, + 6.99992375, + ), + distance: 9.899441019866428, + radians: 2.3561890437341098, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top.snap new file mode 100644 index 00000000..af419583 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (0.0, -7.5))" +--- +Location { + key: ( + 0, + -8, + ), + block_index: 2, + bit_index: 0, + point: ( + 0.0, + -7.5, + ), + distance: 7.5, + radians: 0.0, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_left.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_left.snap new file mode 100644 index 00000000..c39163b5 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_left.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (-7.5, -7.5))" +--- +Location { + key: ( + -8, + -8, + ), + block_index: 0, + bit_index: 0, + point: ( + -7.5, + -7.5, + ), + distance: 10.606601717798213, + radians: 5.497787143782138, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_oob.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_oob.snap new file mode 100644 index 00000000..103efad3 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_oob.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (0.0, -8.1))" +--- +Location { + key: ( + 0, + -8, + ), + block_index: 2, + bit_index: 0, + point: ( + 0.0, + -8.1, + ), + distance: 8.1, + radians: 0.0, + contained: false, +} diff --git a/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_right.snap b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_right.snap new file mode 100644 index 00000000..f6bb33c1 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/snapshots/spatial_grid_map__location__tests__top_right.snap @@ -0,0 +1,19 @@ +--- +source: workspaces/spatial-grid-map/src-rust/./location_tests.rs +expression: "Location::new(2, 1.0, (7.5, -7.5))" +--- +Location { + key: ( + 7, + -8, + ), + block_index: 2, + bit_index: 7, + point: ( + 7.5, + -7.5, + ), + distance: 10.606601717798213, + radians: 0.7853981633974483, + contained: true, +} diff --git a/workspaces/spatial-grid-map/src-rust/utils.rs b/workspaces/spatial-grid-map/src-rust/utils.rs new file mode 100644 index 00000000..b1edc5a2 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/utils.rs @@ -0,0 +1,67 @@ +#[path = "./utils_tests.rs"] +#[cfg(test)] +mod tests; + +use std::cmp::Ordering; +use std::f64::consts::PI; + +const PRECISION_RADIAN: f64 = 0.0001; +const PRECISION_COORDINATE: f64 = 0.001; + +pub fn compare_f64(a: f64, b: f64, precision: f64) -> Ordering { + if (a - b).abs() <= precision + f64::EPSILON { + return Ordering::Equal; + } + + a.partial_cmp(&b).unwrap() +} + +pub fn compare_radians(a: f64, b: f64) -> std::cmp::Ordering { + compare_f64(a, b, PRECISION_RADIAN) +} + +pub fn compare_coordinate(a: f64, b: f64) -> std::cmp::Ordering { + compare_f64(a, b, PRECISION_COORDINATE) +} + +pub fn coordinate_equals(a: f64, b: f64) -> bool { + compare_coordinate(a, b) == Ordering::Equal +} + +pub fn round_coordinate(coordinate: f64) -> f64 { + let rounded = ((coordinate + f64::EPSILON) / PRECISION_COORDINATE).round() * PRECISION_COORDINATE; + + // Even though -0.0 == 0.0, we want to return 0.0 + // to avoid issues with hashing + if rounded == -0.0 { + return 0.0; + } + + rounded +} + +pub fn radian_to_degrees(radian: f64) -> u16 { + (((radian * 180.0 / PI) * 10.0).round() / 10.0) as u16 +} + +pub fn degrees_to_radian(degrees: u16) -> f64 { + degrees as f64 * PI / 180.0 +} + +pub fn normalize_radian(mut radian: f64) -> f64 { + radian += PI * 0.5; + + if radian < -PRECISION_RADIAN { + radian += PI * 2.0; + } + + radian +} + +pub fn get_radians_for_x_y(x: f64, y: f64) -> f64 { + if coordinate_equals(x, 0.0) && coordinate_equals(y, 0.0) { + return 0.0; + } + + normalize_radian(y.atan2(x)) +} diff --git a/workspaces/spatial-grid-map/src-rust/utils_tests.rs b/workspaces/spatial-grid-map/src-rust/utils_tests.rs new file mode 100644 index 00000000..885d9774 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/utils_tests.rs @@ -0,0 +1,88 @@ +use super::*; + +#[test] +fn test_compare_coordinate() { + assert_eq!(compare_coordinate(0.0, 0.001), Ordering::Equal); + assert_ne!(compare_coordinate(0.0, 0.01), Ordering::Equal); + assert_eq!(compare_coordinate(0.0, 0.0), Ordering::Equal); +} + +#[test] +fn test_compare_radians() { + assert_eq!(compare_radians(0.0, 0.0001), Ordering::Equal); + assert_ne!(compare_radians(0.0, 0.001), Ordering::Equal); + assert_ne!(compare_radians(0.0, 0.01), Ordering::Equal); + assert_eq!(compare_radians(0.0, 0.0), Ordering::Equal); +} + +#[test] +fn test_round_coordinate() { + let tests = vec![ + (6.661338147750939e-16, -4.440892098500626e-16), + (-1.7320508075688774, -1.7320508075688772), + (-2.884444029575346e-16, 2.884444029575346e-16), + (-1.6653345369377358e-16, 1.6653345369377353e-16), + (1.5000000000000004, 1.5), + (-0.8660254037844375, -0.8660254037844386), + (-2.8844440295753455e-16, 2.884444029575345e-16), + (1.6653345369377348e-16, -1.6653345369377365e-16), + (-1.5000000000000004, -1.5000000000000002), + (-0.8660254037844378, -0.8660254037844379), + (-2.884444029575344e-16, 2.884444029575346e-16), + (1.6653345369377368e-16, -1.6653345369377348e-16), + (-5.551115123125783e-16, -3.3306690738754696e-16), + (-1.7320508075688767, -1.7320508075688774), + (-0.0, 0.0), + ]; + + for (a, b) in tests { + assert_eq!( + round_coordinate(a).to_string(), + round_coordinate(b).to_string() + ); + } + + let tests = vec![(0.5, 0.6)]; + + for (a, b) in tests { + assert_ne!( + round_coordinate(a).to_string(), + round_coordinate(b).to_string() + ); + } +} + +#[test] +fn test_normalize_radian() { + let neg: f64 = -1.0; + let zero: f64 = 0.0; + let pos: f64 = 1.0; + + // atan2 + assert!((neg.atan2(zero) - PI * -0.5).abs() < PRECISION_RADIAN); + assert!((neg.atan2(pos) - PI * -0.25).abs() < PRECISION_RADIAN); + assert!((zero.atan2(pos) - PI * 0.0).abs() < PRECISION_RADIAN); + assert!((pos.atan2(pos) - PI * 0.25).abs() < PRECISION_RADIAN); + assert!((pos.atan2(zero) - PI * 0.5).abs() < PRECISION_RADIAN); + assert!((pos.atan2(neg) - PI * 0.75).abs() < PRECISION_RADIAN); + assert!((zero.atan2(neg) - PI * 1.0).abs() < PRECISION_RADIAN); + assert!((neg.atan2(neg) - PI * -0.75).abs() < PRECISION_RADIAN); + + // normalize_radian + assert!((normalize_radian(neg.atan2(zero)) - PI * 0.0).abs() < PRECISION_RADIAN); + assert!((normalize_radian(neg.atan2(pos)) - PI * 0.25).abs() < PRECISION_RADIAN); + assert!((normalize_radian(zero.atan2(pos)) - PI * 0.5).abs() < PRECISION_RADIAN); + assert!((normalize_radian(pos.atan2(pos)) - PI * 0.75).abs() < PRECISION_RADIAN); + assert!((normalize_radian(pos.atan2(zero)) - PI * 1.0).abs() < PRECISION_RADIAN); + assert!((normalize_radian(pos.atan2(neg)) - PI * 1.25).abs() < PRECISION_RADIAN); + assert!((normalize_radian(zero.atan2(neg)) - PI * 1.5).abs() < PRECISION_RADIAN); + assert!((normalize_radian(neg.atan2(neg)) - PI * 1.75).abs() < PRECISION_RADIAN); +} + +#[test] +fn test_radian_to_degrees() { + assert_eq!(radian_to_degrees(PI), 180); + assert_eq!(radian_to_degrees(PI / 2.0), 90); + assert_eq!(radian_to_degrees(PI / 4.0), 45); + assert_eq!(radian_to_degrees(PI / 6.0), 30); +} diff --git a/workspaces/spatial-grid-map/src-rust/visitor.rs b/workspaces/spatial-grid-map/src-rust/visitor.rs new file mode 100644 index 00000000..66ad095a --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/visitor.rs @@ -0,0 +1,79 @@ +#[path = "./visitor_tests.rs"] +#[cfg(test)] +mod tests; + +pub struct Visitor { + center: (i64, i64), + radius: u8, + + current_radius: u8, + current_point: Option<(i64, i64)>, +} + +impl Visitor { + pub fn new(point: (i64, i64), radius: u8) -> Self { + Visitor { + center: point, + radius, + + current_radius: 0, + current_point: None, + } + } +} + +impl Iterator for Visitor { + type Item = (i64, i64); + + fn next(&mut self) -> Option { + let (cx, cy) = self.center; + let r = self.current_radius as i64; + + match self.current_point { + // The first point we visit is the center. + None => { + self.current_point = Some((cx, cy)); + } + // After the center, we visit the points on the spiral, + // starting from the top left corner. + Some((x, y)) if x == cx && y == cy => { + self.current_point = Some((cx - 1, cy - 1)); + self.current_radius = 1 + } + // Move right until we reach the top right corner. + Some((x, y)) if x < cx + r && y == cy - r => { + self.current_point = Some((x + 1, y)); + } + // Move down until we reach the bottom right corner. + Some((x, y)) if x == cx + r && y < cy + r => { + self.current_point = Some((x, y + 1)); + } + // Move left until we reach the bottom left corner. + Some((x, y)) if x > cx - r && y == cy + r => { + self.current_point = Some((x - 1, y)); + } + // Move up until we reach the top left corner. + Some((x, y)) if x == cx - r && y > cy - r + 1 => { + self.current_point = Some((x, y - 1)); + } + // Move to the next level of the spiral. + Some((x, y)) if x == cx - r && y == cy - r + 1 => { + // If we've reached the maximum radius, we're done. + if self.current_radius == self.radius { + return None; + } + + self.current_radius += 1; + self.current_point = Some(( + cx - self.current_radius as i64, + cy - self.current_radius as i64, + )); + } + _ => { + return None; + } + }; + + self.current_point + } +} diff --git a/workspaces/spatial-grid-map/src-rust/visitor_tests.rs b/workspaces/spatial-grid-map/src-rust/visitor_tests.rs new file mode 100644 index 00000000..6aa643c4 --- /dev/null +++ b/workspaces/spatial-grid-map/src-rust/visitor_tests.rs @@ -0,0 +1,82 @@ +use super::*; + +#[test] +fn with_radius_1() { + let visitor = Visitor::new((0, 0), 1); + let visited: Vec<_> = visitor.collect(); + + assert_eq!( + visited, + vec![ + (0, 0), + (-1, -1), + (0, -1), + (1, -1), + (1, 0), + (1, 1), + (0, 1), + (-1, 1), + (-1, 0) + ] + ); +} + +#[test] +fn with_radius_2() { + let visitor = Visitor::new((0, 0), 2); + let visited: Vec<_> = visitor.collect(); + + assert_eq!( + visited, + vec![ + (0, 0), + // r = 1 + (-1, -1), + (0, -1), + (1, -1), + (1, 0), + (1, 1), + (0, 1), + (-1, 1), + (-1, 0), + // r = 2 + (-2, -2), + (-1, -2), + (0, -2), + (1, -2), + (2, -2), + (2, -1), + (2, 0), + (2, 1), + (2, 2), + (1, 2), + (0, 2), + (-1, 2), + (-2, 2), + (-2, 1), + (-2, 0), + (-2, -1) + ] + ); +} + +#[test] +fn with_offset_center() { + let visitor = Visitor::new((10, 10), 1); + let visited: Vec<_> = visitor.collect(); + + assert_eq!( + visited, + vec![ + (10, 10), + (9, 9), + (10, 9), + (11, 9), + (11, 10), + (11, 11), + (10, 11), + (9, 11), + (9, 10) + ] + ); +} diff --git a/workspaces/spatial-grid-map/src/Article/index.tsx b/workspaces/spatial-grid-map/src/Article/index.tsx new file mode 100644 index 00000000..56f21104 --- /dev/null +++ b/workspaces/spatial-grid-map/src/Article/index.tsx @@ -0,0 +1,16 @@ +import { ArticlePage } from '@hogg/common'; +import { ArticleHeading, ArticleParagraph, ArticleSection } from 'preshape'; + +const Article = () => { + return ( + + + Introduction + + Foo + + + ); +}; + +export default Article; diff --git a/workspaces/spatial-grid-map/src/Project.tsx b/workspaces/spatial-grid-map/src/Project.tsx new file mode 100644 index 00000000..cde27e94 --- /dev/null +++ b/workspaces/spatial-grid-map/src/Project.tsx @@ -0,0 +1,6 @@ +import { ProjectPage, ProjectPageProps } from '@hogg/common'; +import Article from './Article'; + +export default function Project(props: ProjectPageProps) { + return } />; +} diff --git a/workspaces/spatial-grid-map/src/index.ts b/workspaces/spatial-grid-map/src/index.ts new file mode 100644 index 00000000..8ba80c5c --- /dev/null +++ b/workspaces/spatial-grid-map/src/index.ts @@ -0,0 +1,15 @@ +import { type Project, ProjectKey } from '@hogg/common'; + +export { default as Project } from './Project'; + +export const meta: Project = { + id: ProjectKey.spatial_grid_map, + name: 'Subdividing a tiling plane for efficient lookups and collision detection', + description: + 'A method for subdividing a euclidean tiling plane into a grid of bits for quickly checking an area for the existence of polygon components and performing collision detection', + tags: ['algorithms', 'data structures', 'rust'], + deploy: false, + wip: true, + created: '2024-11-03', + updated: '2024-11-03', +}; diff --git a/workspaces/spirals/package.json b/workspaces/spirals/package.json index 489d71e7..d8189ae4 100644 --- a/workspaces/spirals/package.json +++ b/workspaces/spirals/package.json @@ -5,21 +5,26 @@ "private": true, "type": "module", "main": "./src/index.ts", - "devDependencies": { - "@types/lodash": "^4.14.202", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "caniuse-lite": "^1.0.30000697", - "typescript": "^5.0.4" - }, "dependencies": { "@hogg/common": "workspace:^", + "@visx/axis": "^3.5.0", + "@visx/curve": "^3.3.0", + "@visx/scale": "^3.5.0", + "@visx/shape": "^3.5.0", + "@visx/xychart": "^3.5.1", "gl-matrix": "^3.4.3", "lodash": "^4.17.21", - "preshape": "^19.1.0", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", "regl": "^2.1.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.202", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "caniuse-lite": "^1.0.30000697", + "typescript": "^5.0.4" } } diff --git a/workspaces/tilings/.DS_Store b/workspaces/tilings/.DS_Store new file mode 100644 index 00000000..12569d96 Binary files /dev/null and b/workspaces/tilings/.DS_Store differ diff --git a/workspaces/tilings/package.json b/workspaces/tilings/package.json index 8ad8c04c..8700d2bd 100644 --- a/workspaces/tilings/package.json +++ b/workspaces/tilings/package.json @@ -11,10 +11,10 @@ }, "scripts": { "build": "yarn clean && yarn build:types", - "build:types": "typeshare src-rust --lang=typescript --output-file=./src/types.ts", + "build:types": "typeshare --lang=typescript --output-file=./src/types.ts ./src-rust ../circular-sequence ../spatial-grid-map", "clean": "rm -rf ./logs", - "dev": "cargo watch -w workspaces/circular-sequence/src-rust -w workspaces/tilings/src-rust -q -s \"yarn workspace @hogg/tilings run build:types\"", - "dev:searcher": "cargo watch -w workspaces/circular-sequence/src-rust -w workspaces/tilings/src-rust -q -x \"run --bin tiling-searcher -- --drop-outstanding --reset --migrations-dir=workspaces/tilings/migrations\"", + "dev": "cargo watch -w workspaces/spatial-grid-map/src-rust -w workspaces/circular-sequence/src-rust -w workspaces/line-segment-extending/src-rust -w workspaces/tilings/src-rust -q -s \"yarn workspace @hogg/tilings run build:types\"", + "dev:searcher": "cargo watch -w workspaces/spatial-grid-map/src-rust -w workspaces/circular-sequence/src-rust-w workspaces/line-segment-extending/src-rust -w workspaces/tilings/src-rust -q -x \"run --bin tiling-searcher -- --drop-outstanding --reset --migrations-dir=workspaces/tilings/migrations\"", "export:results": "cargo run --release --bin tiling-export", "generate:images": "node ./scripts/generate-images.js", "generate:results": "cargo run --release --bin tiling-searcher -- --reset --log-to-file" @@ -22,8 +22,15 @@ "dependencies": { "@hogg/common": "workspace:^", "@hogg/wasm": "workspace:^", - "lucide-react": "0.439.0", - "preshape": "^19.1.0", + "@visx/axis": "^3.5.0", + "@visx/curve": "^3.3.0", + "@visx/scale": "^3.5.0", + "@visx/shape": "^3.5.0", + "@visx/xychart": "^3.5.1", + "lodash": "^4.17.21", + "lucide-react": "0.447.0", + "open-color": "^1.9.1", + "preshape": "^19.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", @@ -32,6 +39,7 @@ "devDependencies": { "@swc-node/register": "^1.8.0", "@swc/core": "^1.4.2", + "@types/lodash": "^4.17.10", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "puppeteer": "23.3.0", diff --git a/workspaces/tilings/src-rust/.DS_Store b/workspaces/tilings/src-rust/.DS_Store new file mode 100644 index 00000000..d766efc1 Binary files /dev/null and b/workspaces/tilings/src-rust/.DS_Store differ diff --git a/workspaces/tilings/src-rust/datastore/src/tilings/insert.rs b/workspaces/tilings/src-rust/datastore/src/tilings/insert.rs index 1ec966c7..b7884959 100644 --- a/workspaces/tilings/src-rust/datastore/src/tilings/insert.rs +++ b/workspaces/tilings/src-rust/datastore/src/tilings/insert.rs @@ -72,8 +72,8 @@ pub async fn insert(pool: &Pool, request: InsertRequest) -> Result<()> .bind(has_12) .bind(path_index) .bind(t.transform_index) - .bind(t.uniform) - .bind(t.hash) + // .bind(t.uniform) // TODO: implement uniform or remove from db + // .bind(t.hash) // TODO: implement hash or remove from db .execute(pool) }); diff --git a/workspaces/tilings/src-rust/renderer/Cargo.toml b/workspaces/tilings/src-rust/renderer/Cargo.toml index 52a68a82..10476104 100644 --- a/workspaces/tilings/src-rust/renderer/Cargo.toml +++ b/workspaces/tilings/src-rust/renderer/Cargo.toml @@ -25,8 +25,10 @@ web-sys = { workspace = true, features = [ 'OffscreenCanvasRenderingContext2d', ] } +circular_sequence = { path = "../../../circular-sequence" } colorgrad = "0.7.0" # This is needed as a dependecy for wasm-bindgen getrandom = { version = "0.2", features = ["js"] } rand = "0.8.5" +spatial_grid_map = { path = "../../../spatial-grid-map" } tiling = { path = "../tiling" } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/collision.rs b/workspaces/tilings/src-rust/renderer/src/canvas/collision.rs index 02890408..2e73bb57 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/collision.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/collision.rs @@ -1,17 +1,17 @@ +use spatial_grid_map::{ResizeMethod, SpatialGridMap}; use tiling::geometry::BBox; use super::component::{Component, Draw}; use super::Scale; -use crate::Error; pub struct Theia { - components: Vec, + components: SpatialGridMap, } impl Theia { pub fn new() -> Self { Self { - components: Vec::new(), + components: SpatialGridMap::default().with_resize_method(ResizeMethod::Maximum), } } @@ -22,21 +22,29 @@ impl Theia { content_bbox: &BBox, scale: &Scale, a: &Component, - ) -> Result { - for b in self.components.iter() { - if a.bbox(context, canvas_bbox, content_bbox, scale)? - == b.bbox(context, canvas_bbox, content_bbox, scale)? - { + ) -> bool { + if a.interactive() == Some(false) { + return false; + } + + let a_bbox = a.inner().bbox(context, canvas_bbox, content_bbox, scale); + let a_centroid: (f64, f64) = a_bbox.centroid().into(); + let a_size = a_bbox.height().max(a_bbox.width()); + + let nearby_boxes = self.components.iter_values_around(&a_centroid, 1); + + for b_bbox in nearby_boxes { + if a_bbox == *b_bbox { continue; } - if a.collides_with(context, canvas_bbox, content_bbox, scale, b)? { - return Ok(true); + if a_bbox.intersects(b_bbox) { + return true; } } - self.components.push(a.clone()); + self.components.insert(a_centroid, a_size, a_bbox); - Ok(false) + false } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/arc.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/arc.rs index b6694e5b..57a24d82 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/arc.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/arc.rs @@ -2,22 +2,47 @@ use std::f64::consts::PI; use tiling::geometry::{BBox, Point}; -use super::{Chevron, Component, Draw, Style}; +use super::{Draw, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Arc { - pub point: Point, - pub radius: f64, - pub start_angle: f64, - pub end_angle: f64, - pub style: Style, + point: Point, + radius: f64, + start_angle: f64, + end_angle: f64, + style: Style, } impl Arc { - fn get_radius(&self, scale: &Scale) -> f64 { + pub fn with_point(mut self, point: Point) -> Self { + self.point = point; + self + } + + pub fn with_radius(mut self, radius: f64) -> Self { + self.radius = radius; + self + } + + pub fn with_start_angle(mut self, start_angle: f64) -> Self { + self.start_angle = start_angle; + self + } + + pub fn with_end_angle(mut self, end_angle: f64) -> Self { + self.end_angle = end_angle; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + + pub fn get_radius(&self, scale: &Scale) -> f64 { let line_thickness = self.style.get_line_thickness(scale); let chevron_size = self.style.get_chevron_size(scale); let stroke_width = self.style.get_stroke_width(scale); @@ -34,17 +59,6 @@ impl Arc { self.end_angle - PI * 0.5 } - fn get_chevron(&self, scale: &Scale) -> Chevron { - Chevron { - point: self - .point - .translate(&Point::default().with_xy(0.0, -self.get_radius(scale))) - .rotate(self.end_angle, Some(&self.point)), - direction: self.end_angle, - style: self.style.clone(), - } - } - fn draw_path( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, @@ -69,11 +83,11 @@ impl Arc { impl Draw for Arc { fn bbox( &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, - canvas_bbox: &BBox, - content_bbox: &BBox, - scale: &Scale, - ) -> Result { + _context: &web_sys::OffscreenCanvasRenderingContext2d, + _canvas_bbox: &BBox, + _content_bbox: &BBox, + _scale: &Scale, + ) -> BBox { let aa = self.start_angle - PI * 0.5; let ea = self.end_angle - PI * 0.5; @@ -106,20 +120,13 @@ impl Draw for Arc { let x_max = if abm * abe > 0.0 { ex } else { ax.max(bx) }; let y_max = if abm * abn > 0.0 { ny } else { ay.max(by) }; - let min = Point::default().with_xy(x_min, y_min); - let max = Point::default().with_xy(x_max, y_max); - let bbox = BBox::default().with_min(min).with_max(max); - - Ok( - bbox.union( - &self - .get_chevron(scale) - .bbox(context, canvas_bbox, content_bbox, scale)?, - ), - ) + let min = Point::at(x_min, y_min); + let max = Point::at(x_max, y_max); + + BBox::from_min_max(min, max) } - fn component(&self) -> Component { + fn component(&self) -> super::Component { self.clone().into() } @@ -130,10 +137,10 @@ impl Draw for Arc { fn draw( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, - canvas_bbox: &BBox, - content_bbox: &BBox, + _canvas_bbox: &BBox, + _content_bbox: &BBox, scale: &Scale, - theia: &mut Theia, + _theia: &mut Theia, ) -> Result<(), Error> { let line_thickness = self.style.get_line_thickness(scale); let stroke_width = self.style.get_stroke_width(scale); @@ -153,14 +160,10 @@ impl Draw for Arc { &self .style .set_fill(None) - .set_stroke_color(self.style.get_fill()) + .set_stroke_color(Some(self.style.get_fill())) .set_stroke_width(scale, Some(line_thickness)), )?; - self - .get_chevron(scale) - .draw(context, canvas_bbox, content_bbox, scale, theia)?; - Ok(()) } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/arc_arrow.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/arc_arrow.rs new file mode 100644 index 00000000..99b301fc --- /dev/null +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/arc_arrow.rs @@ -0,0 +1,112 @@ +use tiling::geometry::{BBox, Point}; + +use super::{Arc, Chevron, Draw, Style}; +use crate::canvas::Scale; + +#[derive(Clone, Debug, Default)] +pub struct ArcArrow { + point: Point, + radius: f64, + start_angle: f64, + end_angle: f64, + style: Style, +} + +impl ArcArrow { + pub fn with_point(mut self, point: Point) -> Self { + self.point = point; + self + } + + pub fn with_radius(mut self, radius: f64) -> Self { + self.radius = radius; + self + } + + pub fn with_start_angle(mut self, start_angle: f64) -> Self { + self.start_angle = start_angle; + self + } + + pub fn with_end_angle(mut self, end_angle: f64) -> Self { + self.end_angle = end_angle; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } +} + +// https://stackoverflow.com/questions/77798747/how-to-calculate-the-bounding-box-of-an-arc +impl Draw for ArcArrow { + fn bbox( + &self, + context: &web_sys::OffscreenCanvasRenderingContext2d, + canvas_bbox: &BBox, + content_bbox: &BBox, + scale: &Scale, + ) -> BBox { + let arc = Arc::default() + .with_point(self.point) + .with_radius(self.radius) + .with_start_angle(self.start_angle) + .with_end_angle(self.end_angle) + .with_style(self.style.clone()); + + let chevron = Chevron::default() + .with_point( + self + .point + .translate(&Point::at(0.0, -arc.get_radius(scale))) + .rotate(self.end_angle, Some(&self.point)), + ) + .with_direction(self.end_angle) + .with_style(self.style.clone()); + + let arc_bbox = arc.bbox(context, canvas_bbox, content_bbox, scale); + let chevron_bbox = chevron.bbox(context, canvas_bbox, content_bbox, scale); + + arc_bbox.union(&chevron_bbox) + } + + fn component(&self) -> super::Component { + self.clone().into() + } + + fn draw( + &self, + context: &web_sys::OffscreenCanvasRenderingContext2d, + canvas_bbox: &BBox, + content_bbox: &BBox, + scale: &Scale, + theia: &mut crate::canvas::collision::Theia, + ) -> Result<(), crate::Error> { + let arc = Arc::default() + .with_point(self.point) + .with_radius(self.radius) + .with_start_angle(self.start_angle) + .with_end_angle(self.end_angle) + .with_style(self.style.clone()); + + let chevron = Chevron::default() + .with_point( + self + .point + .translate(&Point::at(0.0, -arc.get_radius(scale))) + .rotate(self.end_angle, Some(&self.point)), + ) + .with_direction(self.end_angle) + .with_style(self.style.clone()); + + arc.draw(context, canvas_bbox, content_bbox, scale, theia)?; + chevron.draw(context, canvas_bbox, content_bbox, scale, theia)?; + + Ok(()) + } + + fn style(&self) -> &Style { + &self.style + } +} diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/arrow.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/arrow.rs index 77b7e0b0..9d5d3b84 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/arrow.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/arrow.rs @@ -2,44 +2,48 @@ use std::f64::consts::PI; use tiling::geometry::BBox; -use super::{Chevron, Component, Draw, LineSegment, Style}; +use super::{Chevron, Draw, LineSegment, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Arrow { - pub line_segment: tiling::geometry::LineSegment, - pub style: Style, + line_segment: tiling::geometry::LineSegment, + style: Style, } impl Arrow { + pub fn with_line_segment(mut self, line_segment: tiling::geometry::LineSegment) -> Self { + self.line_segment = line_segment; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + fn get_chevron(&self, scale: &Scale) -> Chevron { let direction = self.line_segment.p2.radian_to(&self.line_segment.p1) - PI * 0.5; let point = self.line_segment.p2; - Chevron { - point, - direction, - style: self.style.set_line_dash(scale, None), - } + Chevron::default() + .with_point(point) + .with_direction(direction) + .with_style(self.style.set_line_dash(scale, None)) } fn get_line_segment(&self, scale: &Scale) -> LineSegment { - LineSegment { - points: self.line_segment.into(), - extend_start: false, - extend_end: false, - style: self.style.set_line_dash(scale, None), - } + LineSegment::default() + .with_points(self.line_segment.into()) + .with_extend_start(false) + .with_extend_end(false) + .with_style(self.style.set_line_dash(scale, None)) } } impl Draw for Arrow { - fn component(&self) -> Component { - self.clone().into() - } - fn style(&self) -> &Style { &self.style } @@ -50,45 +54,20 @@ impl Draw for Arrow { canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - ) -> Result { + ) -> BBox { let chevron = self .get_chevron(scale) - .bbox(context, canvas_bbox, content_bbox, scale)?; + .bbox(context, canvas_bbox, content_bbox, scale); - let line_segment = - self - .get_line_segment(scale) - .bbox(context, canvas_bbox, content_bbox, scale)?; + let line_segment = self + .get_line_segment(scale) + .bbox(context, canvas_bbox, content_bbox, scale); - // TODO: Is this 0.15 pad legit? - Ok(chevron.union(&line_segment).pad(0.15)) + line_segment.union(&chevron) } - fn collides_with( - &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, - canvas_bbox: &BBox, - content_bbox: &BBox, - scale: &Scale, - other: &Component, - ) -> Result { - if match other { - Component::Arrow(arrow) => arrow - .bbox(context, canvas_bbox, content_bbox, scale)? - .intersects_bbox(&self.bbox(context, canvas_bbox, content_bbox, scale)?), - Component::Point(point) => point - .bbox(context, canvas_bbox, content_bbox, scale)? - .intersects_bbox(&self.bbox(context, canvas_bbox, content_bbox, scale)?), - Component::LineSegment(line_segment) => line_segment.intersects_bbox( - content_bbox, - &self.bbox(context, canvas_bbox, content_bbox, scale)?, - ), - _ => false, - } { - return Ok(true); - } - - Ok(false) + fn component(&self) -> super::Component { + self.clone().into() } fn draw( @@ -99,14 +78,13 @@ impl Draw for Arrow { scale: &Scale, theia: &mut Theia, ) -> Result<(), Error> { - if !theia.has_collision(context, canvas_bbox, content_bbox, scale, &self.component())? { - self - .get_line_segment(scale) - .draw(context, canvas_bbox, content_bbox, scale, theia)?; - self - .get_chevron(scale) - .draw(context, canvas_bbox, content_bbox, scale, theia)?; - } + self + .get_line_segment(scale) + .draw(context, canvas_bbox, content_bbox, scale, theia)?; + + self + .get_chevron(scale) + .draw(context, canvas_bbox, content_bbox, scale, theia)?; Ok(()) } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/chevron.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/chevron.rs index b7a0c3b5..c62f6d66 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/chevron.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/chevron.rs @@ -2,64 +2,72 @@ use std::f64::consts::PI; use tiling::geometry::{BBox, Point}; -use super::{Component, Draw, LineSegment, Style}; +use super::{Draw, LineSegment, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Chevron { - pub point: Point, - pub direction: f64, - pub style: Style, + point: Point, + direction: f64, + style: Style, } impl Chevron { + pub fn with_point(mut self, point: Point) -> Self { + self.point = point; + self + } + + pub fn with_direction(mut self, direction: f64) -> Self { + self.direction = direction; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + fn get_points(&self, scale: &Scale) -> Vec { let size = self.style.get_chevron_size(scale); - let point_1 = Point::default() - .with_xy(self.point.x - size, self.point.y - size) + let point_1 = Point::at(self.point.x - size, self.point.y - size) .rotate(self.direction - PI * 0.5, Some(&self.point)); - let point_2 = Point::default().with_xy(self.point.x, self.point.y); - let point_3 = Point::default() - .with_xy(self.point.x + size, self.point.y - size) + let point_2 = Point::at(self.point.x, self.point.y); + let point_3 = Point::at(self.point.x + size, self.point.y - size) .rotate(self.direction - PI * 0.5, Some(&self.point)); vec![point_1, point_2, point_3] } fn get_line_segment(&self, scale: &Scale) -> LineSegment { - LineSegment { - points: self.get_points(scale), - extend_start: false, - extend_end: false, - style: self.style.set_line_dash(scale, None), - } + LineSegment::default() + .with_points(self.get_points(scale)) + .with_extend_start(false) + .with_extend_end(false) + .with_style(self.style.set_line_dash(scale, None)) } } impl Draw for Chevron { - fn component(&self) -> Component { - self.clone().into() - } - - fn style(&self) -> &Style { - &self.style - } - fn bbox( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - ) -> Result { + ) -> BBox { self .get_line_segment(scale) .bbox(context, canvas_bbox, content_bbox, scale) } + fn component(&self) -> super::Component { + self.clone().into() + } + fn draw( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, @@ -70,7 +78,10 @@ impl Draw for Chevron { ) -> Result<(), Error> { self .get_line_segment(scale) - .draw(context, canvas_bbox, content_bbox, scale, theia)?; - Ok(()) + .draw(context, canvas_bbox, content_bbox, scale, theia) + } + + fn style(&self) -> &Style { + &self.style } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/draw.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/draw.rs index 70450c96..e22c8f45 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/draw.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/draw.rs @@ -1,4 +1,4 @@ -use tiling::geometry::BBox; +use tiling::geometry::{BBox, Point}; use super::{Component, Style}; use crate::canvas::collision::Theia; @@ -12,25 +12,21 @@ pub trait Draw { _canvas_bbox: &BBox, _content_bbox: &BBox, _scale: &Scale, - ) -> Result { - Ok(BBox::default()) + ) -> BBox { + BBox::default() } - fn component(&self) -> Component; - - fn style(&self) -> &Style; - - fn collides_with( + fn children( &self, - _context: &web_sys::OffscreenCanvasRenderingContext2d, _canvas_bbox: &BBox, _content_bbox: &BBox, _scale: &Scale, - _other: &Component, - ) -> Result { - Ok(false) + ) -> Option>> { + None } + fn component(&self) -> Component; + fn draw_start( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, @@ -57,11 +53,20 @@ pub trait Draw { scale: &Scale, style: &Style, ) -> Result<(), Error> { - let bbox = self.bbox(context, canvas_bbox, content_bbox, scale)?; + let bbox = self.bbox(context, canvas_bbox, content_bbox, scale); + let points: [Point; 4] = (&bbox).into(); + + self.draw_start(context, scale, style)?; + + for (index, point) in points.iter().enumerate() { + match index { + 0 => context.move_to(point.x, point.y), + _ => context.line_to(point.x, point.y), + } + } + + context.line_to(points[0].x, points[0].y); - style.apply(context, scale)?; - context.begin_path(); - context.rect(bbox.min.x, bbox.min.y, bbox.width(), bbox.height()); self.draw_end(context); Ok(()) @@ -77,4 +82,10 @@ pub trait Draw { ) -> Result<(), Error> { Ok(()) } + + fn interactive(&self) -> Option { + Some(true) + } + + fn style(&self) -> &Style; } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/grid.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/grid.rs new file mode 100644 index 00000000..6056bc16 --- /dev/null +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/grid.rs @@ -0,0 +1,138 @@ +use tiling::geometry::{BBox, Point}; + +use super::{Draw, Style}; +use crate::canvas::collision::Theia; +use crate::canvas::Scale; +use crate::Error; + +#[derive(Clone, Debug, Default)] +pub struct Grid { + size: u64, + spacing: f64, + style: Style, +} + +impl Grid { + pub fn with_size(mut self, size: u64) -> Self { + self.size = size; + self + } + + pub fn with_spacing(mut self, spacing: f64) -> Self { + self.spacing = spacing; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } +} + +impl Grid { + fn draw_line( + &self, + context: &web_sys::OffscreenCanvasRenderingContext2d, + scale: &Scale, + start: Point, + end: Point, + style: &Style, + thick_line: bool, + ) -> Result<(), Error> { + let style = if thick_line { + style.clone().set_opacity(Some(1.0)) + } else { + style.clone() + }; + + self.draw_start(context, scale, &style)?; + context.move_to(start.x, start.y); + context.line_to(end.x, end.y); + self.draw_end(context); + Ok(()) + } + + fn draw_grid( + &self, + context: &web_sys::OffscreenCanvasRenderingContext2d, + scale: &Scale, + style: &Style, + offset: Option, + ) -> Result<(), Error> { + let Point { + x: offset_x, + y: offset_y, + .. + } = offset.unwrap_or_default(); + + let min_x = ((self.size as f64) * -0.5) * self.spacing + offset_x; + let max_x = ((self.size as f64) * 0.5) * self.spacing + offset_x; + let min_y = ((self.size as f64) * -0.5) * self.spacing + offset_y; + let max_y = ((self.size as f64) * 0.5) * self.spacing + offset_y; + + // Draw horizontal lines + for i in 0..=self.size { + let thick_line = i % 8 == 0; + let y = min_y + (i as f64) * self.spacing; + + self.draw_line( + context, + scale, + Point::at(min_x, y), + Point::at(max_x, y), + style, + thick_line, + )?; + } + + // Draw vertical lines + for i in 0..=self.size { + let thick_line = i % 8 == 0; + let x = min_x + (i as f64) * self.spacing; + + self.draw_line( + context, + scale, + Point::at(x, min_y), + Point::at(x, max_y), + style, + thick_line, + )?; + } + + Ok(()) + } +} + +impl Draw for Grid { + fn component(&self) -> super::Component { + self.clone().into() + } + + fn style(&self) -> &Style { + &self.style + } + + fn draw( + &self, + context: &web_sys::OffscreenCanvasRenderingContext2d, + _canvas_bbox: &BBox, + _content_bbox: &BBox, + scale: &Scale, + _theia: &mut Theia, + ) -> Result<(), Error> { + self.draw_grid( + context, + scale, + &self + .style + .set_opacity(None) + .set_stroke_color(Some(self.style.get_fill())), + None, + )?; + + self.draw_grid(context, scale, &self.style, None)?; + + Ok(()) + } +} diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment.rs index ead81f7c..57c1cfe1 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment.rs @@ -1,19 +1,45 @@ use tiling::geometry::{BBox, Point}; -use super::{Component, Draw, Style}; +use super::{Draw, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct LineSegment { - pub points: Vec, - pub extend_start: bool, - pub extend_end: bool, - pub style: Style, + points: Vec, + extend_start: bool, + extend_end: bool, + interactive: Option, + style: Style, } impl LineSegment { + pub fn non_interactive(mut self) -> Self { + self.interactive = Some(false); + self + } + + pub fn with_extend_start(mut self, extend_start: bool) -> Self { + self.extend_start = extend_start; + self + } + + pub fn with_extend_end(mut self, extend_end: bool) -> Self { + self.extend_end = extend_end; + self + } + + pub fn with_points(mut self, points: Vec) -> Self { + self.points = points; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + fn get_points(&self, bbox: &BBox) -> Vec { get_extended_points_to_bbox(&self.points, bbox, self.extend_start, self.extend_end) } @@ -38,29 +64,9 @@ impl LineSegment { self.draw_end(context); Ok(()) } - - pub fn intersects_bbox(&self, content_bbox: &BBox, bbox: &BBox) -> bool { - for window in self.get_points(content_bbox).windows(2) { - let a = &window[0]; - let b = &window[1]; - let line_segment = tiling::geometry::LineSegment::default() - .with_start(*a) - .with_end(*b); - - if line_segment.intersects_bbox(bbox) { - return true; - } - } - - false - } } impl Draw for LineSegment { - fn component(&self) -> Component { - self.clone().into() - } - fn style(&self) -> &Style { &self.style } @@ -71,37 +77,41 @@ impl Draw for LineSegment { _canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - ) -> Result { - let mut min = Point::default().with_xy(f64::INFINITY, f64::INFINITY); - let mut max = Point::default().with_xy(f64::NEG_INFINITY, f64::NEG_INFINITY); - - for point in self.get_points(content_bbox) { - if point.x < min.x { - min.x = point.x - } - - if point.x > max.x { - max.x = point.x - } - - if point.y < min.y { - min.y = point.y + ) -> BBox { + let points = self.get_points(content_bbox); + let line_thickness = self.style.get_line_thickness(scale); + let stroke_width = self.style.get_stroke_width(scale); + let width = line_thickness + stroke_width; + + match points.len() { + 0 => BBox::default(), + 1 => BBox::default().with_center(points[0]), + 2 => { + let a = points[0]; + let b = points[1]; + let line_segment = tiling::geometry::LineSegment::default() + .with_start(a) + .with_end(b) + .extend_to_bbox(content_bbox, self.extend_start, self.extend_end); + + BBox::default() + .with_center(line_segment.mid_point()) + .with_height(line_segment.length() + width) + .with_width(width) + .with_rotation(line_segment.theta()) } + _ => { + let bbox: BBox = (&points).into(); - if point.y > max.y { - max.y = point.y + bbox + .with_width(bbox.width() + width) + .with_height(bbox.height() + width) } } + } - let line_thickness = self.style.get_line_thickness(scale) * 0.5; - let stroke_width = self.style.get_stroke_width(scale) * 0.5; - let offset = line_thickness + stroke_width; - - Ok( - BBox::default() - .with_min(min.translate(&Point::default().with_xy(-offset, -offset))) - .with_max(max.translate(&Point::default().with_xy(offset, offset))), - ) + fn component(&self) -> super::Component { + self.clone().into() } fn draw( @@ -134,12 +144,16 @@ impl Draw for LineSegment { &self .style .set_fill(None) - .set_stroke_color(self.style.get_fill()) + .set_stroke_color(Some(self.style.get_fill())) .set_stroke_width(scale, Some(line_thickness)), )?; Ok(()) } + + fn interactive(&self) -> Option { + self.interactive + } } pub fn get_extended_points_to_bbox( @@ -166,6 +180,7 @@ pub fn get_extended_points_to_bbox( .with_start(points[points.len() - 2]) .with_end(points[points.len() - 1]) .extend_to_bbox(bbox, false, extend_end); + point = line_segment.p2; } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment_arrows.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment_arrows.rs index f9309eee..37714d01 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment_arrows.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/line_segment_arrows.rs @@ -1,33 +1,53 @@ use tiling::geometry::{BBox, LineSegment, LineSegmentOrigin}; -use super::{Arrow, Component, Draw, Style}; -use crate::canvas::collision::Theia; +use super::{Arrow, Draw, Style}; use crate::canvas::Scale; -use crate::Error; const GAP_BETWEEN_ARROWS_MULTIPLIER: f64 = 10.0; const GAP_FROM_LINE_SEGMENT: f64 = 2.0; const ARROW_LENGTH: f64 = 2.5; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct LineSegmentArrows { - pub line_segment: LineSegment, - pub extend_start: bool, - pub extend_end: bool, - pub direction: f64, - pub style: Style, + line_segment: LineSegment, + extend_start: bool, + extend_end: bool, + direction: f64, + style: Style, } impl LineSegmentArrows { - fn get_extended_line_segment(&self, bbox: &BBox) -> LineSegment { + pub fn with_line_segment(mut self, line_segment: LineSegment) -> Self { + self.line_segment = line_segment; + self + } + + pub fn with_extend_start(mut self, extend_start: bool) -> Self { + self.extend_start = extend_start; + self + } + + pub fn with_extend_end(mut self, extend_end: bool) -> Self { + self.extend_end = extend_end; + self + } + + pub fn with_direction(mut self, direction: f64) -> Self { + self.direction = direction; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; self - .line_segment - .extend_to_bbox(bbox, self.extend_start, self.extend_end) } fn get_arrows(&self, _canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale) -> Vec { let chevron_size = self.style.get_chevron_size(scale); - let line_segment = self.get_extended_line_segment(content_bbox); + let line_segment = + self + .line_segment + .extend_to_bbox(content_bbox, self.extend_start, self.extend_end); let gap_between_arrows = chevron_size * GAP_BETWEEN_ARROWS_MULTIPLIER; let gap_from_line_segment = chevron_size * GAP_FROM_LINE_SEGMENT; @@ -59,10 +79,11 @@ impl LineSegmentArrows { LineSegmentOrigin::End, ); - arrows.push(Arrow { - line_segment: arrow_line_segment, - style: self.style.clone(), - }); + arrows.push( + Arrow::default() + .with_line_segment(arrow_line_segment) + .with_style(self.style.clone()), + ); shift += gap_between_arrows; } @@ -72,63 +93,26 @@ impl LineSegmentArrows { } impl Draw for LineSegmentArrows { - fn component(&self) -> Component { - self.clone().into() - } - - fn style(&self) -> &Style { - &self.style - } - - fn bbox( + fn children( &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - ) -> Result { - let mut bbox = self - .get_arrows(canvas_bbox, content_bbox, scale) - .iter() - .map(|arrow| { - arrow - .bbox(context, canvas_bbox, content_bbox, scale) - .unwrap_or_default() - }) - .reduce(|a, b| a.union(&b)) - .unwrap_or_default(); - - bbox = bbox.union(&self.get_extended_line_segment(canvas_bbox).bbox()); - Ok(bbox) + ) -> Option>> { + Some( + self + .get_arrows(canvas_bbox, content_bbox, scale) + .into_iter() + .map(|arrow| Box::new(arrow) as Box) + .collect::>(), + ) } - fn draw_bbox( - &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, - canvas_bbox: &BBox, - content_bbox: &BBox, - scale: &Scale, - style: &Style, - ) -> Result<(), Error> { - for arrow in self.get_arrows(canvas_bbox, content_bbox, scale).iter() { - arrow.draw_bbox(context, canvas_bbox, content_bbox, scale, style)?; - } - - Ok(()) + fn component(&self) -> super::Component { + self.clone().into() } - fn draw( - &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, - canvas_bbox: &BBox, - content_bbox: &BBox, - scale: &Scale, - theia: &mut Theia, - ) -> Result<(), Error> { - for arrow in self.get_arrows(canvas_bbox, content_bbox, scale).iter() { - arrow.draw(context, canvas_bbox, content_bbox, scale, theia)?; - } - - Ok(()) + fn style(&self) -> &Style { + &self.style } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/mod.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/mod.rs index 3effb95f..b9674f88 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/mod.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/mod.rs @@ -1,68 +1,68 @@ mod arc; +mod arc_arrow; mod arrow; mod chevron; mod draw; +mod grid; mod line_segment; mod line_segment_arrows; mod point; mod polygon; -mod rect; use tiling::geometry::BBox; pub use self::arc::Arc; +pub use self::arc_arrow::ArcArrow; pub use self::arrow::Arrow; pub use self::chevron::Chevron; pub use self::draw::Draw; +pub use self::grid::Grid; pub use self::line_segment::LineSegment; pub use self::line_segment_arrows::LineSegmentArrows; pub use self::point::Point; pub use self::polygon::Polygon; -pub use self::rect::Rect; use super::collision::Theia; use super::Scale; pub use super::Style; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Component { Arc(Arc), + ArcArrow(ArcArrow), Arrow(Arrow), Chevron(Chevron), + Grid(Grid), LineSegment(LineSegment), LineSegmentArrows(LineSegmentArrows), Point(Point), Polygon(Polygon), - Rect(Rect), } impl Component { pub fn inner(&self) -> &dyn Draw { match self { Self::Arc(d) => d, + Self::ArcArrow(d) => d, Self::Arrow(d) => d, Self::Chevron(d) => d, + Self::Grid(d) => d, Self::LineSegment(d) => d, Self::LineSegmentArrows(d) => d, Self::Point(d) => d, Self::Polygon(d) => d, - Self::Rect(d) => d, } } } impl Draw for Component { - fn collides_with( + fn children( &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - other: &Component, - ) -> Result { - self - .inner() - .collides_with(context, canvas_bbox, content_bbox, scale, other) + ) -> Option>> { + self.inner().children(canvas_bbox, content_bbox, scale) } fn component(&self) -> Component { @@ -79,7 +79,7 @@ impl Draw for Component { canvas_bbox: &BBox, content_bbox: &BBox, scale: &Scale, - ) -> Result { + ) -> BBox { self.inner().bbox(context, canvas_bbox, content_bbox, scale) } @@ -91,7 +91,7 @@ impl Draw for Component { scale: &Scale, theia: &mut Theia, ) -> Result<(), Error> { - if !theia.has_collision(context, canvas_bbox, content_bbox, scale, self)? { + if !theia.has_collision(context, canvas_bbox, content_bbox, scale, self) { self .inner() .draw(context, canvas_bbox, content_bbox, scale, theia)?; @@ -112,6 +112,16 @@ impl Draw for Component { .inner() .draw_bbox(context, canvas_bbox, content_bbox, scale, style) } + + fn interactive(&self) -> Option { + self.inner().interactive() + } +} + +impl Default for Component { + fn default() -> Self { + Self::Polygon(Polygon::default()) + } } impl From for Component { @@ -120,6 +130,12 @@ impl From for Component { } } +impl From for Component { + fn from(arc_arrow: ArcArrow) -> Self { + Self::ArcArrow(arc_arrow) + } +} + impl From for Component { fn from(arrow: Arrow) -> Self { Self::Arrow(arrow) @@ -132,6 +148,12 @@ impl From for Component { } } +impl From for Component { + fn from(value: Grid) -> Self { + Self::Grid(value) + } +} + impl From for Component { fn from(line_segment: LineSegment) -> Self { Self::LineSegment(line_segment) @@ -155,9 +177,3 @@ impl From for Component { Self::Polygon(shape) } } - -impl From for Component { - fn from(shape: Rect) -> Self { - Self::Rect(shape) - } -} diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/point.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/point.rs index da52151b..ac6a36ca 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/point.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/point.rs @@ -2,18 +2,34 @@ use std::f64::consts::PI; use tiling::geometry::BBox; -use super::{Component, Draw, Style}; +use super::{Draw, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Point { - pub point: tiling::geometry::Point, - pub style: Style, + point: tiling::geometry::Point, + interactive: Option, + style: Style, } impl Point { + pub fn non_interactive(mut self) -> Self { + self.interactive = Some(false); + self + } + + pub fn with_point(mut self, point: tiling::geometry::Point) -> Self { + self.point = point; + self + } + + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } + fn draw_path( &self, context: &web_sys::OffscreenCanvasRenderingContext2d, @@ -39,7 +55,7 @@ impl Point { } impl Draw for Point { - fn component(&self) -> Component { + fn component(&self) -> super::Component { self.clone().into() } @@ -53,20 +69,20 @@ impl Draw for Point { _canvas_bbox: &BBox, _content_bbox: &BBox, scale: &Scale, - ) -> Result { + ) -> BBox { let radius = self.style.get_point_radius(scale); let min = self .point .clone() - .translate(&tiling::geometry::Point::default().with_xy(-radius, -radius)); + .translate(&tiling::geometry::Point::at(-radius, -radius)); let max = self .point .clone() - .translate(&tiling::geometry::Point::default().with_xy(radius, radius)); + .translate(&tiling::geometry::Point::at(radius, radius)); - Ok(BBox { min, max }) + BBox::from_min_max(min, max) } fn draw( @@ -95,4 +111,8 @@ impl Draw for Point { Ok(()) } + + fn interactive(&self) -> Option { + self.interactive + } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/polygon.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/polygon.rs index fe2e7b2e..043f26db 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/polygon.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/component/polygon.rs @@ -1,21 +1,35 @@ use tiling::geometry::BBox; -use super::{Component, Draw, Style}; +use super::{Draw, Style}; use crate::canvas::collision::Theia; use crate::canvas::Scale; use crate::Error; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Polygon { - pub polygon: tiling::geometry::Polygon, - pub style: Style, + polygon: tiling::geometry::Polygon, + style: Style, + interactive: Option, } -impl Draw for Polygon { - fn component(&self) -> Component { - self.clone().into() +impl Polygon { + pub fn non_interactive(mut self) -> Self { + self.interactive = Some(false); + self + } + + pub fn with_polygon(mut self, polygon: tiling::geometry::Polygon) -> Self { + self.polygon = polygon; + self } + pub fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } +} + +impl Draw for Polygon { fn style(&self) -> &Style { &self.style } @@ -26,8 +40,15 @@ impl Draw for Polygon { _canvas_bbox: &BBox, _content_bbox: &BBox, _scale: &Scale, - ) -> Result { - Ok(self.polygon.bbox) + ) -> BBox { + let min = self.polygon.bbox.min(); + let max = self.polygon.bbox.max(); + + BBox::from_min_max(min, max) + } + + fn component(&self) -> super::Component { + self.clone().into() } fn draw( @@ -47,10 +68,20 @@ impl Draw for Polygon { } } - context.line_to(self.polygon.points[0].x, self.polygon.points[0].y); + let first_point = self + .polygon + .points + .first() + .expect("First point for polygon not found"); + + context.line_to(first_point.x, first_point.y); self.draw_end(context); Ok(()) } + + fn interactive(&self) -> Option { + self.interactive + } } diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/component/rect.rs b/workspaces/tilings/src-rust/renderer/src/canvas/component/rect.rs deleted file mode 100644 index d26014ad..00000000 --- a/workspaces/tilings/src-rust/renderer/src/canvas/component/rect.rs +++ /dev/null @@ -1,49 +0,0 @@ -use tiling::geometry::{BBox, Point}; - -use super::{Component, Draw, Style, Theia}; -use crate::canvas::Scale; -use crate::Error; - -#[derive(Clone)] -pub struct Rect { - pub min: Point, - pub max: Point, - pub style: Style, -} - -impl Draw for Rect { - fn component(&self) -> Component { - self.clone().into() - } - - fn style(&self) -> &Style { - &self.style - } - - fn bbox( - &self, - _context: &web_sys::OffscreenCanvasRenderingContext2d, - _canvas_bbox: &BBox, - _content_bbox: &BBox, - _scale: &Scale, - ) -> Result { - Ok(BBox::default().with_min(self.min).with_max(self.max)) - } - - fn draw( - &self, - context: &web_sys::OffscreenCanvasRenderingContext2d, - _canvas_bbox: &BBox, - _content_bbox: &BBox, - scale: &Scale, - _theia: &mut Theia, - ) -> Result<(), Error> { - let bbox = BBox::default().with_min(self.min).with_max(self.max); - - self.draw_start(context, scale, &self.style)?; - context.rect(bbox.min.x, bbox.min.y, bbox.width(), bbox.height()); - self.draw_end(context); - - Ok(()) - } -} diff --git a/workspaces/tilings/src-rust/renderer/src/canvas/mod.rs b/workspaces/tilings/src-rust/renderer/src/canvas/mod.rs index 9f768b66..562e9189 100644 --- a/workspaces/tilings/src-rust/renderer/src/canvas/mod.rs +++ b/workspaces/tilings/src-rust/renderer/src/canvas/mod.rs @@ -3,39 +3,44 @@ mod component; mod scale; mod style; -use std::collections::BTreeMap; -use std::hash::Hash; +use std::collections::{BTreeMap, HashMap, VecDeque}; use anyhow::Result; use tiling::geometry::BBox; use wasm_bindgen::JsCast; use self::collision::Theia; -pub use self::component::{Arc, LineSegment, LineSegmentArrows, Point, Polygon}; -use self::component::{Component, Draw, Rect}; +pub use self::component::{ArcArrow, Grid, LineSegment, LineSegmentArrows, Point, Polygon}; +use self::component::{Component, Draw}; pub use self::scale::{Scale, ScaleMode}; pub use self::style::Style; -use crate::Error; +use crate::draw::Layer; +use crate::{Error, Options}; -pub struct Canvas { +pub struct Canvas { pub scale: Scale, context: web_sys::OffscreenCanvasRenderingContext2d, content_bbox: BBox, - show_debug_layer: bool, - debug_style: Style, + draw_bounding_boxes: bool, + draw_bounding_boxes_style: Style, + + layers: Option>>, + layers_enabled: HashMap, - layers: Option>>, theia: Theia, } -impl Canvas -where - TLayer: Eq + Hash + Ord, -{ - pub fn new(canvas: web_sys::OffscreenCanvas, scale: Scale) -> Result { +impl Canvas { + pub fn new(canvas: web_sys::OffscreenCanvas, options: &Options) -> Result { let context = canvas.get_context("2d"); + let scale = Scale::default() + .with_auto_rotate(options.auto_rotate) + .with_padding(options.padding) + .with_mode(options.scale_mode); + + let layers_enabled = options.show_layers.clone().unwrap_or_default(); if context.is_err() { return Err(Error::ApplicationError { @@ -77,10 +82,14 @@ where context, scale: scale.with_canvas_bbox(canvas_bbox), - show_debug_layer: false, - debug_style: Style::default(), + draw_bounding_boxes: layers_enabled + .get(&Layer::BoundingBoxes) + .cloned() + .unwrap_or(false), + draw_bounding_boxes_style: options.styles.bounding_boxes.clone().unwrap_or_default(), layers: None, + layers_enabled, theia: Theia::new(), }) } @@ -89,22 +98,32 @@ where &self.content_bbox } - pub fn draw_debug(&mut self, style: &Option