diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..255603e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +rustflags = ["-C", "target-feature=+simd128"] + +[unstable] +build-std = ["panic_abort", "std"] diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..5f53029 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,65 @@ +name: Prepare release + +on: + workflow_dispatch: + inputs: + releaseType: + description: 'Type of release' + required: true + default: 'patch' + type: choice + options: + - major + - minor + - patch + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-root + cancel-in-progress: true + +jobs: + prepare_release: + runs-on: ubuntu-latest + + outputs: + ref: ${{ steps.push-tag.outputs.commit_long_sha }} + + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ steps.generate-token.outputs.token }} + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - run: cargo install cargo-edit + + - name: Bump version + run: cargo set-version --bump "${{ inputs.releaseType }}" + + - name: Get version + run: echo "IMMICH_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages.[0].version')" >> $GITHUB_ENV + + - name: Commit and tag + id: push-tag + uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: 'chore: version ${{ env.IMMICH_VERSION }}' + tag: ${{ env.IMMICH_VERSION }} + push: true + + - name: Create draft release + uses: softprops/action-gh-release@v2 + with: + draft: true + tag_name: ${{ env.IMMICH_VERSION }} + token: ${{ steps.generate-token.outputs.token }} + generate_release_notes: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a9e9fb8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + packages: write + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # This sets up .npmrc to use NODE_AUTH_TOKEN below + # See https://github.com/actions/setup-node/blob/48b90677b6048efbc723b11a94acb950d3f1ac36/src/authutil.ts#L48 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + registry-url: 'https://registry.npmjs.org' + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: v0.13.1 + + - name: Build + run: npm run build + + - name: Publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6045409 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test +on: + workflow_dispatch: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: browser-actions/setup-firefox@v1 + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: v0.13.1 + + - name: Test + run: wasm-pack test --firefox --headless diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f76a348 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +node_modules +*.ll diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..56a409e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,314 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "cc" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "justified_layout" +version = "0.1.0" +dependencies = [ + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d44563646eb934577f2772656c7ad5e9c90fac78aa8013d776fcdaf24625d" +dependencies = [ + "js-sys", + "minicov", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54171416ce73aa0b9c377b51cc3cb542becee1cd678204812e8392e5b0e4a031" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7ab6276 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "justified_layout" +version = "0.1.0" +edition = "2021" +license = "AGPL-3" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" + +[dev-dependencies] +wasm-bindgen-test = "0.3.0" + +[profile.release] +lto = true +codegen-units = 1 +strip = true +panic = "abort" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["--enable-simd", "--fast-math", "--gufa", "-O3", "--type-finalizing"] diff --git a/js/index.ts b/js/index.ts new file mode 100644 index 0000000..64c7269 --- /dev/null +++ b/js/index.ts @@ -0,0 +1,54 @@ +import { get_justified_layout } from '../pkg/justified-layout-wasm.js'; + +export interface LayoutOptions { + rowHeight: number; + rowWidth: number; + spacing: number; + heightTolerance: number; +} + +/* + * This is a wrapper class to interact with the layout generated by the Wasm function. + * This design is used because: + * - The layout is stored in a Int32Array for efficiency, which is not very user-friendly + * - Creating objects from the Int32Array takes more time than the actual layout calculation + * - Using getter methods avoids dynamic heap allocation beyond the initial layout calculation + * - The class provides a static prototype that can be JIT-compiled efficiently, inlining all of its methods + * - V8 does not box int32 values, unlike most other data types + * - Moving floats from Wasm to JS is expensive, probably due to validation around NaN and Infinity + */ +export class JustifiedLayout { + layout: Int32Array; + + constructor(aspectRatios: Float32Array, { rowHeight, rowWidth, spacing, heightTolerance }: LayoutOptions) { + if (aspectRatios.length === 0) { + this.layout = Int32Array.of(0, 0, 0, 0); + } else { + this.layout = get_justified_layout(aspectRatios, rowHeight, rowWidth, spacing, heightTolerance); + } + } + + get containerWidth() { + return this.layout[0]; + } + + get containerHeight() { + return this.layout[1]; + } + + getTop(boxIdx: number) { + return this.layout[boxIdx * 4 + 4]; // the first 4 elements are containerWidth, containerHeight, padding, padding + } + + getLeft(boxIdx: number) { + return this.layout[boxIdx * 4 + 5]; + } + + getWidth(boxIdx: number) { + return this.layout[boxIdx * 4 + 6]; + } + + getHeight(boxIdx: number) { + return this.layout[boxIdx * 4 + 7]; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5730ee9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "justified-layout-wasm", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "justified-layout-wasm", + "version": "0.1.0", + "license": "AGPL-3" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4468026 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "@immich/justified-layout-wasm", + "type": "module", + "version": "0.0.0", + "license": "AGPL-3", + "scripts": { + "build": "wasm-pack build --no-pack --out-name justified-layout-wasm" + }, + "files": [ + "pkg/justified-layout-wasm.js", + "pkg/justified-layout-wasm_bg.js", + "pkg/justified-layout-wasm_bg.wasm" + ], + "main": "js/index.ts", + "sideEffects": [ + "pkg/justified-layout-wasm.js" + ] +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f70d225 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6e0e4d8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,127 @@ +#![feature(slice_as_chunks)] + +use wasm_bindgen::prelude::*; + +pub struct LayoutOptions { + pub row_height: f32, + pub row_width: f32, + pub spacing: f32, + pub tolerance: f32, +} + +/// Given an input of aspect ratios representing boxes, returns a vector 4 times its length + 4. +/// The first element is the maximum width across all rows, the second is the total height required +/// to display all rows, the next two are padding, and the remaining elements are sequences of 4 +/// elements for each box, representing the top, left, width and height positions. +/// `row_height` is a positive float that is the target height of the row. +/// It is not strictly followed; the actual height may be off by one due to truncation, and may be +/// substantially different if only one box can fit on a row and this box cannot fit with the +/// target height. The height cannot exceed this target unless `tolerance` is greater than zero. +/// `row_width` is a positive float that is the target width of the row. +/// It will not be exceeded, but a row may have a shorter width if the boxes cannot fill the row +/// width given the `tolerance`. +/// `spacing` is a non-negative float that controls the spacing between boxes, including between rows. +/// Notably, there is no offset applied in directions where there is no box. +/// The first box will have its top and left positions both at 0, not at `spacing`, and so on. +/// `tolerance` is a non-negative float that gives more freedom to fill the row width. +/// When there is free space in the row and the next box cannot fit in this row, it can scale +/// the boxes to a larger height to fill this space while respecting aspect ratios. A value of +/// 0.15 signifies that the actual row height may be up to 15% greater than the target height. +/// +/// Note: The response being Vec rather than a struct or list of structs is important, as the +/// JS-WASM interop is *massively* slower when moving structs to JS instead of an array and +/// importing integers is faster than floats. +#[wasm_bindgen] +pub fn get_justified_layout( + aspect_ratios: &[f32], + row_height: f32, + row_width: f32, + spacing: f32, + tolerance: f32, +) -> Vec { + let options = LayoutOptions { + row_height, + row_width, + spacing, + tolerance, + }; + + _get_justified_layout(aspect_ratios, options) +} + +#[inline(always)] +fn _get_justified_layout(aspect_ratios: &[f32], options: LayoutOptions) -> Vec { + let mut positions = vec![0.0; aspect_ratios.len() * 4 + 4]; // 2 for container width and height, 2 for alignment + let max_row_height = options.row_height * (1.0 + options.tolerance); + let mut cur_row_width = 0.0; + let mut max_actual_row_width = 0.0; + let mut row_start_idx: usize = 0; + let mut top = 0.0; + + for (i, aspect_ratio) in aspect_ratios.iter().enumerate() { + let box_width = aspect_ratio * options.row_height; + cur_row_width += box_width; + + // there are no more boxes that can fit in this row + if cur_row_width > options.row_width && i > 0 { + let row = &mut positions[row_start_idx * 4 + 4..i * 4 + 4]; + let aspect_ratio_row = &aspect_ratios[row_start_idx..i]; + + // treat the row's boxes as a single entity and scale them to fit the row width + let total_aspect_ratio: f32 = aspect_ratio_row.iter().sum(); + let spacing_pixels = options.spacing * f32::from(aspect_ratio_row.len() as u16 - 1); + let scaled_row_height = + ((options.row_width - spacing_pixels) / total_aspect_ratio).min(max_row_height); + + let mut actual_row_width = spacing_pixels; + let mut left = 0.0; + // SAFETY: this slice's length is guaranteed to be a multiple of 4 + let row_positions = unsafe { row.as_chunks_unchecked_mut::<4>() }; + for i in 0..aspect_ratio_row.len() { + let pos = &mut row_positions[i]; + let width = aspect_ratio_row[i] * scaled_row_height; + pos[0] = top; + pos[1] = left; + pos[2] = width; + pos[3] = scaled_row_height; + left += width + options.spacing; + actual_row_width += width; + } + top += scaled_row_height + options.spacing; + max_actual_row_width = actual_row_width.max(max_actual_row_width); + row_start_idx = i; + cur_row_width = box_width; + } + cur_row_width += options.spacing; + } + + // this is the same as in the for loop and processes the last row + // inlined because it ends up producing much better assembly + let row = &mut positions[row_start_idx * 4 + 4..]; + let aspect_ratio_row = &aspect_ratios[row_start_idx..]; + let total_aspect_ratio: f32 = aspect_ratio_row.iter().sum(); + let spacing_pixels = options.spacing * f32::from(aspect_ratio_row.len() as u16 - 1); + let scaled_row_height = + ((options.row_width - spacing_pixels) / total_aspect_ratio).min(max_row_height); + + let mut actual_row_width = spacing_pixels; + let mut left = 0.0; + // SAFETY: this slice's length is guaranteed to be a multiple of 4 + let row_positions = unsafe { row.as_chunks_unchecked_mut::<4>() }; + for i in 0..aspect_ratio_row.len() { + let pos = &mut row_positions[i]; + let width = aspect_ratio_row[i] * scaled_row_height; + pos[0] = top; + pos[1] = left; + pos[2] = width; + pos[3] = scaled_row_height; + left += width + options.spacing; + actual_row_width += width; + } + // SAFETY: these indices are guaranteed to be within the vector's bounds + unsafe { + *positions.get_unchecked_mut(0) = actual_row_width.max(max_actual_row_width).ceil(); + *positions.get_unchecked_mut(1) = (top + scaled_row_height).ceil(); + } + positions.into_iter().map(|val| val as i32).collect() +} diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..14dc2c0 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,427 @@ +use justified_layout::get_justified_layout; +use wasm_bindgen_test::*; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn fits_perfectly_on_one_row() { + let input: Vec = vec![1.0, 1.0, 1.0]; + let row_width = 900.0; + let row_height = 300.0; + let spacing = 0.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 900); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 300); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 300); + assert_eq!(height1, 300); + + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, 0); + assert_eq!(left2, width1); + assert_eq!(width2, 300); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, 0); + assert_eq!(left3, width1 + width2); + assert_eq!(width3, 300); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn applies_spacing() { + let input: Vec = vec![1.0, 1.0, 1.0]; + let row_width = 904.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 904); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 300); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 300); + assert_eq!(height1, 300); + + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, 0); + assert_eq!(left2, width1 + spacing as i32); + assert_eq!(width2, 300); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, 0); + assert_eq!(left3, width1 + spacing as i32 + width2 + spacing as i32); + assert_eq!(width3, 300); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn expands_row_based_on_height_tolerance() { + let input: Vec = vec![1.0, 1.0, 1.0]; + let row_width = 1000.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.1; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 994); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 330); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 330); + assert_eq!(height1, 330); + + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, 0); + assert_eq!(left2, width1 + spacing as i32); + assert_eq!(width2, 330); + assert_eq!(height2, 330); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, 0); + assert_eq!(left3, width1 + spacing as i32 + width2 + spacing as i32); + assert_eq!(width3, 330); + assert_eq!(height3, 330); +} + +#[wasm_bindgen_test] +fn adds_second_row_due_to_spacing() { + let input: Vec = vec![1.0, 1.0, 1.0]; + let row_width = 900.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 602); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 602); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 300); + assert_eq!(height1, 300); + // + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, 0); + assert_eq!(left2, width1 + spacing as i32); + assert_eq!(width2, 300); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, (row_height + spacing) as i32); + assert_eq!(left3, 0); + assert_eq!(width3, 300); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn positions_boxes_with_different_aspect_ratios() { + let input: Vec = vec![16.0 / 9.0, 2.0, 9.0 / 16.0]; + let row_width = 900.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 771); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 602); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, (300.0 * (16.0 / 9.0)) as i32); + assert_eq!(height1, 300); + // + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, height1 + spacing as i32); + assert_eq!(left2, 0); + assert_eq!(width2, 300 * 2); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, height1 + spacing as i32); + assert_eq!(left3, width2 + spacing as i32); + assert_eq!(width3, (300.0 * (9.0 / 16.0)) as i32); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn scales_boxes_with_different_aspect_ratios_when_using_height_tolerance() { + let input: Vec = vec![16.0 / 9.0, 2.0, 9.0 / 16.0]; + let row_width = 900.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.2; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 900); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 712 + 1); // 1 to account for ceil() + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 640); + assert_eq!(height1, 360); + // + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, height1 + spacing as i32); + assert_eq!(left2, 0); + assert_eq!(width2, 700); + assert_eq!(height2, 350); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, height1 + spacing as i32); + assert_eq!(left3, width2 + spacing as i32); + assert_eq!(width3, 197); + assert_eq!(height3, 350); +} + +#[wasm_bindgen_test] +fn one_square_box_on_each_row() { + let input: Vec = vec![1.0, 1.0, 1.0]; + let row_width = 599.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 300); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 904); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 300); + assert_eq!(height1, 300); + + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, height1 + spacing as i32); + assert_eq!(left2, 0); + assert_eq!(width2, 300); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, height1 + spacing as i32 + height2 + spacing as i32); + assert_eq!(left3, 0); + assert_eq!(width3, 300); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn different_shaped_boxes_on_each_row() { + let input: Vec = vec![16.0 / 9.0, 2.0, 9.0 / 16.0]; + let row_width = 600.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.0; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 600); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 904); + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 533); + assert_eq!(height1, 300); + // + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, height1 + spacing as i32); + assert_eq!(left2, 0); + assert_eq!(width2, 600); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, height1 + spacing as i32 + height2 + spacing as i32); + assert_eq!(left3, 0); + assert_eq!(width3, 168); + assert_eq!(height3, 300); +} + +#[wasm_bindgen_test] +fn one_box_on_each_row_with_scaling() { + let input: Vec = vec![16.0 / 9.0, 2.0, 9.0 / 16.0]; + let row_width = 600.0; + let row_height = 300.0; + let spacing = 2.0; + let height_tolerance = 0.15; + + let layout = get_justified_layout( + input.as_slice(), + row_height, + row_width, + spacing, + height_tolerance, + ); + assert_eq!(layout.len(), 16); + let max_row_width = layout[0]; + assert_eq!(max_row_width, 600); + + let max_row_height = layout[1]; + assert_eq!(max_row_height, 337 + 2 + 300 + 2 + 345 + 1); // 1 to account for ceil() + + let [top1, left1, width1, height1] = layout[4..8] else { + unreachable!() + }; + assert_eq!(top1, 0); + assert_eq!(left1, 0); + assert_eq!(width1, 600); + assert_eq!(height1, 337); + // + let [top2, left2, width2, height2] = layout[8..12] else { + unreachable!() + }; + assert_eq!(top2, height1 + spacing as i32); + assert_eq!(left2, 0); + assert_eq!(width2, 600); + assert_eq!(height2, 300); + + let [top3, left3, width3, height3] = layout[12..16] else { + unreachable!() + }; + assert_eq!(top3, height1 + spacing as i32 + height2 + spacing as i32); + assert_eq!(left3, 0); + assert_eq!(width3, 194); + assert_eq!(height3, 345); +}