diff --git a/.format_all b/.format_all new file mode 100755 index 00000000..a24b78d9 --- /dev/null +++ b/.format_all @@ -0,0 +1,21 @@ +#!/bin/bash + +# Directories to exclude (relative to repo root) +EXCLUDE_EXTERNAL="./thirdparty" +EXCLUDE_BUILD="./build" + +# Recursively find source files and apply clang-format with -i -style=file +find . \( \ + -path "$EXCLUDE_EXTERNAL" -prune \ + -o -path "${EXCLUDE_BUILD}*" -prune \ + \) -o \( \ + -name "*.cpp" -o \ + -name "*.c" -o \ + -name "*.h" -o \ + -name "*.hpp" -o \ + -name "*.cu" -o \ + -name "*.cuh" -o \ + -name "*.cc" \ + \) -exec clang-format -i -style=file {} + + +echo "✔ All matching source files formatted (excluding external and build* folders) with clang-format -style=file" \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..25611723 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,83 @@ +name: Build and publish documentation + +on: + # Branch filters cannot use repository variables. Filter at the job level so + # switching DOCS_PUBLISH_BRANCH does not require editing this workflow. + push: + branches: ["**"] + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "src/DEM/**" + - "VERSION" + - "LICENSE.md" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + # Set this repository variable to DEME3_docs initially, then main when ready. + # An unset variable disables publication; PRs still validate the site. + if: >- + github.event_name == 'pull_request' || + (github.ref_type == 'branch' && github.ref_name == vars.DOCS_PUBLISH_BRANCH) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Check out documentation source + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install documentation tools + run: | + sudo apt-get update + sudo apt-get install --yes doxygen + python -m pip install -r docs/requirements.txt + + # The committed Python reference avoids importing the CUDA extension on + # hosted runners. Doxygen parses public headers without compiling DEME. + - name: Build C++ and Python documentation + run: make -C docs html + + - name: Upload documentation site + uses: actions/upload-pages-artifact@v4 + with: + path: docs/_build/html + + deploy: + needs: build + if: >- + github.event_name != 'pull_request' && + github.ref_type == 'branch' && github.ref_name == vars.DOCS_PUBLISH_BRANCH + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + # Serialize publication to this repository's single Pages site. Let an + # active deployment finish rather than cancelling it midway through upload. + concurrency: + group: github-pages + cancel-in-progress: false + steps: + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index cf82e3ba..31777491 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -1,10 +1,10 @@ -name: Build Python wheels +name: Build DEME Python wheels on: workflow_dispatch: inputs: publish_to_pypi: - description: Publish the six validated wheels to the deme project on PyPI + description: Publish validated wheels (deme on release branches, deme3 on Mesh_Particles_Py) required: true type: boolean default: false @@ -14,6 +14,9 @@ on: - "CMakeLists.txt" - "cmake/**" - "pyproject.toml" + - "README.md" + - "docs/prepare_pypi_readme.py" + - "python/tests/**" - "VERSION" - "src/**" - "thirdparty/**" @@ -41,6 +44,33 @@ jobs: persist-credentials: false submodules: recursive + # Resolve the distribution at build time so merges cannot redirect preview publication. + - name: Select branch distribution + env: + DISTRIBUTION: ${{ github.ref_name == 'Mesh_Particles_Py' && 'deme3' || 'deme' }} + run: | + python - <<'PYTHON' + import os + from pathlib import Path + import re + + path = Path("pyproject.toml") + source = path.read_text() + source, count = re.subn(r'^name = "deme3?"$', 'name = "' + os.environ["DISTRIBUTION"] + '"', source, flags=re.M) + if count != 1: + raise SystemExit("Expected one DEME distribution name") + path.write_text(source) + PYTHON + + # PyPI cannot resolve repository-relative links. Rewrite only this build + # checkout so the wheel description uses the same README with permanent URLs. + - name: Prepare README for PyPI + run: | + python python/tests/test_pypi_readme.py + python docs/prepare_pypi_readme.py README.md \ + --repository-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ + --revision "$GITHUB_SHA" + - name: Build and repair wheel uses: pypa/cibuildwheel@v4.1.1 env: @@ -55,30 +85,66 @@ jobs: python -m auditwheel show wheelhouse/*.whl python - <<'PY' from pathlib import Path - from zipfile import ZipFile wheels = list(Path("wheelhouse").glob("*.whl")) if len(wheels) != 1: raise SystemExit(f"Expected exactly one wheel, found: {wheels}") - wheel = wheels[0] + name = wheels[0].name expected_python = "${{ matrix.python }}" - if expected_python not in wheel.name or "manylinux_2_28_x86_64" not in wheel.name: - raise SystemExit(f"Unexpected wheel tags: {wheel.name}") + distribution = "${{ github.ref_name == 'Mesh_Particles_Py' && 'deme3' || 'deme' }}" + if not name.startswith(distribution + "-"): + raise SystemExit(f"Expected a {distribution} distribution wheel, found: {name}") + if expected_python not in name or "manylinux_2_28_x86_64" not in name: + raise SystemExit(f"Unexpected wheel tags: {name}") + + # CUDA comes from NVIDIA pip dependencies or a system toolkit. Auditwheel must not copy its build-time driver + # stub or an incomplete NVRTC installation into the repaired artifact. + from zipfile import ZipFile + from email.parser import BytesParser - with ZipFile(wheel) as archive: + with ZipFile(wheels[0]) as archive: + metadata_files = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")] + if len(metadata_files) != 1: + raise SystemExit("Expected one wheel METADATA file") + metadata = BytesParser().parsebytes(archive.read(metadata_files[0])) + if metadata["Name"] != distribution or "cuda12" not in metadata.get_all("Provides-Extra", []): + raise SystemExit("Unexpected distribution name or missing cuda12 extra") + if metadata["Description-Content-Type"] != "text/markdown": + raise SystemExit("Expected a Markdown PyPI description") + description = metadata.get_payload(decode=True).decode("utf-8").strip() + if description != Path("README.md").read_text(encoding="utf-8").strip(): + raise SystemExit("Wheel description does not match the prepared README") + standalone_visualizer = [ + member for member in archive.namelist() if member.endswith("/libDEMEVisualizer.so") + ] bundled_cuda = [ member for member in archive.namelist() if ".libs/" in member and any(library in member for library in ("libcuda", "libcudart", "libnvrtc")) ] + if standalone_visualizer: + raise SystemExit( + "Python embeds the visualizer and must not package its native C++ facade: " + f"{standalone_visualizer}" + ) if bundled_cuda: raise SystemExit(f"CUDA libraries must remain external: {bundled_cuda}") - print(f"Validated {wheel.name} ({wheel.stat().st_size / 1024 / 1024:.1f} MiB)") + print(f"Validated {name} ({wheels[0].stat().st_size / 1024 / 1024:.1f} MiB)") PY + - name: Test cuda12 extra without a system CUDA toolkit + env: + PYTHON_ABI: ${{ matrix.python }} + DISTRIBUTION: ${{ github.ref_name == 'Mesh_Particles_Py' && 'deme3' || 'deme' }} + run: | + minor="${PYTHON_ABI#cp3}" + docker run --rm -v "$PWD:/work:ro" -e DISTRIBUTION "python:3.${minor}-slim-bookworm" \ + sh -ec 'for wheel in /work/wheelhouse/*.whl; do python -m pip install "$wheel[cuda12]"; done; \ + python /work/python/tests/cuda12_smoke.py --distribution "$DISTRIBUTION"' + - name: Upload wheel artifact uses: actions/upload-artifact@v4 with: @@ -88,11 +154,14 @@ jobs: retention-days: 14 publish-pypi: - name: Publish deme wheels to PyPI - if: github.event_name == 'workflow_dispatch' && inputs.publish_to_pypi + name: Publish branch distribution to PyPI + if: >- + github.event_name == 'workflow_dispatch' && + inputs.publish_to_pypi && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/Mesh_Particles' || github.ref == 'refs/heads/Mesh_Particles_Py') needs: build-linux-wheels runs-on: ubuntu-24.04 - environment: pypi + environment: ${{ github.ref_name == 'Mesh_Particles_Py' && 'pypi-deme3' || 'pypi' }} permissions: contents: read id-token: write diff --git a/.gitignore b/.gitignore index 788fdc5b..9f5b0524 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,64 @@ -build* -.vscode/ -*.vtk -*.csv +# Local build trees and CMake/Ninja products +build*/ +cmake-build-*/ +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +compile_commands.json +Makefile +*.ninja +.ninja_* + +# Native/CUDA build artifacts +*.o +*.a +*.so +*.dylib +*.dll +*.exe +*.out +*.ptx +*.cubin +*.fatbin +*.sass + +# Python packaging, caches, and local environments +__pycache__/ +*.py[cod] *.egg-info/ .cache/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ dist/ -output/* -post/* +wheelhouse/ +docs/_build/ +# Demo/test/generated simulation output +DemoOutput_*/ +modular_test_output/ +output/ +post/ +*.vtk +*.vtp +*.vtu +/*.csv +Contact_pairs_*.csv +DEMdemo_output_*.csv +DEMdemo_forces_*.csv + +# CodeQL and other scanner scratch files _codeql* + +# Editor, OS, and temporary local files +.vscode/ +.idea/ +.DS_Store +*~ +*.swp +*.swo +*.tmp +*.log +*.diff diff --git a/.gitmodules b/.gitmodules index 137dc0ec..69c71a5a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ path = thirdparty/pybind11 url = https://github.com/pybind/pybind11.git branch = stable +[submodule "thirdparty/raylib"] + path = thirdparty/raylib + url = https://github.com/raysan5/raylib.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..4627d14b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# DEM-Engine Agent Guide + +This file is a living guide for coding agents working in DEM-Engine. Keep it practical: add rules here when a mistake would be expensive, subtle, or easy to repeat. + +## Project Shape + +DEM-Engine is a performance-sensitive CUDA/C++ DEM solver with a Chrono-like public API, runtime-compiled kernels, dual worker threads (`dT` and `kT`), and a demo/test suite that doubles as usage documentation. Changes should respect that mix: public APIs should stay approachable, implementation code should stay explicit, and GPU code should be treated as a place where small assumptions can become hard-to-debug runtime failures. + +## General Philosophy + +- Prefer local patterns over new abstractions. If an existing class, helper, `DualArray`, timer, error macro, or CMake pattern already solves the problem, use it. +- Keep changes narrow. DEM-Engine has many tightly coupled host/device data paths; avoid broad refactors unless the task explicitly calls for them. +- Make data movement obvious. When adding simulation state, follow the full path deliberately: API/cache data, dT/kT storage, `DEMData*` pointer binding, host-to-device transfer, JIT substitution if needed, and runtime refresh/update behavior. +- Preserve the setup/runtime boundary. Template/cache data is usually resolved during `Initialize()` or `Update()`, not rebuilt inside high-frequency `DoDynamics()` paths. +- Keep CPU-side convenience separate from GPU hot paths. Public APIs can be friendly; kernels and per-step loops should be boring, direct, and allocation-free. +- Prefer explicit names over cleverness. Names like `COMBINED_OWNER_REIMPOSITION_BLOCK` are good because they describe the kernel context and the tuning purpose. +- Use comments to explain non-obvious physics, synchronization, ownership, or CUDA/JIT constraints. Do not narrate simple assignments. +- When modifying or adding code, preserve existing explanatory comments whenever possible. Delete one only when the + corresponding code or explanation has become completely obsolete. +- When adding code that uses a non-obvious coding trick or workaround, add a concise comment explaining why it is needed. +- Newly added functions and substantial code blocks should include accompanying comments that explain what they do and + the approach they use. Keep these comments practical: describe intent, data flow, or algorithmic strategy rather than + restating each assignment. + +## Style + +- Follow `.clang-format` for C++/CUDA formatting. The repo uses Chromium style with 4-space indentation and a 120-column limit. +- Match existing CMake style in the directory being edited. Do not modernize unrelated CMake while adding a target or option. +- Keep includes, namespace use, and helper placement consistent with nearby files. +- Put helper-only structs/classes/functions in an appropriate utility/helper header/source rather than embedding them directly in primary implementation files. For example, dT-only diagnostics and regulation helpers belong in `src/DEM/utils/DynamicThreadHelpers.hpp` or a similarly named helper file, not directly in `dT.h`/`dT.cpp`. +- Include headers from this project with quotes (`"..."`), and include headers from other projects or the standard library + with angle brackets (`<...>`). +- Prefer `DEME_ERROR`, `DEME_WARNING`, and existing diagnostic patterns over ad hoc exceptions or `std::cout` in library code. +- Demos and modular tests may print concise PASS/FAIL information, but core library code should avoid noisy output unless controlled by existing verbosity mechanisms. + +## CUDA And JIT Kernels + +- Treat CUDA launch configuration as part of correctness, not just performance. +- For added new kernel calls' block size, prefer a reasonable `constexpr` value defined near the kernel call, for example `COMBINED_OWNER_REIMPOSITION_BLOCK`. Use `DEME_MAX_THREADS_PER_BLOCK` only when a 1024-thread block is known to be fine. Oversized blocks can make larger CUDA kernels fail silently, which is especially dangerous. +- Kernel call arguments must avoid implicit type conversion. The type passed at launch must match the kernel call pattern exactly. For example, do not pass a `size_t` value to a kernel argument declared as `unsigned int`; assume CUDA jitified kernels will not perform smart type conversion for launch arguments. +- When adding a kernel argument, update all related launch sites, instantiation/prewarm paths, and any JIT call patterns together. +- Keep kernel argument lists and `DEMData*` structs type-stable. If a host value must be narrowed, validate it and cast explicitly at the boundary. +- Avoid adding host/device synchronization in hot paths unless it is required for correctness. Prefer existing events, stream usage, and worker-thread coordination patterns. +- If a kernel can run in both contact and no-contact scenarios, make sure it is not accidentally nested under `nContactPairs > 0`. +- When adding per-step GPU work, add NVTX ranges and timers only where they help separate meaningful phases. + +## Simulation Semantics + +- Respect owner/member distinctions. Clumps, meshes, templates, combined owners, and tracked objects have different lifetimes and ownership rules. +- Combined-owner behavior should preserve rigid relative motion among members unless a feature explicitly says otherwise. +- Contact suppression, family prescriptions, user-added acceleration, and custom force models can interact. Check the order in `calculateForces()`, integration, and re-imposition paths before changing behavior. +- Keep units, frames, and quaternion conventions explicit. Many angular quantities are local-frame internally and global-frame in user-facing helpers. + +## Public API Changes + +- Follow existing API naming and overload patterns. New APIs should look like the surrounding `DEMSolver`, `DEMTracker`, `DEMMesh`, and template-loading methods. +- Validate user inputs early with useful messages. Prefer failing at setup time over letting invalid state reach kernels. +- Keep Python bindings in sync when changing public C++ APIs that should be exposed to Python. +- Preserve backward compatibility unless the requested change explicitly breaks it. + +## Tests And Demos + +- Prefer modular tests for targeted behavior changes. They should be deterministic, self-checking, and small enough to run as normal build targets. +- Demos should remain useful examples, not only regression tests. If adding assertions to a demo-style file, keep the printed output readable. +- For CUDA-dependent tests, at least verify the target builds locally. If runtime execution is blocked by driver/toolkit availability, report the exact CUDA error. +- When fixing a bug, add a test that would have failed before the fix whenever feasible. + +## Build And Verification + +- Use focused builds first, for example `cmake --build build --target DEMTest_CombinedOwners -- -j1`, then broaden only if the touched surface warrants it. +- Do not assume generated runtime data has refreshed after changing kernel source or force-model text. Clean rebuilds or copying generated assets may be required for some issues. +- Be careful with files under `build/`; they may contain generated outputs or local experiment artifacts and usually should not be edited. + +## Git Hygiene + +- Always provide a code review block for the agent session, using the native Review button and diff tab when available, + so the user can inspect the changes before committing. If the interface cannot provide that control, say so and + provide the diff directly instead of claiming a review button was generated. +- Keep session changes uncommitted until the user explicitly says they are done testing or asks the agent to commit. + Passing agent-run tests does not mean the user is done testing, and permission for an earlier commit does not + authorize committing later changes. +- Work on a topic branch when asked. Keep unrelated user changes intact. +- Do not revert or reformat files outside the task scope. +- Before handing off, check `git status --short` and summarize the touched files and verification. diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d76c2d8..133d823d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,32 +16,16 @@ if(NOT DEME_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") message(FATAL_ERROR "VERSION must contain a three-component semantic version; found '${DEME_VERSION}'") endif() -string(REPLACE "." ";" DEME_VERSION_COMPONENTS "${DEME_VERSION}") -list(GET DEME_VERSION_COMPONENTS 0 DEME_VERSION_MAJOR) -list(GET DEME_VERSION_COMPONENTS 1 DEME_VERSION_MINOR) -list(GET DEME_VERSION_COMPONENTS 2 DEME_VERSION_PATCH) - -# HIP/ROCm support option (must be set before project() to select the right language) -option(USE_HIP "Build with HIP for AMD GPUs" OFF) - -if(USE_HIP) - project( - Chrono-DEM-Engine - VERSION ${DEME_VERSION_MAJOR}.${DEME_VERSION_MINOR}.${DEME_VERSION_PATCH} - LANGUAGES CXX HIP - ) - # The LANGUAGES ... HIP enable above auto-detects the host GPU arch (and - # errors on a no-GPU host); pass -DCMAKE_HIP_ARCHITECTURES to override. - add_compile_definitions(USE_HIP) - # CXX files that include hip_runtime.h need this defined (HIP compiler defines it automatically) - add_compile_definitions(__HIP_PLATFORM_AMD__) -else() - project( - Chrono-DEM-Engine - VERSION ${DEME_VERSION_MAJOR}.${DEME_VERSION_MINOR}.${DEME_VERSION_PATCH} - LANGUAGES CXX CUDA - ) -endif() +project( + Chrono-DEM-Engine + VERSION ${DEME_VERSION} + LANGUAGES CXX CUDA +) + +# Preserve the established names consumed by generated API-version headers. +set(DEME_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(DEME_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(DEME_VERSION_PATCH ${PROJECT_VERSION_PATCH}) if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting CMAKE_BUILD_TYPE to 'Release' (default)") @@ -58,82 +42,68 @@ add_compile_definitions(DEME_BEING_CMAKE_COMPILED) include(GNUInstallDirs) include(CMakePackageConfigHelpers) -include(FetchContent) include(cmake/FixNinjaColors.cmake) fix_ninja_colors() -option(DEME_BUILD_PYTHON "Build Python bindings for DEME" OFF) -option(DEME_BUILD_DEMOS "Build DEME demos and modular tests" ON) - -if(DEME_BUILD_PYTHON) - if(USE_HIP) - message(FATAL_ERROR "Python wheels currently support the CUDA backend only") - endif() - find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/pybind11/CMakeLists.txt") - add_subdirectory(thirdparty/pybind11 EXCLUDE_FROM_ALL) - else() - find_package(pybind11 CONFIG REQUIRED) - endif() - message(STATUS "Python bindings enabled for Python ${Python_VERSION}") -endif() +find_package(CUDAToolkit REQUIRED) -if(USE_HIP) - # Find hipCUB for HIP builds - find_package(hipcub REQUIRED) - find_package(hip REQUIRED) -else() - find_package(CUDAToolkit REQUIRED) - # Find CUB library (this might need to be done in source-level config) - find_package( - CUB REQUIRED - HINTS ${CUDAToolkit_ROOT}/lib64/cmake/cub +# Some CUDA manylinux images expose the versioned NVRTC shared object without the unversioned development symlink. +# FindCUDAToolkit then reports the toolkit as found but omits CUDA::nvrtc. Recover the standard target from the +# versioned library so source and wheel builds use the same link interface. +if(NOT TARGET CUDA::nvrtc) + find_file( + DEME_NVRTC_LIBRARY + NAMES libnvrtc.so libnvrtc.so.${CUDAToolkit_VERSION_MAJOR} + HINTS + ${CUDAToolkit_LIBRARY_DIR} + ${CUDAToolkit_TARGET_DIR}/lib + ${CUDAToolkit_TARGET_DIR}/lib64 ) -endif() - -# Find Jitify library (NVIDIA version only; HIP uses hiprtc directly via JitKernel abstraction) -if(NOT USE_HIP) - find_path( - JitifyPath - NAMES jitify.hpp - PATHS "${CMAKE_CURRENT_LIST_DIR}/thirdparty/jitify" + if(NOT DEME_NVRTC_LIBRARY) + message(FATAL_ERROR + "CUDA Toolkit ${CUDAToolkit_VERSION} was found at ${CUDAToolkit_TARGET_DIR}, but its NVRTC shared " + "library was not found. Install the CUDA NVRTC runtime and development packages." + ) + endif() + add_library(CUDA::nvrtc SHARED IMPORTED) + set_target_properties(CUDA::nvrtc PROPERTIES + IMPORTED_LOCATION "${DEME_NVRTC_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${CUDAToolkit_INCLUDE_DIRS}" ) - # Keep backward compat alias - set(NVIDIAJitifyPath ${JitifyPath}) + message(STATUS "Recovered CUDA::nvrtc from ${DEME_NVRTC_LIBRARY}") endif() -# Let the user decide if they want to use ChPF -option(USE_CHPF "Toggle the use of ChPF for outputting" OFF) - -if(USE_CHPF) - # Find ChPF, else fetch it - find_package(ChPF 3.0 QUIET) - if (NOT ChPF_FOUND) - FetchContent_Declare( - ChPF - GIT_REPOSITORY https://gitlab.com/uwsbel/chpf.git - GIT_TAG 0699f01e720cecf58719a1651ab797c6a62a1897 - ) - FetchContent_MakeAvailable(ChPF) - endif() - - # Based on whether ChPF was found or fetched, set the target name CMake can use to find it - if (ChPF_FOUND) - set(ChPF_IMPORTED_NAME "ChPF::ChPF") - elseif (chpf_POPULATED) - set(ChPF_IMPORTED_NAME "ChPF") - endif() +# Option to build Python bindings +option(DEME_BUILD_PYTHON "Build Python bindings for DEME" OFF) +option(DEME_BUILD_DEMOS "Build DEME demos and modular tests" ON) +option(DEME_BUILD_DOCS "Add the Doxygen and Sphinx documentation target" OFF) +option(DEME_BUILD_VISUALIZER "Build the native OpenGL/ImGui visualizer" ON) + +# Find Python and pybind11 if building Python bindings +if(DEME_BUILD_PYTHON) + # Extension modules need the target interpreter and its module headers, but not the embedding library. Manylinux + # deliberately omits Development.Embed, so requesting the aggregate Development component breaks wheel builds. + find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) + add_subdirectory(thirdparty/pybind11) + message(STATUS "Python bindings enabled") + message(STATUS "Python version: ${Python_VERSION}") + message(STATUS "Python executable: ${Python_EXECUTABLE}") endif() -# Let the user decide if they want to use managed arrays, rather than default cudaMalloc and cudaMallocHost memory. -# Note that turning this on gives no performance benefits, and it's considered legacy. -set(USE_MANAGED_ARRAYS_DESC - "Use managed CUDA arrays (instead of manual CUDA host and device memory) to store data" - "Switching it on makes the project operable on a Linux platform (excluding WSL) only." +# Find CUB library (this might need to be done in source-level config) +find_package( + CUB REQUIRED + HINTS ${CUDAToolkit_ROOT}/lib64/cmake/cub +) + +# Find NVIDIA's Jitify library +find_path( + NVIDIAJitifyPath + NAMES jitify.hpp + PATHS "${CMAKE_CURRENT_LIST_DIR}/thirdparty/jitify" ) -option(USE_MANAGED_ARRAYS "${USE_MANAGED_ARRAYS_DESC}" OFF) # ---------------------------------------------------------------------------- # # Global Configuration @@ -167,13 +137,8 @@ cxx_std_autodetect() set(ProjectIncludeSource "${CMAKE_CURRENT_SOURCE_DIR}/src") set(ProjectIncludeGenerated "${CMAKE_BINARY_DIR}/src") -# Global fix for CUDA/HIP language bug -if(USE_HIP) - # Add HIP include directories for all targets (both CXX and HIP) - include_directories(${HIP_INCLUDE_DIR}) -else() - include_directories(${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) -endif() +# Global fix for CUDA language bug +include_directories(${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) #------------------------------------------------------------ @@ -187,8 +152,19 @@ set(DEME_INSTALL_DEMO "bin") # ---------------------------------------------------------------------------- # add_subdirectory(src/core) -add_subdirectory(src/DEM) add_subdirectory(src/algorithms) +if(DEME_BUILD_VISUALIZER) + # Build only raylib's static graphics library. Audio and upstream examples are unrelated to DEME visualization. + set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(CUSTOMIZE_BUILD ON CACHE BOOL "" FORCE) + set(USE_AUDIO OFF CACHE BOOL "" FORCE) + set(SUPPORT_MODULE_RAUDIO OFF CACHE BOOL "" FORCE) + set(WITH_PIC ON CACHE BOOL "" FORCE) + add_subdirectory(thirdparty/raylib EXCLUDE_FROM_ALL) + add_subdirectory(src/visualization) +endif() +add_subdirectory(src/DEM) # ---------------------------------------------------------------------------- # # Final Library Generation @@ -212,53 +188,18 @@ target_include_directories(simulator_multi_gpu PUBLIC ${CORE_INTERFACE} ) -# If use ChPF, inform the source -if(USE_CHPF) - target_compile_definitions(simulator_multi_gpu PUBLIC DEME_USE_CHPF) - set(USE_CHPF_STR "ON") - - if(USE_HIP) - find_library(HIPRTC_LIB hiprtc HINTS ${ROCM_PATH}/lib /opt/rocm/lib REQUIRED) - target_link_libraries(simulator_multi_gpu - PUBLIC hip::host - PUBLIC ${HIPRTC_LIB} - PUBLIC ${ChPF_IMPORTED_NAME} - PUBLIC DEMERuntimeDataHelper - ) - else() - target_link_libraries(simulator_multi_gpu - PUBLIC CUDA::cudart - PUBLIC CUDA::nvrtc - PUBLIC CUDA::cuda_driver - PUBLIC ${ChPF_IMPORTED_NAME} - PUBLIC DEMERuntimeDataHelper - ) - endif() -else() - set(USE_CHPF_STR "OFF") - - if(USE_HIP) - find_library(HIPRTC_LIB hiprtc HINTS ${ROCM_PATH}/lib /opt/rocm/lib REQUIRED) - target_link_libraries(simulator_multi_gpu - PUBLIC hip::host - PUBLIC ${HIPRTC_LIB} - PUBLIC DEMERuntimeDataHelper - ) - else() - target_link_libraries(simulator_multi_gpu - PUBLIC CUDA::cudart - PUBLIC CUDA::nvrtc - PUBLIC CUDA::cuda_driver - PUBLIC DEMERuntimeDataHelper - ) - endif() -endif() - -# If use managed arrays, define a macro -if(USE_MANAGED_ARRAYS) - target_compile_definitions(simulator_multi_gpu PUBLIC DEME_USE_MANAGED_ARRAYS) -endif() +# Native targets run against the synchronized data and kernels in the build tree, even when the Python extension is +# enabled in the same configuration. The extension links its Python-specific helper directly in src/DEM/CMakeLists.txt. +set(DEME_LINK_LIBS + CUDA::cudart + CUDA::nvrtc + CUDA::cuda_driver + DEMERuntimeDataHelper +) +target_link_libraries(simulator_multi_gpu + PUBLIC ${DEME_LINK_LIBS} +) # Specific to Windows... if(WIN32) target_link_libraries(simulator_multi_gpu @@ -266,40 +207,24 @@ if(WIN32) endif() # Attach include directories to the top-level library target -if(USE_HIP) - set_target_properties(simulator_multi_gpu - PROPERTIES - LINKER_LANGUAGE HIP - ) -else() - set_target_properties(simulator_multi_gpu - PROPERTIES - LINKER_LANGUAGE CUDA - ) -endif() +set_target_properties(simulator_multi_gpu + PROPERTIES + LINKER_LANGUAGE CUDA +) -if(DEME_BUILD_PYTHON) - pybind11_add_module(_deme - src/DEM/PyDEME.cpp - $ - $ - $ +if(DEME_BUILD_VISUALIZER) + # Keep graphics dependencies out of the solver library. Only its tiny callback bridge belongs in the archive. + # Linking the static solver into both the executable and this shared facade duplicates JIT global destructors. + target_sources(simulator_multi_gpu PRIVATE $) + add_library(DEMEVisualizer SHARED $) + target_link_libraries(DEMEVisualizer PRIVATE raylib) + target_link_libraries(DEMEVisualizer INTERFACE simulator_multi_gpu) + target_include_directories(DEMEVisualizer PUBLIC + $ + $ + $ ) - target_include_directories(_deme PRIVATE ${ProjectIncludeSource} ${ProjectIncludeGenerated}) - target_link_libraries(_deme PRIVATE - CUDA::cudart - CUDA::nvrtc - CUDA::cuda_driver - DEMERuntimeDataHelper_python - pybind11::headers - ) - if(USE_CHPF) - target_link_libraries(_deme PRIVATE ${ChPF_IMPORTED_NAME}) - endif() - set_target_properties(_deme PROPERTIES CXX_STANDARD ${CXXSTD_SUPPORTED} POSITION_INDEPENDENT_CODE TRUE) - install(TARGETS _deme LIBRARY DESTINATION deme COMPONENT python) - install(FILES src/DEM/python/deme/__init__.py DESTINATION deme COMPONENT python) - install(FILES src/DEM/python/DEME.py DESTINATION . COMPONENT python) + set_target_properties(DEMEVisualizer PROPERTIES CXX_STANDARD ${CXXSTD_SUPPORTED} WINDOWS_EXPORT_ALL_SYMBOLS TRUE) endif() # ---------------------------------------------------------------------------- # @@ -316,6 +241,22 @@ install( DESTINATION ${CMAKE_INSTALL_LIBDIR} ) +# Python wheels embed the visualizer object code in ``_deme`` and must not also install the standalone C++ facade. +# That facade links the source/install runtime helper used by native CMake consumers, while Python uses its own static +# runtime-path helper. Keeping the native facade out of the wheel prevents an invalid build-tree helper dependency. +set(DEME_INSTALL_VISUALIZER ${DEME_BUILD_VISUALIZER}) +if(SKBUILD) + set(DEME_INSTALL_VISUALIZER OFF) +endif() + +if(DEME_INSTALL_VISUALIZER) + install( + TARGETS DEMEVisualizer + EXPORT DEMETargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ) +endif() + install(CODE "message(\"Removing build-tree libraries from installation target...\")") install(CODE "file(REMOVE \"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/libDEMERuntimeDataHelper.so\")") @@ -324,7 +265,7 @@ install( EXPORT DEMETargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ) +) # Export the generated library target export(EXPORT DEMETargets @@ -386,6 +327,12 @@ install( # Copy data directory over # ---------------------------------------------------------------------------- # file(COPY ${CMAKE_CURRENT_LIST_DIR}/data/ DESTINATION ${CMAKE_BINARY_DIR}/data/) +# Keep build-tree data in sync (so newly added meshes like STL assets are always available without re-running CMake) +add_custom_target(sync_data ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_LIST_DIR}/data + ${CMAKE_BINARY_DIR}/data + COMMENT "Syncing data directory into build tree") install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/data/ DESTINATION ${CMAKE_INSTALL_DATADIR}/DEME/data/) # ---------------------------------------------------------------------------- # @@ -394,6 +341,17 @@ install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/data/ DESTINATION ${CMAKE_INSTALL_DA file(COPY ${CMAKE_CURRENT_LIST_DIR}/src/kernel/ DESTINATION ${CMAKE_BINARY_DIR}/kernel/) file(COPY ${CMAKE_CURRENT_LIST_DIR}/src/DEM/Defines.h DESTINATION ${CMAKE_BINARY_DIR}/DEM) file(COPY ${CMAKE_CURRENT_LIST_DIR}/src/DEM/VariableTypes.h DESTINATION ${CMAKE_BINARY_DIR}/DEM) +add_custom_target(sync_kernels ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_LIST_DIR}/src/kernel + ${CMAKE_BINARY_DIR}/kernel + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_LIST_DIR}/src/DEM/Defines.h + ${CMAKE_BINARY_DIR}/DEM/Defines.h + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_LIST_DIR}/src/DEM/VariableTypes.h + ${CMAKE_BINARY_DIR}/DEM/VariableTypes.h + COMMENT "Syncing kernel sources into build tree") install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/src/kernel/ DESTINATION ${CMAKE_INSTALL_DATADIR}/DEME/kernel/) install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/src/kernel/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kernel/ FILES_MATCHING @@ -407,3 +365,7 @@ install(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/src/kernel/ DESTINATION ${CMAKE_INST if(DEME_BUILD_DEMOS) add_subdirectory(src/demo) endif() + +if(DEME_BUILD_DOCS) + add_subdirectory(docs) +endif() diff --git a/LICENSE.md b/LICENSE.md index 3e32d6c0..d3370ae5 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -11,7 +11,7 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -> See CONTRIBUTORS.md for a list of project authors by name and date. +> See docs/CONTRIBUTORS.md for a list of project authors by name and date. ### NOTICE OF THIRDPARTY LICENSES diff --git a/MANIFEST.in b/MANIFEST.in index 248269f9..b128386f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ include README.md include LICENSE.md -include VERSION include pyproject.toml recursive-include data * recursive-include src/kernel *.cu *.cuh +include src/jitify/jitify.hpp include src/DEM/Defines.h include src/DEM/VariableTypes.h diff --git a/README.md b/README.md index 27140199..cca416fd 100644 --- a/README.md +++ b/README.md @@ -1,374 +1,149 @@ -# SBEL GPU DEM-Engine -__A dual-GPU DEM solver with complex grain geometry support__ +# SBEL Chrono DEM-Engine -### 🚨 DEM-Engine now works on both Linux (including WSL) and Windows! 🚨 +**Now version 3: GPU-accelerated discrete element simulation built for performance, with C++ and Python APIs.** -**⚠️ Windows users are still encouraged to use it via WSL for a more well-tested experience. ⚠️** +DEM-Engine (DEME) simulates granular materials using one or two NVIDIA GPUs. +This branch supports sphere clumps, mesh particles, analytical boundaries, +rigid combined owners, and customizable contact force models. It also provides +an interactive visualizer and host/device data access for co-simulation.

- - + DEM-Engine granular simulation + DEM-Engine demo animation +

- - + DEM-Engine simulation animation + DEM-Engine demo animation

-## Quick links +## What's new in DEME 3? + +**[Get started with DEME 3: installation guide](docs/installation.rst)** — +Python packages, C++ source builds, and system requirements. -
  • Overview, movies of demos, and where to get help
  • +- **Mesh contact:** mesh–mesh collisions and a new clump–mesh scheme that combines + triangle contributions into patch/island contacts before evaluating forces. +- **Rigid combined bodies:** group members into one rigid assembly, replacing + geometry-wildcard-based constructions with member-level controls. +- **On-device coupling:** exchange state and forces directly with other GPU packages. +- **Persistent kernel caching:** reuse compiled kernels across compatible repeated + runs to reduce initialization time. +- **Interactive visualization and expanded Python workflows.** -
  • Use DEME with Python
  • +**When to stay with DEME 2:** DEME 3 currently supports only NVIDIA GPUs and may +use more memory. If you need non-NVIDIA GPU support, or your application does +not need mesh–mesh contact or the new aggregated clump–mesh contact scheme, +consider staying with **DEME 2.4.2**, the final DEME 2 release. + +For C++, use the upstream [v2.4.2 tag](https://github.com/projectchrono/DEM-Engine/tree/v2.4.2): -
  • How to compile from source
  • - -
  • Numerical examples and use cases
  • - - - -
  • Install as C++ library
  • - -
  • Postprocessing recommendation
  • - -
  • Licensing
  • - -
  • Limitations
  • - -
  • Cite this work
  • - - -

    Description

    - -DEM-Engine, nicknamed _DEME_, does Discrete Element Method simulations: - -- Using up to two GPUs at the same time (works great on consumer _and_ data center GPUs, on both NVIDIA CUDA and AMD ROCm). -- With the particles having complex shapes represented by clumped spheres. -- With support for customizable contact force models (want to add a non-standard cohesive force, or an electrostatic repulsive force? You got this). -- With an emphasis on computational efficiency. As a rule of thumb, using 3-sphere clump elements, simulating 1 million elements for 1 million time steps takes around 1 hour on two RTX 3080s. -- Supporting a wide range of problems with flexible API designs. Deformable meshes and grain breakage can be simulated by leveraging the explicit controls given to the user. -- With support for co-simulation with other C/C++ packages, such as [Chrono](https://github.com/projectchrono/chrono). - -Currently _DEME_ is a C++ package with an API design similar to Chrono's, and should be easy to learn for existing Chrono users. - -You can find the movies of some of _DEME_'s demos [here](https://uwmadison.app.box.com/s/u4m9tee3k1vizf097zkq3rgv54orphyv). - -You are welcome to discuss _DEME_ on [Project Chrono's forum](https://groups.google.com/g/projectchrono). - -

    PyDEME

    - -_DEME_ is now available as a Python package, _pyDEME_. It is quick to install and pick up its usage by trying this Python version. If you want to maximize the performance and use the cutting-edge features, you can instead install the C++ version of _DEME_ from source. - -> **pyDEME binary-wheel support:** The PyPI package currently supports 64-bit x86 Linux (including Linux under WSL), CPython 3.9 through 3.14, and NVIDIA GPUs through CUDA. Native Windows, macOS, Linux on ARM, PyPy, and AMD ROCm wheels are not currently provided. The CUDA driver and runtime must be installed separately; they are not bundled in the wheel. These restrictions apply to the PyPI package, not necessarily to DEME built from source. - -To install _pyDEME_, use a supported Linux environment and install CUDA if you do not already have it. Useful installation instructions may be found [here](https://developer.nvidia.com/cuda-downloads). - -- CUDA 12.8 is recommended. Versions older than 12.8 generally work, but note there are scattered bugged versions such as 11.3, 12.1, 12.2, 12.3 etc. - - Avoid CUDA 13.0+. Currently version 13.0+ results in extremely slow just-in-time compilation. This may be fixed in future versions of DEME. - - On Linux, do not go above GCC 15+ because its glibc might bring math function conflicts with CUDA. In general, GCC 11 is recommended and if you use Ubuntu, simply go version 22.04 and do not try 26.04. - -Once CUDA is ready, you can `pip` install _pyDEME_. In your conda environement, do -``` -conda create -n pyDEME python=3.11 -conda activate pyDEME -conda install cmake -pip3 install deme -``` - -`pyDEME` can be replaced with an environment name of your choice. Any supported CPython version from 3.9 through 3.14 can be used in place of 3.11. - -Then [Python scripts](https://github.com/projectchrono/DEM-Engine/tree/pyDEME_demo/src/demo) can be executed in this environment. To understand the content of each Python demo, refer to the explanations of the C++ demos with the same names in Numerical examples section. - -If you use _pyDEME_ in conjunction with PyChrono, you should `import pyDEME` first, then PyChrono. - -### Persistent Jitify startup cache - -_DEME_ uses Jitify/NVRTC to compile CUDA kernels at solver initialization time. -By default, _DEME_ keeps upstream Jitify header loading behavior. - -For workloads that start many fresh _DEME_ processes, users can opt in to a -persistent header source cache. Set `DEME_PERSISTENT_JITIFY_CACHE` to any -writable file path where _DEME_ should create and reuse the cache file. The file -does not need to exist before the first run. For a simple per-user temporary -cache path, set it to `1`, `true`, `on`, or `yes`. - -Automatic path: - -``` -export DEME_PERSISTENT_JITIFY_CACHE=1 -``` - -Explicit path: - -``` -export DEME_PERSISTENT_JITIFY_CACHE="$HOME/.cache/deme/jitify_header_cache.bin" -mkdir -p "$(dirname "$DEME_PERSISTENT_JITIFY_CACHE")" -``` - -The persistent cache follows the CUDA/toolchain setup, not the simulation -setup. Changing material properties, geometry, particle counts, or timesteps -does not invalidate it. If CUDA versions, include paths, or compiler options -change, _DEME_ ignores the old cache file and fills it again during that run. -Unset `DEME_PERSISTENT_JITIFY_CACHE`, or set it to `0`, `false`, `off`, or -`no`, to use the default Jitify behavior. With the automatic path on Linux/WSL, -_DEME_ uses `/tmp/deme_jitify_header_cache_$USER.bin`. - -Pros: -- Can significantly reduce startup time for workflows that launch many fresh - _DEME_ processes with the same CUDA/toolchain configuration. -- Keeps the default behavior unchanged unless the environment variable is set. -- Safely falls back to rediscovering headers when the CUDA/toolchain setup - changes. - -Cons: -- The first run still has to discover and save the header sources. -- Users must choose a writable cache location and manage it like other local - build/runtime caches. -- If CUDA headers or include paths change, the old file may no longer be useful; - deleting the cache file forces a clean rebuild. - -

    Compilation

    - -You can also build C++ _DEME_ from source. It allows for potentially more performance and more tailoring. - -### Linux and WSL - -First, [install CUDA](https://developer.nvidia.com/cuda-downloads). The newest release is recommended. - -Once CUDA is ready, clone this project and then: - -``` -git submodule init -git submodule update -``` - -This will pull the submodule NVIDIA/jitify so that we can do runtime compilation. - -Then, one typical choice is to make a build directory in it. Then in the build directory, use `cmake` to configure the compilation. An example: - -``` -mkdir build -cd build -cmake -DCMAKE_BUILD_TYPE=Release .. -``` - -You may want to use [this information](https://askubuntu.com/questions/1203635/installing-latest-cmake-on-ubuntu-18-04-3-lts-run-via-wsl-openssl-error) if you need to update cmake to the newest. - -We suggest that you install a `cmake` GUI such as `ccmake`, and `ninja_build` generator, to better help you configure the project. In this case, the example above can be done like this alternatively: - -``` -mkdir build -cd build -ccmake -G Ninja .. -``` - -You generally do not have to change the build options in the GUI, but preferably you can change `CMAKE_BUILD_TYPE` to `Release`, and if you need to install this package as a library you can specify a `CMAKE_INSTALL_PREFIX`. - -Some additional troubleshooting tips for generating the project: - -- If some dependencies such as CUB are not found, then you probably need to manually set `$PATH` and `$LD_LIBRARY_PATH`. An example is given below for a specific version of CUDA, note it may be different on your machine or cluster. You should also inspect if `nvidia-smi` and `nvcc --version` give correct returns. -``` -export CPATH=/usr/local/cuda-12.8/targets/x86_64-linux/include${CPATH:+:${CPATH}} -export PATH=/usr/local/cuda-12.8/bin${PATH:+:${PATH}} -export PATH=/usr/local/cuda-12.8/lib64/cmake${PATH:+:${PATH}} -export LD_LIBRARY_PATH=/usr/local/cuda-12.8/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} -export CUDA_HOME=/usr/local/cuda-12.8 -``` - -Finally, build the project. - -``` -ninja +```bash +git clone --branch v2.4.2 --recurse-submodules https://github.com/projectchrono/DEM-Engine.git DEM-Engine-2.4.2 ``` -Some additional troubleshooting tips for building the project: +For pyDEME, explicitly pin the Python distribution: `python -m pip install "deme==2.4.2"`. +Use version 2.4.2's installation requirements and examples for either route. +See [DEME 3 features and migration considerations](docs/deme3-new-features.rst) +for details, including the contact-model changes and memory tradeoffs. -- If you see some grammatical errors during compilation, such as `filesystem` not being a member of `std` or arguments not expanded with `...`, then manually setting the flag `TargetCXXStandard` to `STD_CXX17` might help. -- If CUB is not found, then you may manually set it in the `ccmake` GUI as `/usr/local/cuda/lib64/cmake/cub`. It may be a slightly different path on your machine or cluster. -- If `libcudacxx` is not found, then you may manually set it in the `ccmake` GUI as `/usr/local/cuda-12.8/targets/x86_64-linux/lib/cmake/libcudacxx`. Depending on your CUDA version it may be a slightly different path on your machine or cluster. You may also try to find these packages using `find`. +## Why use DEME? -### AMD GPUs (ROCm) +DEME is designed for large granular simulations where particle shape, contact +physics, and computational cost matter. Typical applications include mixing, +hopper flow, soil penetration, wheel–terrain interaction, and granular impact. -_DEME_ also builds and runs on AMD GPUs through ROCm/HIP. Instead of CUDA, install a recent [ROCm](https://rocm.docs.amd.com/) release (the HIP runtime, `hiprtc`, and `hipCUB` are required). You do not need the NVIDIA/jitify submodule for an AMD build; runtime kernel compilation is handled by `hiprtc`. +- **Complex particle shapes.** Represent grains with clumped spheres or mesh + particles, and build rigid assemblies with combined owners. DEME supports + **mesh–mesh contact**, allowing mesh particles to collide with one another. +- **Custom contact physics.** Define your own contact force models, including + cohesion, electrostatic interactions, and bonds that can break. Material + properties and per-contact variables let you tailor the model to your problem. +- **GPU performance.** Use one or two NVIDIA GPUs, including consumer and data + center hardware. As an illustrative benchmark from the main-branch README, + one million three-sphere clumps simulated for one million timesteps takes + around one hour on RTX 3080s. Runtime depends on the geometry, contact + model, and simulation settings. +- **Control over the simulation.** Prescribe motion, extract forces, and update + geometry to model processes such as mesh deformation or grain breakage. + The examples show how to supply these behaviors through the API. +- **On-device co-simulation.** Exchange simulation state and forces directly + with other GPU-based packages through device buffers, avoiding CPU round trips + for the exchanged data. Host data access also supports coupling to solvers + such as [Chrono](https://github.com/projectchrono/chrono) for multibody dynamics + or other physics. +- **C++ and Python workflows.** Start with Python or integrate the C++ library + into an application. The C++ API follows a Chrono-like design, and the + interactive visualizer helps inspect simulations as they run. -Configure the project the same way as in the **Linux and WSL** section, but pass `-DUSE_HIP=ON`, and set `CMAKE_HIP_ARCHITECTURES` to your GPU's architecture (for example `gfx90a` for MI200-series, `gfx1100` for RDNA3 desktop, or `gfx1201` for RDNA4). An example: +## Start here -``` -mkdir build -cd build -cmake -DCMAKE_BUILD_TYPE=Release -DUSE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx90a -DCMAKE_PREFIX_PATH=/opt/rocm .. -ninja -``` - -If `CMAKE_HIP_ARCHITECTURES` is left unset it defaults to `gfx90a`. You can list the architecture of an installed device with `rocminfo`. If the ROCm install is not on CMake's default search path, point `-DCMAKE_PREFIX_PATH` at it (e.g. `/opt/rocm`) so `find_package` can locate hip and hipCUB. The demos are then run exactly as in the **Numerical examples** section. - -This backend has also been validated on AMD Instinct data-center GPUs: MI300X (`gfx942`) and MI350X (`gfx950`) nodes running ROCm 7.2.0, configured with `-DCMAKE_HIP_ARCHITECTURES="gfx90a;gfx942;gfx950"`. Note that `CMAKE_HIP_ARCHITECTURES` only governs ahead-of-time compiled code; the simulation kernels are compiled at run time by `hiprtc` for whatever device is present, so one multi-architecture build can serve nodes with different GPUs. +| Task | Documentation | +| --- | --- | +| Install Python, build C++, or install the C++ library | [Installation](docs/installation.rst) | +| Run a first simulation | [Quickstart](docs/quickstart.rst) · [Python example](docs/python/quickstart.rst) | +| Understand owners, families, frames, and runtime setup | [Core concepts](docs/concepts.rst) | +| Use mesh particles, templates, and combined bodies | [Mesh particles](docs/mesh-particles.rst) | +| Find a demo to adapt | [Examples](docs/examples.rst) · [C++ sources](src/demo) | +| Use Python | [Python guide](docs/python/index.rst) · [Demos](python/demos/README.md) · [API reference](docs/python/reference.rst) | +| Look up the C++ API | [C++ reference](docs/cpp-api/index.rst) | +| Select GPUs or exchange simulation data | [Device selection](docs/python/device-selection.rst) · [Data access](docs/python/data-access.rst) | +| Visualize results | [Interactive visualization and ParaView](docs/visualization.rst) | +| Diagnose installation or runtime errors | [Troubleshooting](docs/troubleshooting.rst) | +| Build or host the documentation website | [Build and preview](docs/README.md) · [Hosting](docs/developer/hosting.rst) | +| Contribute or cite DEME | [Project information](docs/project.rst) · [Architecture](docs/developer/architecture.rst) | -#### AMD GPUs on native Windows +The [documentation index](docs/index.rst) collects the complete guide. The C++ +reference is rendered from Doxygen by the documentation build; build the site +to browse C++ declarations and Python documentation together. -_DEME_ also builds on Windows with the [AMD HIP SDK](https://rocm.docs.amd.com/projects/install-on-windows/en/latest/) (validated with HIP SDK 7.1 on a Radeon 8060S, `gfx1151`). Two Windows-specific points: +## Python in brief -- CMake does not support mixing MSVC for C++ with clang for HIP, so use the HIP SDK's own clang for both languages. From a VS 2022 Developer shell: +On a supported Linux or WSL2 host with a CUDA 12.9-compatible NVIDIA driver: +```bash +python -m pip install "deme[cuda12]" ``` -cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DUSE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1151 ^ - -DCMAKE_PREFIX_PATH="C:/Program Files/AMD/ROCm/7.1" ^ - -DCMAKE_CXX_COMPILER="C:/Program Files/AMD/ROCm/7.1/bin/clang++.exe" ^ - -DCMAKE_HIP_COMPILER="C:/Program Files/AMD/ROCm/7.1/bin/clang++.exe" .. -ninja -``` - -- At run time, the runtime kernel compilation locates the ROCm headers through the `ROCM_PATH` or `HIP_PATH` environment variable; the HIP SDK installer sets `HIP_PATH`, so no extra setup is normally needed. - -### Windows - -The process is similar to [the installation of Chrono](https://api.projectchrono.org/tutorial_install_chrono.html), which you can use as reference. The steps depend on your choice of tools, and what listed here are our recommendation. - -- Follow the guide in the **Linux and WSL** section to install CUDA on Windows. -- Use [Sourcetree](https://www.sourcetreeapp.com/) to clone the repo's main branch, with the _recursive_ option on. -- Use [CMake GUI](https://cmake.org/download/) to configure the project. You should get all the dependency fields filled automatically; if not, manually fill them. Example CUB directory: `C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.8/lib/cmake/cub`. Use the default `x64` architecture and `MSVC` compiler. If you need to install this package as a library later, you can specify a `CMAKE_INSTALL_PREFIX`. -- After configuration and generation, find the file `Chrono-DEM-Engine.sln` in the build directory, open it using Visual Studio, select `Release` build type and build the project. - -

    Numerical examples

    - -After the build process is done, you can start trying out the demos. On Windows, these demos may be in the `bin\Release` directory instead. - -- `./bin/DEMdemo_SingleSphereCollide` can be used to test a correct installation. If it runs outputting a lot of texts (those are debug messages; the user do not have to worry about the content) and stops without an error in the end, the installation is probably good. -- An all-rounder beginner example featuring a bladed mixer interacting with complex shaped particles: `./bin/DEMdemo_Mixer`. -- A place to learn how prescribed motions work in this package, using either analytical boundaries or particle-represented boundaries: `./bin/DEMdemo_Centrifuge` and `./bin/DEMdemo_Sieve`. -- A few representative engineering experiments reproduced in DEM simulations, which potentially serve as starting points for your own DEM scripts: `/bin/DEMdemo_BallDrop`, `./bin/DEMdemo_ConePenetration`, `/bin/DEMdemo_RotatingDrum`, `./bin/DEMdemo_Repose`, `./bin/DEMdemo_Plow`. -- `./bin/DEMdemo_WheelDP` shows how to load a checkpointed configuration file to instantly generate a settled granular terrain, then run a drawbar-pull test on it. This demo therefore requires you to first finish the two GRCPrep demos to obtain the terrain checkpoint file. The granular terrain in these demos features DEM particles with a variety of sizes and shapes. -- `./bin/DEMdemo_WheelDPSimplified` is a simplified version of the previous drawbar-pull test which has no prerequisite. The terrain is simpilified to be made of only one type of irregular-shaped particles. It serves as a quick starting point for people who want to create similar experiments. -- `./bin/DEMdemo_Indentation` is a more advanced examples showing the usage of the custom additional properties (called _wildcards_) that you can associate with the simulation entities, and use them in the force model and/or change them in simulation then deposit them into the output files. _Wildcards_ have more use cases especially if coupled together with a custom force model, as shown in some of the follwing demos. -- `./bin/DEMdemo_Electrostatic` simulates a pile of complex-shaped and charged granular particles interacting with a mesh that is also charged. Its purpose is to show how to define a non-local force (electrostatic force) which takes effect even when the bodies are not in contact, using a custom force model file. This idea can be extended to modeling a custom cohesion force etc. -- `./bin/DEMdemo_FlexibleMesh` simulates a deforming mesh interacting with DEM particles. The intention is to show that the user can extract the force pairs acting on a mesh, then update the mesh with deformation information. _DEME_ does not care how this deformation is calculated. Presumably the user can feed the forces to their own solid mechanics solver to get the deformation. _DEME_ does not come with a built-in linear solver so for simplicity, in this demo the mesh deformation is instead prescribed. -- `./bin/DEMdemo_GameOfLife` is a fun game-of-life simulator built with the package, showing the flexibility in terms of how you can use this tool. -- `./bin/DEMdemo_FractureBox` simulates a concrete bar breaking using a custom force model that creates inter-particle bonds and lets them break under certain conditions. This is a showcase for advanced usage of custom models that involves per-contact wildcard variables. -- It is a good idea to read the comment lines at the top of the demo files to understand what they each does. - -[The documentations for _DEME_](https://api.projectchrono.org/) are hosted on Chrono website (work in progress). -Some additional troubleshooting tips for running the demos: +The `cuda12` extra installs CUDA runtime libraries, NVRTC, and headers through pip; +no system CUDA Toolkit installation is needed for Python wheels. This setup applies +only to the Python extension; standalone C++ applications keep their normal CUDA +configuration. Use plain `pip install deme` to use an existing toolkit. -- If the program silently exits while creating the `DEMSolver` object, check if your NVIDIA driver supports your current CUDA version (`nvidia-smi`). -- If errors similar to `CUDA_ERROR_UNSUPPORTED_PTX_VERSION` are encountered while you run the demos, or (rarely) the simulations proceed without detecting any contacts, then please make sure the CUDA installation is the same version as when the code is compiled. -- Another cause for the simulations proceeding without detecting any contacts, could be the force kernel silently failed. This could also lead to a **too-many-geometries-in-bin** crash. The cause is usually that the force kernel was launched with too many threads per block, therefore not enough registers can be leveraged. This can be avoided by calling `SetForceCalcThreadsPerBlock` prior to the start of simulation with the argument being smaller choices like 128. **Note that you should only try this if you are using a custom force model**. -- Used your own force model but got runtime compilation error like `expression must have pointer-to-object type but it has type "float"`, or `unknown variable "delta_time"`? Check out what we did in demo `DEMdemo_Electrostatic`. You may need to manually specify what material properties are pairwise and what contact wildcards you have using `SetMustPairwiseMatProp` and `SetPerContactWildcards`. -- Just running provided demos or a script that used to work, but the jitification of the force model failed or the simulation fails at the first kernel call (probably in `DEMCubContactDetection.cu`)? Then did you pull a new version and just re-built in-place? A new update may modify the force model, and the force model in _DEME_ are given as text files so might not be automatically copied over when the project is re-built. I am sorry for the trouble it might cause, but you can do a clean re-build from an empty directory and it should fix the problem. Do not forget to first commit your own branches' changes and relocate the data you generated in the build directory. Another solution is to copy everything in `src/DEM` to the `DEM` directory in the build directory, then everything in `src/kernel` to the `kernel` directory in the build directory, then try again. +```python +import deme - - -

    Limitations

    - -_DEME_ is designed to simulate the interaction among clump-represented particles, the interaction between particles and mesh-represented bodies, as well as the interaction between particles and analytical boundaries. _DEME_ does not resolve mesh–mesh or mesh–analytical contacts. - -- It is able to handle mesh-represented bodies with relatively simple physics, for example a meshed plow moving through granular materials with a prescribed velocity, or several meshed projectiles flying and hitting the granular ground. -- However, if the bodies' physics are complex multibody problems, say it is a vehicle that has joint-connected parts and a motor with certain driving policies, or the meshed bodies have collisions among themselves that needs to be simulated, then _DEME_ alone does not have the infrastructure to handle them. But you can install _DEME_ as a library and do coupled simulations with other tools such as [Chrono](https://github.com/projectchrono/chrono), where _DEME_ is exclusively tasked with handling the granular materials and the influence they exert on the outside world (with high efficiency, of course). See the following section. - -

    Install as library

    - -### Linux and WSL - -Set `CMAKE_INSTALL_PREFIX` flag in `cmake` GUI to your desired installation path and then - -``` -ninja install +solver = deme.DEMSolver() ``` -### Windows +After installation, run a Python demo from the repository root: -Set `CMAKE_INSTALL_PREFIX` flag in `cmake` GUI to your desired installation path, then open a command line window in Visual Studio (may be found in Tools -> Command Line), and use the following command to install +```bash +python python/demos/single_sphere_collide.py --smoke-test ``` -cmake --install . -``` - -### Example scripts in Chrono-projects - -We provide examples of linking against both [Chrono](https://github.com/projectchrono/chrono) and _DEME_ for co-simulations in [chrono-projects](https://github.com/projectchrono/chrono-projects/tree/feature/DEME). - -**For now, you need to checkout the `feature/DEME` branch of chrono-projects after cloning the code to access the scripts!** - -You need to build `chrono-projects` linking against a Chrono installation (Chrono installation guide is [here](https://api.projectchrono.org/tutorial_install_chrono_linux.html); note you have to `make install` to install Chrono, not just build it), then link against _DEME_. The steps for building `chrono-projects`: -- Start by linking against Chrono. Set `Chrono_DIR`. It should be in `/lib/cmake`. Then configure the project; -- Make sure `ENABLE_PROJECTS` to `ON` and configure the project; -- Linkage against Chrono is done, now move on to link against _DEME_. Set `ENABLE_DEME_TESTS` to `ON`. Then configure the project; -- Set `DEME_DIR` when prompted. It should be in `/lib64/cmake/DEME`. Then configure the project. -- You may see the `ChPF_DIR` option being prompted. If you did not build _DEME_ with USE_CHPF being ON, you can ignore this option. Otherwise CMake will refuse to generate, and you need to set this option to be `/lib64/cmake/ChPF`. +This headless example simulates two colliding spheres over meshes and writes +visualization files. The first run may take time to compile CUDA kernels. +See the [Python demos](python/demos/README.md) for more examples, command-line +options, and instructions for viewing their output. -Then build the project and you should be able to run the demo scripts that demonstrate the co-simulation between _DEME_ and Chrono. +See [installation requirements](docs/installation.rst) for wheel compatibility +and source builds. New scripts should use `import deme`; `import DEME` remains +a compatibility alias. Features in this checkout may be newer than a released wheel. -More documentations on using this package for co-simulations are being added. +## Community and license -

    Postprocessing recommendation

    +[Demo videos](https://uwmadison.app.box.com/s/u4m9tee3k1vizf097zkq3rgv54orphyv) · +[Project Chrono forum](https://groups.google.com/g/projectchrono) · +[Contributors](docs/CONTRIBUTORS.md) · [BSD-3-Clause license](LICENSE.md) · +[Citation](#citation) -_DEME_ mainly outputs clumps or component spheres in `csv` format, so you can postprocess the raw data however you desire. The following is simply an example work flow which gives a quick and interactive rendering scene of the outputted component spheres (`WriteSphereFile`) using [ParaView](https://www.paraview.org/). +## Citation -- Directly load the outputted `vtk` mesh files in ParaView and show them. -- Load the `csv` component sphere files. Use filter **Table To Points**, and set X, Y and Z coordinates to be read from the _X_, _Y_ and _Z_ columns of the `csv` files, then click **Apply**. After that, apply **Glyph** filter. Set **Glyph Type** to be _Sphere_ (you may reduce **Theta** and **Phi Resolition** to save memory), then set the option **Scale Array** to be the _r_ column of the `csv` file, set **Scale Factor** to be 2 (_DEME_ outputs the radii but in ParaView we should scale the spheres by diameter). Finally, set **Glyph Mode** to _All Points_, then click **Apply**. +If you use DEME in your research, please cite the +[DEM-Engine design and usage paper](https://doi.org/10.1016/j.cpc.2024.109196): -

    Licensing

    - -This project should be treated as the collective intellectual property of the Author(s) and the University of Wisconsin - Madison. The following copyright statement should be included in any new or modified source files -``` -Copyright (c) 2021, Simulation-Based Engineering Laboratory -Copyright (c) 2021, University of Wisconsin - Madison - -SPDX-License-Identifier: BSD-3-Clause -``` - -New authors should add their name to the file `CONTRIBUTORS.md` rather than to individual copyright headers. - -#### Notes on code included from Project Chrono - -This project exists independently of Chrono so developers should be sure to include the appropriate BSD license header on any code which is sourced from Chrono::GPU(DEM) or other parts of Chrono. - -> #### SAMPLE header for files sourced from Chrono - -> ``` -> Copyright (c) 2021, SBEL GPU Development Team -> Copyright (c) 2021, University of Wisconsin - Madison -> -> SPDX-License-Identifier: BSD-3-Clause -> -> -> This file contains modifications of the code authored by the Project Chrono -> Development Team. The original license can be found below: -> -> Copyright (c) 2016, Project Chrono Development Team -> All rights reserved. -> -> Use of this source code is governed by a BSD-style license that can be found -> in the LICENSE file at the top level of the distribution and at -> http://projectchrono.org/license-chrono.txt. A copy of the license is below. -> -> Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -> -> - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -> - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -> - Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -> -> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -> ``` - -

    Citation

    - -See [the paper that explains the design and usage of _DEME_](https://www.sciencedirect.com/science/article/pii/S001046552400119X?via%3Dihub) and cite ```bibtex @article{zhang_2024_deme, title = {Chrono {DEM-Engine}: A Discrete Element Method dual-{GPU} simulator with customizable contact forces and element shape}, @@ -383,7 +158,9 @@ keywords = {Discrete Element Method, GPU computing, Physics-based simulation, Sc } ``` -See [the paper on using _DEME_ for simulating rover dynamics](https://link.springer.com/article/10.1007/s00366-023-01921-9) and cite +For the clump-based granular solver and its application to rover dynamics, see +[the granular simulation paper](https://doi.org/10.1007/s00366-023-01921-9): + ```bibtex @article{ruochunGRC-DEM2023, title={A {GPU}-accelerated simulator for the {DEM} analysis of granular systems composed of clump-shaped elements}, diff --git a/VERSION b/VERSION index 8e8299dc..eea30e59 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.4.2 +3.0.13 diff --git a/cmake/DEMEConfig.cmake.in b/cmake/DEMEConfig.cmake.in index e6fbcbb5..b164b77a 100644 --- a/cmake/DEMEConfig.cmake.in +++ b/cmake/DEMEConfig.cmake.in @@ -30,7 +30,6 @@ endif() set(DEME_INCLUDE_DIRS @CONF_INCLUDE_DIRS@) set(DEME_DATA_DIRS @CONF_SHARE_DATA_DIRS@) set(DEME_LIBRARIES DEME::simulator_multi_gpu) - -# The info on whether it is compiled with ChPF on -set(DEME_WITH_CHPF "@USE_CHPF_STR@") - +if(@DEME_INSTALL_VISUALIZER@) + list(APPEND DEME_LIBRARIES DEME::DEMEVisualizer) +endif() diff --git a/cmake/FixNinjaColors.cmake b/cmake/FixNinjaColors.cmake index 9522de7a..775923aa 100644 --- a/cmake/FixNinjaColors.cmake +++ b/cmake/FixNinjaColors.cmake @@ -36,7 +36,7 @@ function(fix_ninja_colors) # Fix GCC colors if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") #set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "-fdiagnostics-color=always" PARENT_SCOPE) - string(APPEND CMAKE_CXX_FLAGS " -fdiagnostics-color=always") + string(APPEND CMAKE_CXX_FLAGS " -fdiagnostics-color=always") # Fix Clang colors elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") diff --git a/data/mesh/TriangleShape4mm.stl b/data/mesh/TriangleShape4mm.stl new file mode 100755 index 00000000..0978013a Binary files /dev/null and b/data/mesh/TriangleShape4mm.stl differ diff --git a/data/mesh/box_50mm_reverse_winding.obj b/data/mesh/box_50mm_reverse_winding.obj new file mode 100644 index 00000000..6b47afe4 --- /dev/null +++ b/data/mesh/box_50mm_reverse_winding.obj @@ -0,0 +1,1806 @@ +# Exported from Abaqus INP +# translation: 0.0, 0.0, 0.0 +v 0.000000000 0.000000000 50.000000000 +v 0.000000000 50.000000000 50.000000000 +v 0.000000000 50.000000000 0.000000000 +v 0.000000000 0.000000000 0.000000000 +v 50.000000000 50.000000000 50.000000000 +v 50.000000000 50.000000000 0.000000000 +v 50.000000000 0.000000000 50.000000000 +v 50.000000000 0.000000000 0.000000000 +v 0.000000000 5.000000000 50.000000000 +v 0.000000000 10.000000000 50.000000000 +v 0.000000000 15.000000000 50.000000000 +v 0.000000000 20.000000000 50.000000000 +v 0.000000000 25.000000000 50.000000000 +v 0.000000000 30.000000000 50.000000000 +v 0.000000000 35.000000000 50.000000000 +v 0.000000000 40.000000000 50.000000000 +v 0.000000000 45.000000000 50.000000000 +v 0.000000000 50.000000000 45.000000000 +v 0.000000000 50.000000000 40.000000000 +v 0.000000000 50.000000000 35.000000000 +v 0.000000000 50.000000000 30.000000000 +v 0.000000000 50.000000000 25.000000000 +v 0.000000000 50.000000000 20.000000000 +v 0.000000000 50.000000000 15.000000000 +v 0.000000000 50.000000000 10.000000000 +v 0.000000000 50.000000000 5.000000000 +v 0.000000000 45.000000000 0.000000000 +v 0.000000000 40.000000000 0.000000000 +v 0.000000000 35.000000000 0.000000000 +v 0.000000000 30.000000000 0.000000000 +v 0.000000000 25.000000000 0.000000000 +v 0.000000000 20.000000000 0.000000000 +v 0.000000000 15.000000000 0.000000000 +v 0.000000000 10.000000000 0.000000000 +v 0.000000000 5.000000000 0.000000000 +v 0.000000000 0.000000000 5.000000000 +v 0.000000000 0.000000000 10.000000000 +v 0.000000000 0.000000000 15.000000000 +v 0.000000000 0.000000000 20.000000000 +v 0.000000000 0.000000000 25.000000000 +v 0.000000000 0.000000000 30.000000000 +v 0.000000000 0.000000000 35.000000000 +v 0.000000000 0.000000000 40.000000000 +v 0.000000000 0.000000000 45.000000000 +v 5.000000000 50.000000000 50.000000000 +v 10.000000000 50.000000000 50.000000000 +v 15.000000000 50.000000000 50.000000000 +v 20.000000000 50.000000000 50.000000000 +v 25.000000000 50.000000000 50.000000000 +v 30.000000000 50.000000000 50.000000000 +v 35.000000000 50.000000000 50.000000000 +v 40.000000000 50.000000000 50.000000000 +v 45.000000000 50.000000000 50.000000000 +v 50.000000000 50.000000000 45.000000000 +v 50.000000000 50.000000000 40.000000000 +v 50.000000000 50.000000000 35.000000000 +v 50.000000000 50.000000000 30.000000000 +v 50.000000000 50.000000000 25.000000000 +v 50.000000000 50.000000000 20.000000000 +v 50.000000000 50.000000000 15.000000000 +v 50.000000000 50.000000000 10.000000000 +v 50.000000000 50.000000000 5.000000000 +v 45.000000000 50.000000000 0.000000000 +v 40.000000000 50.000000000 0.000000000 +v 35.000000000 50.000000000 0.000000000 +v 30.000000000 50.000000000 0.000000000 +v 25.000000000 50.000000000 0.000000000 +v 20.000000000 50.000000000 0.000000000 +v 15.000000000 50.000000000 0.000000000 +v 10.000000000 50.000000000 0.000000000 +v 5.000000000 50.000000000 0.000000000 +v 50.000000000 45.000000000 50.000000000 +v 50.000000000 40.000000000 50.000000000 +v 50.000000000 35.000000000 50.000000000 +v 50.000000000 30.000000000 50.000000000 +v 50.000000000 25.000000000 50.000000000 +v 50.000000000 20.000000000 50.000000000 +v 50.000000000 15.000000000 50.000000000 +v 50.000000000 10.000000000 50.000000000 +v 50.000000000 5.000000000 50.000000000 +v 50.000000000 0.000000000 45.000000000 +v 50.000000000 0.000000000 40.000000000 +v 50.000000000 0.000000000 35.000000000 +v 50.000000000 0.000000000 30.000000000 +v 50.000000000 0.000000000 25.000000000 +v 50.000000000 0.000000000 20.000000000 +v 50.000000000 0.000000000 15.000000000 +v 50.000000000 0.000000000 10.000000000 +v 50.000000000 0.000000000 5.000000000 +v 50.000000000 5.000000000 0.000000000 +v 50.000000000 10.000000000 0.000000000 +v 50.000000000 15.000000000 0.000000000 +v 50.000000000 20.000000000 0.000000000 +v 50.000000000 25.000000000 0.000000000 +v 50.000000000 30.000000000 0.000000000 +v 50.000000000 35.000000000 0.000000000 +v 50.000000000 40.000000000 0.000000000 +v 50.000000000 45.000000000 0.000000000 +v 45.000000000 0.000000000 50.000000000 +v 40.000000000 0.000000000 50.000000000 +v 35.000000000 0.000000000 50.000000000 +v 30.000000000 0.000000000 50.000000000 +v 25.000000000 0.000000000 50.000000000 +v 20.000000000 0.000000000 50.000000000 +v 15.000000000 0.000000000 50.000000000 +v 10.000000000 0.000000000 50.000000000 +v 5.000000000 0.000000000 50.000000000 +v 5.000000000 0.000000000 0.000000000 +v 10.000000000 0.000000000 0.000000000 +v 15.000000000 0.000000000 0.000000000 +v 20.000000000 0.000000000 0.000000000 +v 25.000000000 0.000000000 0.000000000 +v 30.000000000 0.000000000 0.000000000 +v 35.000000000 0.000000000 0.000000000 +v 40.000000000 0.000000000 0.000000000 +v 45.000000000 0.000000000 0.000000000 +v 0.000000000 5.000000000 45.000000000 +v 0.000000000 10.000000000 45.000000000 +v 0.000000000 15.000000000 45.000000000 +v 0.000000000 20.000000000 45.000000000 +v 0.000000000 25.000000000 45.000000000 +v 0.000000000 30.000000000 45.000000000 +v 0.000000000 35.000000000 45.000000000 +v 0.000000000 40.000000000 45.000000000 +v 0.000000000 45.000000000 45.000000000 +v 0.000000000 5.000000000 40.000000000 +v 0.000000000 10.000000000 40.000000000 +v 0.000000000 15.000000000 40.000000000 +v 0.000000000 20.000000000 40.000000000 +v 0.000000000 25.000000000 40.000000000 +v 0.000000000 30.000000000 40.000000000 +v 0.000000000 35.000000000 40.000000000 +v 0.000000000 40.000000000 40.000000000 +v 0.000000000 45.000000000 40.000000000 +v 0.000000000 5.000000000 35.000000000 +v 0.000000000 10.000000000 35.000000000 +v 0.000000000 15.000000000 35.000000000 +v 0.000000000 20.000000000 35.000000000 +v 0.000000000 25.000000000 35.000000000 +v 0.000000000 30.000000000 35.000000000 +v 0.000000000 35.000000000 35.000000000 +v 0.000000000 40.000000000 35.000000000 +v 0.000000000 45.000000000 35.000000000 +v 0.000000000 5.000000000 30.000000000 +v 0.000000000 10.000000000 30.000000000 +v 0.000000000 15.000000000 30.000000000 +v 0.000000000 20.000000000 30.000000000 +v 0.000000000 25.000000000 30.000000000 +v 0.000000000 30.000000000 30.000000000 +v 0.000000000 35.000000000 30.000000000 +v 0.000000000 40.000000000 30.000000000 +v 0.000000000 45.000000000 30.000000000 +v 0.000000000 5.000000000 25.000000000 +v 0.000000000 10.000000000 25.000000000 +v 0.000000000 15.000000000 25.000000000 +v 0.000000000 20.000000000 25.000000000 +v 0.000000000 25.000000000 25.000000000 +v 0.000000000 30.000000000 25.000000000 +v 0.000000000 35.000000000 25.000000000 +v 0.000000000 40.000000000 25.000000000 +v 0.000000000 45.000000000 25.000000000 +v 0.000000000 5.000000000 20.000000000 +v 0.000000000 10.000000000 20.000000000 +v 0.000000000 15.000000000 20.000000000 +v 0.000000000 20.000000000 20.000000000 +v 0.000000000 25.000000000 20.000000000 +v 0.000000000 30.000000000 20.000000000 +v 0.000000000 35.000000000 20.000000000 +v 0.000000000 40.000000000 20.000000000 +v 0.000000000 45.000000000 20.000000000 +v 0.000000000 5.000000000 15.000000000 +v 0.000000000 10.000000000 15.000000000 +v 0.000000000 15.000000000 15.000000000 +v 0.000000000 20.000000000 15.000000000 +v 0.000000000 25.000000000 15.000000000 +v 0.000000000 30.000000000 15.000000000 +v 0.000000000 35.000000000 15.000000000 +v 0.000000000 40.000000000 15.000000000 +v 0.000000000 45.000000000 15.000000000 +v 0.000000000 5.000000000 10.000000000 +v 0.000000000 10.000000000 10.000000000 +v 0.000000000 15.000000000 10.000000000 +v 0.000000000 20.000000000 10.000000000 +v 0.000000000 25.000000000 10.000000000 +v 0.000000000 30.000000000 10.000000000 +v 0.000000000 35.000000000 10.000000000 +v 0.000000000 40.000000000 10.000000000 +v 0.000000000 45.000000000 10.000000000 +v 0.000000000 5.000000000 5.000000000 +v 0.000000000 10.000000000 5.000000000 +v 0.000000000 15.000000000 5.000000000 +v 0.000000000 20.000000000 5.000000000 +v 0.000000000 25.000000000 5.000000000 +v 0.000000000 30.000000000 5.000000000 +v 0.000000000 35.000000000 5.000000000 +v 0.000000000 40.000000000 5.000000000 +v 0.000000000 45.000000000 5.000000000 +v 5.000000000 50.000000000 45.000000000 +v 10.000000000 50.000000000 45.000000000 +v 15.000000000 50.000000000 45.000000000 +v 20.000000000 50.000000000 45.000000000 +v 25.000000000 50.000000000 45.000000000 +v 30.000000000 50.000000000 45.000000000 +v 35.000000000 50.000000000 45.000000000 +v 40.000000000 50.000000000 45.000000000 +v 45.000000000 50.000000000 45.000000000 +v 5.000000000 50.000000000 40.000000000 +v 10.000000000 50.000000000 40.000000000 +v 15.000000000 50.000000000 40.000000000 +v 20.000000000 50.000000000 40.000000000 +v 25.000000000 50.000000000 40.000000000 +v 30.000000000 50.000000000 40.000000000 +v 35.000000000 50.000000000 40.000000000 +v 40.000000000 50.000000000 40.000000000 +v 45.000000000 50.000000000 40.000000000 +v 5.000000000 50.000000000 35.000000000 +v 10.000000000 50.000000000 35.000000000 +v 15.000000000 50.000000000 35.000000000 +v 20.000000000 50.000000000 35.000000000 +v 25.000000000 50.000000000 35.000000000 +v 30.000000000 50.000000000 35.000000000 +v 35.000000000 50.000000000 35.000000000 +v 40.000000000 50.000000000 35.000000000 +v 45.000000000 50.000000000 35.000000000 +v 5.000000000 50.000000000 30.000000000 +v 10.000000000 50.000000000 30.000000000 +v 15.000000000 50.000000000 30.000000000 +v 20.000000000 50.000000000 30.000000000 +v 25.000000000 50.000000000 30.000000000 +v 30.000000000 50.000000000 30.000000000 +v 35.000000000 50.000000000 30.000000000 +v 40.000000000 50.000000000 30.000000000 +v 45.000000000 50.000000000 30.000000000 +v 5.000000000 50.000000000 25.000000000 +v 10.000000000 50.000000000 25.000000000 +v 15.000000000 50.000000000 25.000000000 +v 20.000000000 50.000000000 25.000000000 +v 25.000000000 50.000000000 25.000000000 +v 30.000000000 50.000000000 25.000000000 +v 35.000000000 50.000000000 25.000000000 +v 40.000000000 50.000000000 25.000000000 +v 45.000000000 50.000000000 25.000000000 +v 5.000000000 50.000000000 20.000000000 +v 10.000000000 50.000000000 20.000000000 +v 15.000000000 50.000000000 20.000000000 +v 20.000000000 50.000000000 20.000000000 +v 25.000000000 50.000000000 20.000000000 +v 30.000000000 50.000000000 20.000000000 +v 35.000000000 50.000000000 20.000000000 +v 40.000000000 50.000000000 20.000000000 +v 45.000000000 50.000000000 20.000000000 +v 5.000000000 50.000000000 15.000000000 +v 10.000000000 50.000000000 15.000000000 +v 15.000000000 50.000000000 15.000000000 +v 20.000000000 50.000000000 15.000000000 +v 25.000000000 50.000000000 15.000000000 +v 30.000000000 50.000000000 15.000000000 +v 35.000000000 50.000000000 15.000000000 +v 40.000000000 50.000000000 15.000000000 +v 45.000000000 50.000000000 15.000000000 +v 5.000000000 50.000000000 10.000000000 +v 10.000000000 50.000000000 10.000000000 +v 15.000000000 50.000000000 10.000000000 +v 20.000000000 50.000000000 10.000000000 +v 25.000000000 50.000000000 10.000000000 +v 30.000000000 50.000000000 10.000000000 +v 35.000000000 50.000000000 10.000000000 +v 40.000000000 50.000000000 10.000000000 +v 45.000000000 50.000000000 10.000000000 +v 5.000000000 50.000000000 5.000000000 +v 10.000000000 50.000000000 5.000000000 +v 15.000000000 50.000000000 5.000000000 +v 20.000000000 50.000000000 5.000000000 +v 25.000000000 50.000000000 5.000000000 +v 30.000000000 50.000000000 5.000000000 +v 35.000000000 50.000000000 5.000000000 +v 40.000000000 50.000000000 5.000000000 +v 45.000000000 50.000000000 5.000000000 +v 50.000000000 45.000000000 45.000000000 +v 50.000000000 40.000000000 45.000000000 +v 50.000000000 35.000000000 45.000000000 +v 50.000000000 30.000000000 45.000000000 +v 50.000000000 25.000000000 45.000000000 +v 50.000000000 20.000000000 45.000000000 +v 50.000000000 15.000000000 45.000000000 +v 50.000000000 10.000000000 45.000000000 +v 50.000000000 5.000000000 45.000000000 +v 50.000000000 45.000000000 40.000000000 +v 50.000000000 40.000000000 40.000000000 +v 50.000000000 35.000000000 40.000000000 +v 50.000000000 30.000000000 40.000000000 +v 50.000000000 25.000000000 40.000000000 +v 50.000000000 20.000000000 40.000000000 +v 50.000000000 15.000000000 40.000000000 +v 50.000000000 10.000000000 40.000000000 +v 50.000000000 5.000000000 40.000000000 +v 50.000000000 45.000000000 35.000000000 +v 50.000000000 40.000000000 35.000000000 +v 50.000000000 35.000000000 35.000000000 +v 50.000000000 30.000000000 35.000000000 +v 50.000000000 25.000000000 35.000000000 +v 50.000000000 20.000000000 35.000000000 +v 50.000000000 15.000000000 35.000000000 +v 50.000000000 10.000000000 35.000000000 +v 50.000000000 5.000000000 35.000000000 +v 50.000000000 45.000000000 30.000000000 +v 50.000000000 40.000000000 30.000000000 +v 50.000000000 35.000000000 30.000000000 +v 50.000000000 30.000000000 30.000000000 +v 50.000000000 25.000000000 30.000000000 +v 50.000000000 20.000000000 30.000000000 +v 50.000000000 15.000000000 30.000000000 +v 50.000000000 10.000000000 30.000000000 +v 50.000000000 5.000000000 30.000000000 +v 50.000000000 45.000000000 25.000000000 +v 50.000000000 40.000000000 25.000000000 +v 50.000000000 35.000000000 25.000000000 +v 50.000000000 30.000000000 25.000000000 +v 50.000000000 25.000000000 25.000000000 +v 50.000000000 20.000000000 25.000000000 +v 50.000000000 15.000000000 25.000000000 +v 50.000000000 10.000000000 25.000000000 +v 50.000000000 5.000000000 25.000000000 +v 50.000000000 45.000000000 20.000000000 +v 50.000000000 40.000000000 20.000000000 +v 50.000000000 35.000000000 20.000000000 +v 50.000000000 30.000000000 20.000000000 +v 50.000000000 25.000000000 20.000000000 +v 50.000000000 20.000000000 20.000000000 +v 50.000000000 15.000000000 20.000000000 +v 50.000000000 10.000000000 20.000000000 +v 50.000000000 5.000000000 20.000000000 +v 50.000000000 45.000000000 15.000000000 +v 50.000000000 40.000000000 15.000000000 +v 50.000000000 35.000000000 15.000000000 +v 50.000000000 30.000000000 15.000000000 +v 50.000000000 25.000000000 15.000000000 +v 50.000000000 20.000000000 15.000000000 +v 50.000000000 15.000000000 15.000000000 +v 50.000000000 10.000000000 15.000000000 +v 50.000000000 5.000000000 15.000000000 +v 50.000000000 45.000000000 10.000000000 +v 50.000000000 40.000000000 10.000000000 +v 50.000000000 35.000000000 10.000000000 +v 50.000000000 30.000000000 10.000000000 +v 50.000000000 25.000000000 10.000000000 +v 50.000000000 20.000000000 10.000000000 +v 50.000000000 15.000000000 10.000000000 +v 50.000000000 10.000000000 10.000000000 +v 50.000000000 5.000000000 10.000000000 +v 50.000000000 45.000000000 5.000000000 +v 50.000000000 40.000000000 5.000000000 +v 50.000000000 35.000000000 5.000000000 +v 50.000000000 30.000000000 5.000000000 +v 50.000000000 25.000000000 5.000000000 +v 50.000000000 20.000000000 5.000000000 +v 50.000000000 15.000000000 5.000000000 +v 50.000000000 10.000000000 5.000000000 +v 50.000000000 5.000000000 5.000000000 +v 45.000000000 0.000000000 45.000000000 +v 40.000000000 0.000000000 45.000000000 +v 35.000000000 0.000000000 45.000000000 +v 30.000000000 0.000000000 45.000000000 +v 25.000000000 0.000000000 45.000000000 +v 20.000000000 0.000000000 45.000000000 +v 15.000000000 0.000000000 45.000000000 +v 10.000000000 0.000000000 45.000000000 +v 5.000000000 0.000000000 45.000000000 +v 45.000000000 0.000000000 40.000000000 +v 40.000000000 0.000000000 40.000000000 +v 35.000000000 0.000000000 40.000000000 +v 30.000000000 0.000000000 40.000000000 +v 25.000000000 0.000000000 40.000000000 +v 20.000000000 0.000000000 40.000000000 +v 15.000000000 0.000000000 40.000000000 +v 10.000000000 0.000000000 40.000000000 +v 5.000000000 0.000000000 40.000000000 +v 45.000000000 0.000000000 35.000000000 +v 40.000000000 0.000000000 35.000000000 +v 35.000000000 0.000000000 35.000000000 +v 30.000000000 0.000000000 35.000000000 +v 25.000000000 0.000000000 35.000000000 +v 20.000000000 0.000000000 35.000000000 +v 15.000000000 0.000000000 35.000000000 +v 10.000000000 0.000000000 35.000000000 +v 5.000000000 0.000000000 35.000000000 +v 45.000000000 0.000000000 30.000000000 +v 40.000000000 0.000000000 30.000000000 +v 35.000000000 0.000000000 30.000000000 +v 30.000000000 0.000000000 30.000000000 +v 25.000000000 0.000000000 30.000000000 +v 20.000000000 0.000000000 30.000000000 +v 15.000000000 0.000000000 30.000000000 +v 10.000000000 0.000000000 30.000000000 +v 5.000000000 0.000000000 30.000000000 +v 45.000000000 0.000000000 25.000000000 +v 40.000000000 0.000000000 25.000000000 +v 35.000000000 0.000000000 25.000000000 +v 30.000000000 0.000000000 25.000000000 +v 25.000000000 0.000000000 25.000000000 +v 20.000000000 0.000000000 25.000000000 +v 15.000000000 0.000000000 25.000000000 +v 10.000000000 0.000000000 25.000000000 +v 5.000000000 0.000000000 25.000000000 +v 45.000000000 0.000000000 20.000000000 +v 40.000000000 0.000000000 20.000000000 +v 35.000000000 0.000000000 20.000000000 +v 30.000000000 0.000000000 20.000000000 +v 25.000000000 0.000000000 20.000000000 +v 20.000000000 0.000000000 20.000000000 +v 15.000000000 0.000000000 20.000000000 +v 10.000000000 0.000000000 20.000000000 +v 5.000000000 0.000000000 20.000000000 +v 45.000000000 0.000000000 15.000000000 +v 40.000000000 0.000000000 15.000000000 +v 35.000000000 0.000000000 15.000000000 +v 30.000000000 0.000000000 15.000000000 +v 25.000000000 0.000000000 15.000000000 +v 20.000000000 0.000000000 15.000000000 +v 15.000000000 0.000000000 15.000000000 +v 10.000000000 0.000000000 15.000000000 +v 5.000000000 0.000000000 15.000000000 +v 45.000000000 0.000000000 10.000000000 +v 40.000000000 0.000000000 10.000000000 +v 35.000000000 0.000000000 10.000000000 +v 30.000000000 0.000000000 10.000000000 +v 25.000000000 0.000000000 10.000000000 +v 20.000000000 0.000000000 10.000000000 +v 15.000000000 0.000000000 10.000000000 +v 10.000000000 0.000000000 10.000000000 +v 5.000000000 0.000000000 10.000000000 +v 45.000000000 0.000000000 5.000000000 +v 40.000000000 0.000000000 5.000000000 +v 35.000000000 0.000000000 5.000000000 +v 30.000000000 0.000000000 5.000000000 +v 25.000000000 0.000000000 5.000000000 +v 20.000000000 0.000000000 5.000000000 +v 15.000000000 0.000000000 5.000000000 +v 10.000000000 0.000000000 5.000000000 +v 5.000000000 0.000000000 5.000000000 +v 5.000000000 45.000000000 50.000000000 +v 5.000000000 40.000000000 50.000000000 +v 5.000000000 35.000000000 50.000000000 +v 5.000000000 30.000000000 50.000000000 +v 5.000000000 25.000000000 50.000000000 +v 5.000000000 20.000000000 50.000000000 +v 5.000000000 15.000000000 50.000000000 +v 5.000000000 10.000000000 50.000000000 +v 5.000000000 5.000000000 50.000000000 +v 10.000000000 45.000000000 50.000000000 +v 10.000000000 40.000000000 50.000000000 +v 10.000000000 35.000000000 50.000000000 +v 10.000000000 30.000000000 50.000000000 +v 10.000000000 25.000000000 50.000000000 +v 10.000000000 20.000000000 50.000000000 +v 10.000000000 15.000000000 50.000000000 +v 10.000000000 10.000000000 50.000000000 +v 10.000000000 5.000000000 50.000000000 +v 15.000000000 45.000000000 50.000000000 +v 15.000000000 40.000000000 50.000000000 +v 15.000000000 35.000000000 50.000000000 +v 15.000000000 30.000000000 50.000000000 +v 15.000000000 25.000000000 50.000000000 +v 15.000000000 20.000000000 50.000000000 +v 15.000000000 15.000000000 50.000000000 +v 15.000000000 10.000000000 50.000000000 +v 15.000000000 5.000000000 50.000000000 +v 20.000000000 45.000000000 50.000000000 +v 20.000000000 40.000000000 50.000000000 +v 20.000000000 35.000000000 50.000000000 +v 20.000000000 30.000000000 50.000000000 +v 20.000000000 25.000000000 50.000000000 +v 20.000000000 20.000000000 50.000000000 +v 20.000000000 15.000000000 50.000000000 +v 20.000000000 10.000000000 50.000000000 +v 20.000000000 5.000000000 50.000000000 +v 25.000000000 45.000000000 50.000000000 +v 25.000000000 40.000000000 50.000000000 +v 25.000000000 35.000000000 50.000000000 +v 25.000000000 30.000000000 50.000000000 +v 25.000000000 25.000000000 50.000000000 +v 25.000000000 20.000000000 50.000000000 +v 25.000000000 15.000000000 50.000000000 +v 25.000000000 10.000000000 50.000000000 +v 25.000000000 5.000000000 50.000000000 +v 30.000000000 45.000000000 50.000000000 +v 30.000000000 40.000000000 50.000000000 +v 30.000000000 35.000000000 50.000000000 +v 30.000000000 30.000000000 50.000000000 +v 30.000000000 25.000000000 50.000000000 +v 30.000000000 20.000000000 50.000000000 +v 30.000000000 15.000000000 50.000000000 +v 30.000000000 10.000000000 50.000000000 +v 30.000000000 5.000000000 50.000000000 +v 35.000000000 45.000000000 50.000000000 +v 35.000000000 40.000000000 50.000000000 +v 35.000000000 35.000000000 50.000000000 +v 35.000000000 30.000000000 50.000000000 +v 35.000000000 25.000000000 50.000000000 +v 35.000000000 20.000000000 50.000000000 +v 35.000000000 15.000000000 50.000000000 +v 35.000000000 10.000000000 50.000000000 +v 35.000000000 5.000000000 50.000000000 +v 40.000000000 45.000000000 50.000000000 +v 40.000000000 40.000000000 50.000000000 +v 40.000000000 35.000000000 50.000000000 +v 40.000000000 30.000000000 50.000000000 +v 40.000000000 25.000000000 50.000000000 +v 40.000000000 20.000000000 50.000000000 +v 40.000000000 15.000000000 50.000000000 +v 40.000000000 10.000000000 50.000000000 +v 40.000000000 5.000000000 50.000000000 +v 45.000000000 45.000000000 50.000000000 +v 45.000000000 40.000000000 50.000000000 +v 45.000000000 35.000000000 50.000000000 +v 45.000000000 30.000000000 50.000000000 +v 45.000000000 25.000000000 50.000000000 +v 45.000000000 20.000000000 50.000000000 +v 45.000000000 15.000000000 50.000000000 +v 45.000000000 10.000000000 50.000000000 +v 45.000000000 5.000000000 50.000000000 +v 45.000000000 5.000000000 0.000000000 +v 40.000000000 5.000000000 0.000000000 +v 35.000000000 5.000000000 0.000000000 +v 30.000000000 5.000000000 0.000000000 +v 25.000000000 5.000000000 0.000000000 +v 20.000000000 5.000000000 0.000000000 +v 15.000000000 5.000000000 0.000000000 +v 10.000000000 5.000000000 0.000000000 +v 5.000000000 5.000000000 0.000000000 +v 45.000000000 10.000000000 0.000000000 +v 40.000000000 10.000000000 0.000000000 +v 35.000000000 10.000000000 0.000000000 +v 30.000000000 10.000000000 0.000000000 +v 25.000000000 10.000000000 0.000000000 +v 20.000000000 10.000000000 0.000000000 +v 15.000000000 10.000000000 0.000000000 +v 10.000000000 10.000000000 0.000000000 +v 5.000000000 10.000000000 0.000000000 +v 45.000000000 15.000000000 0.000000000 +v 40.000000000 15.000000000 0.000000000 +v 35.000000000 15.000000000 0.000000000 +v 30.000000000 15.000000000 0.000000000 +v 25.000000000 15.000000000 0.000000000 +v 20.000000000 15.000000000 0.000000000 +v 15.000000000 15.000000000 0.000000000 +v 10.000000000 15.000000000 0.000000000 +v 5.000000000 15.000000000 0.000000000 +v 45.000000000 20.000000000 0.000000000 +v 40.000000000 20.000000000 0.000000000 +v 35.000000000 20.000000000 0.000000000 +v 30.000000000 20.000000000 0.000000000 +v 25.000000000 20.000000000 0.000000000 +v 20.000000000 20.000000000 0.000000000 +v 15.000000000 20.000000000 0.000000000 +v 10.000000000 20.000000000 0.000000000 +v 5.000000000 20.000000000 0.000000000 +v 45.000000000 25.000000000 0.000000000 +v 40.000000000 25.000000000 0.000000000 +v 35.000000000 25.000000000 0.000000000 +v 30.000000000 25.000000000 0.000000000 +v 25.000000000 25.000000000 0.000000000 +v 20.000000000 25.000000000 0.000000000 +v 15.000000000 25.000000000 0.000000000 +v 10.000000000 25.000000000 0.000000000 +v 5.000000000 25.000000000 0.000000000 +v 45.000000000 30.000000000 0.000000000 +v 40.000000000 30.000000000 0.000000000 +v 35.000000000 30.000000000 0.000000000 +v 30.000000000 30.000000000 0.000000000 +v 25.000000000 30.000000000 0.000000000 +v 20.000000000 30.000000000 0.000000000 +v 15.000000000 30.000000000 0.000000000 +v 10.000000000 30.000000000 0.000000000 +v 5.000000000 30.000000000 0.000000000 +v 45.000000000 35.000000000 0.000000000 +v 40.000000000 35.000000000 0.000000000 +v 35.000000000 35.000000000 0.000000000 +v 30.000000000 35.000000000 0.000000000 +v 25.000000000 35.000000000 0.000000000 +v 20.000000000 35.000000000 0.000000000 +v 15.000000000 35.000000000 0.000000000 +v 10.000000000 35.000000000 0.000000000 +v 5.000000000 35.000000000 0.000000000 +v 45.000000000 40.000000000 0.000000000 +v 40.000000000 40.000000000 0.000000000 +v 35.000000000 40.000000000 0.000000000 +v 30.000000000 40.000000000 0.000000000 +v 25.000000000 40.000000000 0.000000000 +v 20.000000000 40.000000000 0.000000000 +v 15.000000000 40.000000000 0.000000000 +v 10.000000000 40.000000000 0.000000000 +v 5.000000000 40.000000000 0.000000000 +v 45.000000000 45.000000000 0.000000000 +v 40.000000000 45.000000000 0.000000000 +v 35.000000000 45.000000000 0.000000000 +v 30.000000000 45.000000000 0.000000000 +v 25.000000000 45.000000000 0.000000000 +v 20.000000000 45.000000000 0.000000000 +v 15.000000000 45.000000000 0.000000000 +v 10.000000000 45.000000000 0.000000000 +v 5.000000000 45.000000000 0.000000000 +v 0.000000000 0.000000000 0.000000000 +f 1 117 9 +f 117 1 44 +f 9 118 10 +f 118 9 117 +f 10 119 11 +f 119 10 118 +f 11 120 12 +f 120 11 119 +f 12 121 13 +f 121 12 120 +f 13 122 14 +f 122 13 121 +f 14 123 15 +f 123 14 122 +f 15 124 16 +f 124 15 123 +f 16 125 17 +f 125 16 124 +f 17 18 2 +f 18 17 125 +f 44 126 117 +f 126 44 43 +f 117 127 118 +f 127 117 126 +f 118 128 119 +f 128 118 127 +f 119 129 120 +f 129 119 128 +f 120 130 121 +f 130 120 129 +f 121 131 122 +f 131 121 130 +f 122 132 123 +f 132 122 131 +f 123 133 124 +f 133 123 132 +f 124 134 125 +f 134 124 133 +f 125 19 18 +f 19 125 134 +f 43 135 126 +f 135 43 42 +f 126 136 127 +f 136 126 135 +f 127 137 128 +f 137 127 136 +f 128 138 129 +f 138 128 137 +f 129 139 130 +f 139 129 138 +f 130 140 131 +f 140 130 139 +f 131 141 132 +f 141 131 140 +f 132 142 133 +f 142 132 141 +f 133 143 134 +f 143 133 142 +f 134 20 19 +f 20 134 143 +f 42 144 135 +f 144 42 41 +f 135 145 136 +f 145 135 144 +f 136 146 137 +f 146 136 145 +f 137 147 138 +f 147 137 146 +f 138 148 139 +f 148 138 147 +f 139 149 140 +f 149 139 148 +f 140 150 141 +f 150 140 149 +f 141 151 142 +f 151 141 150 +f 142 152 143 +f 152 142 151 +f 143 21 20 +f 21 143 152 +f 41 153 144 +f 153 41 40 +f 144 154 145 +f 154 144 153 +f 145 155 146 +f 155 145 154 +f 146 156 147 +f 156 146 155 +f 147 157 148 +f 157 147 156 +f 148 158 149 +f 158 148 157 +f 149 159 150 +f 159 149 158 +f 150 160 151 +f 160 150 159 +f 151 161 152 +f 161 151 160 +f 152 22 21 +f 22 152 161 +f 40 162 153 +f 162 40 39 +f 153 163 154 +f 163 153 162 +f 154 164 155 +f 164 154 163 +f 155 165 156 +f 165 155 164 +f 156 166 157 +f 166 156 165 +f 157 167 158 +f 167 157 166 +f 158 168 159 +f 168 158 167 +f 159 169 160 +f 169 159 168 +f 160 170 161 +f 170 160 169 +f 161 23 22 +f 23 161 170 +f 39 171 162 +f 171 39 38 +f 162 172 163 +f 172 162 171 +f 163 173 164 +f 173 163 172 +f 164 174 165 +f 174 164 173 +f 165 175 166 +f 175 165 174 +f 166 176 167 +f 176 166 175 +f 167 177 168 +f 177 167 176 +f 168 178 169 +f 178 168 177 +f 169 179 170 +f 179 169 178 +f 170 24 23 +f 24 170 179 +f 38 180 171 +f 180 38 37 +f 171 181 172 +f 181 171 180 +f 172 182 173 +f 182 172 181 +f 173 183 174 +f 183 173 182 +f 174 184 175 +f 184 174 183 +f 175 185 176 +f 185 175 184 +f 176 186 177 +f 186 176 185 +f 177 187 178 +f 187 177 186 +f 178 188 179 +f 188 178 187 +f 179 25 24 +f 25 179 188 +f 37 189 180 +f 189 37 36 +f 180 190 181 +f 190 180 189 +f 181 191 182 +f 191 181 190 +f 182 192 183 +f 192 182 191 +f 183 193 184 +f 193 183 192 +f 184 194 185 +f 194 184 193 +f 185 195 186 +f 195 185 194 +f 186 196 187 +f 196 186 195 +f 187 197 188 +f 197 187 196 +f 188 26 25 +f 26 188 197 +f 36 35 189 +f 35 36 4 +f 189 34 190 +f 34 189 35 +f 190 33 191 +f 33 190 34 +f 191 32 192 +f 32 191 33 +f 192 31 193 +f 31 192 32 +f 193 30 194 +f 30 193 31 +f 194 29 195 +f 29 194 30 +f 195 28 196 +f 28 195 29 +f 196 27 197 +f 27 196 28 +f 197 3 26 +f 3 197 27 +f 2 198 45 +f 198 2 18 +f 45 199 46 +f 199 45 198 +f 46 200 47 +f 200 46 199 +f 47 201 48 +f 201 47 200 +f 48 202 49 +f 202 48 201 +f 49 203 50 +f 203 49 202 +f 50 204 51 +f 204 50 203 +f 51 205 52 +f 205 51 204 +f 52 206 53 +f 206 52 205 +f 53 54 5 +f 54 53 206 +f 18 207 198 +f 207 18 19 +f 198 208 199 +f 208 198 207 +f 199 209 200 +f 209 199 208 +f 200 210 201 +f 210 200 209 +f 201 211 202 +f 211 201 210 +f 202 212 203 +f 212 202 211 +f 203 213 204 +f 213 203 212 +f 204 214 205 +f 214 204 213 +f 205 215 206 +f 215 205 214 +f 206 55 54 +f 55 206 215 +f 19 216 207 +f 216 19 20 +f 207 217 208 +f 217 207 216 +f 208 218 209 +f 218 208 217 +f 209 219 210 +f 219 209 218 +f 210 220 211 +f 220 210 219 +f 211 221 212 +f 221 211 220 +f 212 222 213 +f 222 212 221 +f 213 223 214 +f 223 213 222 +f 214 224 215 +f 224 214 223 +f 215 56 55 +f 56 215 224 +f 20 225 216 +f 225 20 21 +f 216 226 217 +f 226 216 225 +f 217 227 218 +f 227 217 226 +f 218 228 219 +f 228 218 227 +f 219 229 220 +f 229 219 228 +f 220 230 221 +f 230 220 229 +f 221 231 222 +f 231 221 230 +f 222 232 223 +f 232 222 231 +f 223 233 224 +f 233 223 232 +f 224 57 56 +f 57 224 233 +f 21 234 225 +f 234 21 22 +f 225 235 226 +f 235 225 234 +f 226 236 227 +f 236 226 235 +f 227 237 228 +f 237 227 236 +f 228 238 229 +f 238 228 237 +f 229 239 230 +f 239 229 238 +f 230 240 231 +f 240 230 239 +f 231 241 232 +f 241 231 240 +f 232 242 233 +f 242 232 241 +f 233 58 57 +f 58 233 242 +f 22 243 234 +f 243 22 23 +f 234 244 235 +f 244 234 243 +f 235 245 236 +f 245 235 244 +f 236 246 237 +f 246 236 245 +f 237 247 238 +f 247 237 246 +f 238 248 239 +f 248 238 247 +f 239 249 240 +f 249 239 248 +f 240 250 241 +f 250 240 249 +f 241 251 242 +f 251 241 250 +f 242 59 58 +f 59 242 251 +f 23 252 243 +f 252 23 24 +f 243 253 244 +f 253 243 252 +f 244 254 245 +f 254 244 253 +f 245 255 246 +f 255 245 254 +f 246 256 247 +f 256 246 255 +f 247 257 248 +f 257 247 256 +f 248 258 249 +f 258 248 257 +f 249 259 250 +f 259 249 258 +f 250 260 251 +f 260 250 259 +f 251 60 59 +f 60 251 260 +f 24 261 252 +f 261 24 25 +f 252 262 253 +f 262 252 261 +f 253 263 254 +f 263 253 262 +f 254 264 255 +f 264 254 263 +f 255 265 256 +f 265 255 264 +f 256 266 257 +f 266 256 265 +f 257 267 258 +f 267 257 266 +f 258 268 259 +f 268 258 267 +f 259 269 260 +f 269 259 268 +f 260 61 60 +f 61 260 269 +f 25 270 261 +f 270 25 26 +f 261 271 262 +f 271 261 270 +f 262 272 263 +f 272 262 271 +f 263 273 264 +f 273 263 272 +f 264 274 265 +f 274 264 273 +f 265 275 266 +f 275 265 274 +f 266 276 267 +f 276 266 275 +f 267 277 268 +f 277 267 276 +f 268 278 269 +f 278 268 277 +f 269 62 61 +f 62 269 278 +f 26 71 270 +f 71 26 3 +f 270 70 271 +f 70 270 71 +f 271 69 272 +f 69 271 70 +f 272 68 273 +f 68 272 69 +f 273 67 274 +f 67 273 68 +f 274 66 275 +f 66 274 67 +f 275 65 276 +f 65 275 66 +f 276 64 277 +f 64 276 65 +f 277 63 278 +f 63 277 64 +f 278 6 62 +f 6 278 63 +f 5 279 72 +f 279 5 54 +f 72 280 73 +f 280 72 279 +f 73 281 74 +f 281 73 280 +f 74 282 75 +f 282 74 281 +f 75 283 76 +f 283 75 282 +f 76 284 77 +f 284 76 283 +f 77 285 78 +f 285 77 284 +f 78 286 79 +f 286 78 285 +f 79 287 80 +f 287 79 286 +f 80 81 7 +f 81 80 287 +f 54 288 279 +f 288 54 55 +f 279 289 280 +f 289 279 288 +f 280 290 281 +f 290 280 289 +f 281 291 282 +f 291 281 290 +f 282 292 283 +f 292 282 291 +f 283 293 284 +f 293 283 292 +f 284 294 285 +f 294 284 293 +f 285 295 286 +f 295 285 294 +f 286 296 287 +f 296 286 295 +f 287 82 81 +f 82 287 296 +f 55 297 288 +f 297 55 56 +f 288 298 289 +f 298 288 297 +f 289 299 290 +f 299 289 298 +f 290 300 291 +f 300 290 299 +f 291 301 292 +f 301 291 300 +f 292 302 293 +f 302 292 301 +f 293 303 294 +f 303 293 302 +f 294 304 295 +f 304 294 303 +f 295 305 296 +f 305 295 304 +f 296 83 82 +f 83 296 305 +f 56 306 297 +f 306 56 57 +f 297 307 298 +f 307 297 306 +f 298 308 299 +f 308 298 307 +f 299 309 300 +f 309 299 308 +f 300 310 301 +f 310 300 309 +f 301 311 302 +f 311 301 310 +f 302 312 303 +f 312 302 311 +f 303 313 304 +f 313 303 312 +f 304 314 305 +f 314 304 313 +f 305 84 83 +f 84 305 314 +f 57 315 306 +f 315 57 58 +f 306 316 307 +f 316 306 315 +f 307 317 308 +f 317 307 316 +f 308 318 309 +f 318 308 317 +f 309 319 310 +f 319 309 318 +f 310 320 311 +f 320 310 319 +f 311 321 312 +f 321 311 320 +f 312 322 313 +f 322 312 321 +f 313 323 314 +f 323 313 322 +f 314 85 84 +f 85 314 323 +f 58 324 315 +f 324 58 59 +f 315 325 316 +f 325 315 324 +f 316 326 317 +f 326 316 325 +f 317 327 318 +f 327 317 326 +f 318 328 319 +f 328 318 327 +f 319 329 320 +f 329 319 328 +f 320 330 321 +f 330 320 329 +f 321 331 322 +f 331 321 330 +f 322 332 323 +f 332 322 331 +f 323 86 85 +f 86 323 332 +f 59 333 324 +f 333 59 60 +f 324 334 325 +f 334 324 333 +f 325 335 326 +f 335 325 334 +f 326 336 327 +f 336 326 335 +f 327 337 328 +f 337 327 336 +f 328 338 329 +f 338 328 337 +f 329 339 330 +f 339 329 338 +f 330 340 331 +f 340 330 339 +f 331 341 332 +f 341 331 340 +f 332 87 86 +f 87 332 341 +f 60 342 333 +f 342 60 61 +f 333 343 334 +f 343 333 342 +f 334 344 335 +f 344 334 343 +f 335 345 336 +f 345 335 344 +f 336 346 337 +f 346 336 345 +f 337 347 338 +f 347 337 346 +f 338 348 339 +f 348 338 347 +f 339 349 340 +f 349 339 348 +f 340 350 341 +f 350 340 349 +f 341 88 87 +f 88 341 350 +f 61 351 342 +f 351 61 62 +f 342 352 343 +f 352 342 351 +f 343 353 344 +f 353 343 352 +f 344 354 345 +f 354 344 353 +f 345 355 346 +f 355 345 354 +f 346 356 347 +f 356 346 355 +f 347 357 348 +f 357 347 356 +f 348 358 349 +f 358 348 357 +f 349 359 350 +f 359 349 358 +f 350 89 88 +f 89 350 359 +f 62 98 351 +f 98 62 6 +f 351 97 352 +f 97 351 98 +f 352 96 353 +f 96 352 97 +f 353 95 354 +f 95 353 96 +f 354 94 355 +f 94 354 95 +f 355 93 356 +f 93 355 94 +f 356 92 357 +f 92 356 93 +f 357 91 358 +f 91 357 92 +f 358 90 359 +f 90 358 91 +f 359 8 89 +f 8 359 90 +f 7 360 99 +f 360 7 81 +f 99 361 100 +f 361 99 360 +f 100 362 101 +f 362 100 361 +f 101 363 102 +f 363 101 362 +f 102 364 103 +f 364 102 363 +f 103 365 104 +f 365 103 364 +f 104 366 105 +f 366 104 365 +f 105 367 106 +f 367 105 366 +f 106 368 107 +f 368 106 367 +f 107 44 1 +f 44 107 368 +f 81 369 360 +f 369 81 82 +f 360 370 361 +f 370 360 369 +f 361 371 362 +f 371 361 370 +f 362 372 363 +f 372 362 371 +f 363 373 364 +f 373 363 372 +f 364 374 365 +f 374 364 373 +f 365 375 366 +f 375 365 374 +f 366 376 367 +f 376 366 375 +f 367 377 368 +f 377 367 376 +f 368 43 44 +f 43 368 377 +f 82 378 369 +f 378 82 83 +f 369 379 370 +f 379 369 378 +f 370 380 371 +f 380 370 379 +f 371 381 372 +f 381 371 380 +f 372 382 373 +f 382 372 381 +f 373 383 374 +f 383 373 382 +f 374 384 375 +f 384 374 383 +f 375 385 376 +f 385 375 384 +f 376 386 377 +f 386 376 385 +f 377 42 43 +f 42 377 386 +f 83 387 378 +f 387 83 84 +f 378 388 379 +f 388 378 387 +f 379 389 380 +f 389 379 388 +f 380 390 381 +f 390 380 389 +f 381 391 382 +f 391 381 390 +f 382 392 383 +f 392 382 391 +f 383 393 384 +f 393 383 392 +f 384 394 385 +f 394 384 393 +f 385 395 386 +f 395 385 394 +f 386 41 42 +f 41 386 395 +f 84 396 387 +f 396 84 85 +f 387 397 388 +f 397 387 396 +f 388 398 389 +f 398 388 397 +f 389 399 390 +f 399 389 398 +f 390 400 391 +f 400 390 399 +f 391 401 392 +f 401 391 400 +f 392 402 393 +f 402 392 401 +f 393 403 394 +f 403 393 402 +f 394 404 395 +f 404 394 403 +f 395 40 41 +f 40 395 404 +f 85 405 396 +f 405 85 86 +f 396 406 397 +f 406 396 405 +f 397 407 398 +f 407 397 406 +f 398 408 399 +f 408 398 407 +f 399 409 400 +f 409 399 408 +f 400 410 401 +f 410 400 409 +f 401 411 402 +f 411 401 410 +f 402 412 403 +f 412 402 411 +f 403 413 404 +f 413 403 412 +f 404 39 40 +f 39 404 413 +f 86 414 405 +f 414 86 87 +f 405 415 406 +f 415 405 414 +f 406 416 407 +f 416 406 415 +f 407 417 408 +f 417 407 416 +f 408 418 409 +f 418 408 417 +f 409 419 410 +f 419 409 418 +f 410 420 411 +f 420 410 419 +f 411 421 412 +f 421 411 420 +f 412 422 413 +f 422 412 421 +f 413 38 39 +f 38 413 422 +f 87 423 414 +f 423 87 88 +f 414 424 415 +f 424 414 423 +f 415 425 416 +f 425 415 424 +f 416 426 417 +f 426 416 425 +f 417 427 418 +f 427 417 426 +f 418 428 419 +f 428 418 427 +f 419 429 420 +f 429 419 428 +f 420 430 421 +f 430 420 429 +f 421 431 422 +f 431 421 430 +f 422 37 38 +f 37 422 431 +f 88 432 423 +f 432 88 89 +f 423 433 424 +f 433 423 432 +f 424 434 425 +f 434 424 433 +f 425 435 426 +f 435 425 434 +f 426 436 427 +f 436 426 435 +f 427 437 428 +f 437 427 436 +f 428 438 429 +f 438 428 437 +f 429 439 430 +f 439 429 438 +f 430 440 431 +f 440 430 439 +f 431 36 37 +f 36 431 440 +f 89 116 432 +f 116 89 8 +f 432 115 433 +f 115 432 116 +f 433 114 434 +f 114 433 115 +f 434 113 435 +f 113 434 114 +f 435 112 436 +f 112 435 113 +f 436 111 437 +f 111 436 112 +f 437 110 438 +f 110 437 111 +f 438 109 439 +f 109 438 110 +f 439 108 440 +f 108 439 109 +f 440 4 36 +f 4 440 108 +f 2 441 17 +f 441 2 45 +f 17 442 16 +f 442 17 441 +f 16 443 15 +f 443 16 442 +f 15 444 14 +f 444 15 443 +f 14 445 13 +f 445 14 444 +f 13 446 12 +f 446 13 445 +f 12 447 11 +f 447 12 446 +f 11 448 10 +f 448 11 447 +f 10 449 9 +f 449 10 448 +f 9 107 1 +f 107 9 449 +f 45 450 441 +f 450 45 46 +f 441 451 442 +f 451 441 450 +f 442 452 443 +f 452 442 451 +f 443 453 444 +f 453 443 452 +f 444 454 445 +f 454 444 453 +f 445 455 446 +f 455 445 454 +f 446 456 447 +f 456 446 455 +f 447 457 448 +f 457 447 456 +f 448 458 449 +f 458 448 457 +f 449 106 107 +f 106 449 458 +f 46 459 450 +f 459 46 47 +f 450 460 451 +f 460 450 459 +f 451 461 452 +f 461 451 460 +f 452 462 453 +f 462 452 461 +f 453 463 454 +f 463 453 462 +f 454 464 455 +f 464 454 463 +f 455 465 456 +f 465 455 464 +f 456 466 457 +f 466 456 465 +f 457 467 458 +f 467 457 466 +f 458 105 106 +f 105 458 467 +f 47 468 459 +f 468 47 48 +f 459 469 460 +f 469 459 468 +f 460 470 461 +f 470 460 469 +f 461 471 462 +f 471 461 470 +f 462 472 463 +f 472 462 471 +f 463 473 464 +f 473 463 472 +f 464 474 465 +f 474 464 473 +f 465 475 466 +f 475 465 474 +f 466 476 467 +f 476 466 475 +f 467 104 105 +f 104 467 476 +f 48 477 468 +f 477 48 49 +f 468 478 469 +f 478 468 477 +f 469 479 470 +f 479 469 478 +f 470 480 471 +f 480 470 479 +f 471 481 472 +f 481 471 480 +f 472 482 473 +f 482 472 481 +f 473 483 474 +f 483 473 482 +f 474 484 475 +f 484 474 483 +f 475 485 476 +f 485 475 484 +f 476 103 104 +f 103 476 485 +f 49 486 477 +f 486 49 50 +f 477 487 478 +f 487 477 486 +f 478 488 479 +f 488 478 487 +f 479 489 480 +f 489 479 488 +f 480 490 481 +f 490 480 489 +f 481 491 482 +f 491 481 490 +f 482 492 483 +f 492 482 491 +f 483 493 484 +f 493 483 492 +f 484 494 485 +f 494 484 493 +f 485 102 103 +f 102 485 494 +f 50 495 486 +f 495 50 51 +f 486 496 487 +f 496 486 495 +f 487 497 488 +f 497 487 496 +f 488 498 489 +f 498 488 497 +f 489 499 490 +f 499 489 498 +f 490 500 491 +f 500 490 499 +f 491 501 492 +f 501 491 500 +f 492 502 493 +f 502 492 501 +f 493 503 494 +f 503 493 502 +f 494 101 102 +f 101 494 503 +f 51 504 495 +f 504 51 52 +f 495 505 496 +f 505 495 504 +f 496 506 497 +f 506 496 505 +f 497 507 498 +f 507 497 506 +f 498 508 499 +f 508 498 507 +f 499 509 500 +f 509 499 508 +f 500 510 501 +f 510 500 509 +f 501 511 502 +f 511 501 510 +f 502 512 503 +f 512 502 511 +f 503 100 101 +f 100 503 512 +f 52 513 504 +f 513 52 53 +f 504 514 505 +f 514 504 513 +f 505 515 506 +f 515 505 514 +f 506 516 507 +f 516 506 515 +f 507 517 508 +f 517 507 516 +f 508 518 509 +f 518 508 517 +f 509 519 510 +f 519 509 518 +f 510 520 511 +f 520 510 519 +f 511 521 512 +f 521 511 520 +f 512 99 100 +f 99 512 521 +f 53 72 513 +f 72 53 5 +f 513 73 514 +f 73 513 72 +f 514 74 515 +f 74 514 73 +f 515 75 516 +f 75 515 74 +f 516 76 517 +f 76 516 75 +f 517 77 518 +f 77 517 76 +f 518 78 519 +f 78 518 77 +f 519 79 520 +f 79 519 78 +f 520 80 521 +f 80 520 79 +f 521 7 99 +f 7 521 80 +f 8 522 116 +f 522 8 90 +f 116 523 115 +f 523 116 522 +f 115 524 114 +f 524 115 523 +f 114 525 113 +f 525 114 524 +f 113 526 112 +f 526 113 525 +f 112 527 111 +f 527 112 526 +f 111 528 110 +f 528 111 527 +f 110 529 109 +f 529 110 528 +f 109 530 108 +f 530 109 529 +f 108 35 4 +f 35 108 530 +f 90 531 522 +f 531 90 91 +f 522 532 523 +f 532 522 531 +f 523 533 524 +f 533 523 532 +f 524 534 525 +f 534 524 533 +f 525 535 526 +f 535 525 534 +f 526 536 527 +f 536 526 535 +f 527 537 528 +f 537 527 536 +f 528 538 529 +f 538 528 537 +f 529 539 530 +f 539 529 538 +f 530 34 35 +f 34 530 539 +f 91 540 531 +f 540 91 92 +f 531 541 532 +f 541 531 540 +f 532 542 533 +f 542 532 541 +f 533 543 534 +f 543 533 542 +f 534 544 535 +f 544 534 543 +f 535 545 536 +f 545 535 544 +f 536 546 537 +f 546 536 545 +f 537 547 538 +f 547 537 546 +f 538 548 539 +f 548 538 547 +f 539 33 34 +f 33 539 548 +f 92 549 540 +f 549 92 93 +f 540 550 541 +f 550 540 549 +f 541 551 542 +f 551 541 550 +f 542 552 543 +f 552 542 551 +f 543 553 544 +f 553 543 552 +f 544 554 545 +f 554 544 553 +f 545 555 546 +f 555 545 554 +f 546 556 547 +f 556 546 555 +f 547 557 548 +f 557 547 556 +f 548 32 33 +f 32 548 557 +f 93 558 549 +f 558 93 94 +f 549 559 550 +f 559 549 558 +f 550 560 551 +f 560 550 559 +f 551 561 552 +f 561 551 560 +f 552 562 553 +f 562 552 561 +f 553 563 554 +f 563 553 562 +f 554 564 555 +f 564 554 563 +f 555 565 556 +f 565 555 564 +f 556 566 557 +f 566 556 565 +f 557 31 32 +f 31 557 566 +f 94 567 558 +f 567 94 95 +f 558 568 559 +f 568 558 567 +f 559 569 560 +f 569 559 568 +f 560 570 561 +f 570 560 569 +f 561 571 562 +f 571 561 570 +f 562 572 563 +f 572 562 571 +f 563 573 564 +f 573 563 572 +f 564 574 565 +f 574 564 573 +f 565 575 566 +f 575 565 574 +f 566 30 31 +f 30 566 575 +f 95 576 567 +f 576 95 96 +f 567 577 568 +f 577 567 576 +f 568 578 569 +f 578 568 577 +f 569 579 570 +f 579 569 578 +f 570 580 571 +f 580 570 579 +f 571 581 572 +f 581 571 580 +f 572 582 573 +f 582 572 581 +f 573 583 574 +f 583 573 582 +f 574 584 575 +f 584 574 583 +f 575 29 30 +f 29 575 584 +f 96 585 576 +f 585 96 97 +f 576 586 577 +f 586 576 585 +f 577 587 578 +f 587 577 586 +f 578 588 579 +f 588 578 587 +f 579 589 580 +f 589 579 588 +f 580 590 581 +f 590 580 589 +f 581 591 582 +f 591 581 590 +f 582 592 583 +f 592 582 591 +f 583 593 584 +f 593 583 592 +f 584 28 29 +f 28 584 593 +f 97 594 585 +f 594 97 98 +f 585 595 586 +f 595 585 594 +f 586 596 587 +f 596 586 595 +f 587 597 588 +f 597 587 596 +f 588 598 589 +f 598 588 597 +f 589 599 590 +f 599 589 598 +f 590 600 591 +f 600 590 599 +f 591 601 592 +f 601 591 600 +f 592 602 593 +f 602 592 601 +f 593 27 28 +f 27 593 602 +f 98 63 594 +f 63 98 6 +f 594 64 595 +f 64 594 63 +f 595 65 596 +f 65 595 64 +f 596 66 597 +f 66 596 65 +f 597 67 598 +f 67 597 66 +f 598 68 599 +f 68 598 67 +f 599 69 600 +f 69 599 68 +f 600 70 601 +f 70 600 69 +f 601 71 602 +f 71 601 70 +f 602 3 27 +f 3 602 71 +# faces: 1200 diff --git a/data/mesh/cross.stl b/data/mesh/cross.stl new file mode 100644 index 00000000..a0c19126 Binary files /dev/null and b/data/mesh/cross.stl differ diff --git a/data/mesh/cross_fine.stl b/data/mesh/cross_fine.stl new file mode 100644 index 00000000..d83f148a Binary files /dev/null and b/data/mesh/cross_fine.stl differ diff --git a/data/mesh/cyl_x_r1_h2_8sides.obj b/data/mesh/cyl_x_r1_h2_8sides.obj new file mode 100644 index 00000000..aa6c8a26 --- /dev/null +++ b/data/mesh/cyl_x_r1_h2_8sides.obj @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Closed octagonal cylinder: X axis, radius 1, length 2, centered at the origin. +# 16 vertices and 28 triangles; outward winding, no smooth normals. +o cylinder_x_octagonal +v -1 1 0 +v -1 0.707106781187 0.707106781187 +v -1 0 1 +v -1 -0.707106781187 0.707106781187 +v -1 -1 0 +v -1 -0.707106781187 -0.707106781187 +v -1 0 -1 +v -1 0.707106781187 -0.707106781187 +v 1 1 0 +v 1 0.707106781187 0.707106781187 +v 1 0 1 +v 1 -0.707106781187 0.707106781187 +v 1 -1 0 +v 1 -0.707106781187 -0.707106781187 +v 1 0 -1 +v 1 0.707106781187 -0.707106781187 +f 1 2 10 +f 1 10 9 +f 2 3 11 +f 2 11 10 +f 3 4 12 +f 3 12 11 +f 4 5 13 +f 4 13 12 +f 5 6 14 +f 5 14 13 +f 6 7 15 +f 6 15 14 +f 7 8 16 +f 7 16 15 +f 8 1 9 +f 8 9 16 +f 1 3 2 +f 9 10 11 +f 1 4 3 +f 9 11 12 +f 1 5 4 +f 9 12 13 +f 1 6 5 +f 9 13 14 +f 1 7 6 +f 9 14 15 +f 1 8 7 +f 9 15 16 diff --git a/data/mesh/drum.stl b/data/mesh/drum.stl new file mode 100644 index 00000000..bc08c27e Binary files /dev/null and b/data/mesh/drum.stl differ diff --git a/data/mesh/simpleTriangleShape4mm.stl b/data/mesh/simpleTriangleShape4mm.stl new file mode 100644 index 00000000..6d0f2f1a Binary files /dev/null and b/data/mesh/simpleTriangleShape4mm.stl differ diff --git a/data/mesh/sphere_highres.obj b/data/mesh/sphere_highres.obj new file mode 100755 index 00000000..71f7422b --- /dev/null +++ b/data/mesh/sphere_highres.obj @@ -0,0 +1,18492 @@ +# Generated by Visualization Toolkit +v 0 0 1 +v 0 0 -1 +v 0.04984588548541069 0 0.9987569451332092 +v 0.09956784546375275 0 0.9950307607650757 +v 0.1490422636270523 0 0.9888308048248291 +v 0.19814614951610565 0 0.9801725149154663 +v 0.2467574030160904 0 0.9690772891044617 +v 0.29475516080856323 0 0.955572783946991 +v 0.3420201539993286 0 0.9396926164627075 +v 0.3884347975254059 0 0.9214761853218079 +v 0.4338837265968323 0 0.9009688496589661 +v 0.4782539904117584 0 0.8782215714454651 +v 0.5214352011680603 0 0.853290855884552 +v 0.5633200407028198 0 0.826238751411438 +v 0.6038044095039368 0 0.7971324920654297 +v 0.6427876353263855 0 0.7660444378852844 +v 0.6801727414131165 0 0.7330518960952759 +v 0.7158668637275696 0 0.6982368230819702 +v 0.7497811913490295 0 0.6616858243942261 +v 0.7818315029144287 0 0.6234897971153259 +v 0.8119379878044128 0 0.583743691444397 +v 0.8400259017944336 0 0.542546272277832 +v 0.8660253882408142 0 0.5 +v 0.8898718357086182 0 0.4562106430530548 +v 0.9115058779716492 0 0.4112870991230011 +v 0.9308737516403198 0 0.36534103751182556 +v 0.9479273557662964 0 0.31848666071891785 +v 0.9626242518424988 0 0.27084046602249146 +v 0.9749279022216797 0 0.22252093255519867 +v 0.9848077297210693 0 0.1736481785774231 +v 0.9922391772270203 0 0.12434370815753937 +v 0.9972038269042969 0 0.07473009079694748 +v 0.9996891617774963 0 0.024930691346526146 +v 0.9996891617774963 0 -0.024930691346526146 +v 0.9972038269042969 0 -0.07473009079694748 +v 0.9922391772270203 0 -0.12434370815753937 +v 0.9848077297210693 0 -0.1736481785774231 +v 0.9749279022216797 0 -0.22252093255519867 +v 0.9626242518424988 0 -0.27084046602249146 +v 0.9479273557662964 0 -0.31848666071891785 +v 0.9308737516403198 0 -0.36534103751182556 +v 0.9115058779716492 0 -0.4112870991230011 +v 0.8898718357086182 0 -0.4562106430530548 +v 0.8660253882408142 0 -0.5 +v 0.8400259017944336 0 -0.542546272277832 +v 0.8119379878044128 0 -0.583743691444397 +v 0.7818315029144287 0 -0.6234897971153259 +v 0.7497811913490295 0 -0.6616858243942261 +v 0.7158668637275696 0 -0.6982368230819702 +v 0.6801727414131165 0 -0.7330518960952759 +v 0.6427876353263855 0 -0.7660444378852844 +v 0.6038044095039368 0 -0.7971324920654297 +v 0.5633200407028198 0 -0.826238751411438 +v 0.5214352011680603 0 -0.853290855884552 +v 0.4782539904117584 0 -0.8782215714454651 +v 0.4338837265968323 0 -0.9009688496589661 +v 0.3884347975254059 0 -0.9214761853218079 +v 0.3420201539993286 0 -0.9396926164627075 +v 0.29475516080856323 0 -0.955572783946991 +v 0.2467574030160904 0 -0.9690772891044617 +v 0.19814614951610565 0 -0.9801725149154663 +v 0.1490422636270523 0 -0.9888308048248291 +v 0.09956784546375275 0 -0.9950307607650757 +v 0.04984588548541069 0 -0.9987569451332092 +v 0.04960586503148079 0.004885751288384199 0.9987569451332092 +v 0.0990884006023407 0.009759355336427689 0.9950307607650757 +v 0.14832459390163422 0.014608697034418583 0.9888308048248291 +v 0.1971920132637024 0.019421719014644623 0.9801725149154663 +v 0.24556919932365417 0.024186454713344574 0.9690772891044617 +v 0.29333585500717163 0.028891058638691902 0.955572783946991 +v 0.3403732180595398 0.033523835241794586 0.9396926164627075 +v 0.38656437397003174 0.03807326778769493 0.9214761853218079 +v 0.4317944645881653 0.04252804443240166 0.9009688496589661 +v 0.47595104575157166 0.04687708616256714 0.8782215714454651 +v 0.518924355506897 0.05110958591103554 0.853290855884552 +v 0.5606074929237366 0.0552150197327137 0.826238751411438 +v 0.600896954536438 0.05918318033218384 0.7971324920654297 +v 0.6396924257278442 0.06300420314073563 0.7660444378852844 +v 0.6768975257873535 0.06666858494281769 0.7330518960952759 +v 0.7124197483062744 0.07016722112894058 0.6982368230819702 +v 0.7461708188056946 0.0734914094209671 0.6616858243942261 +v 0.7780667543411255 0.07663288712501526 0.6234897971153259 +v 0.8080282807350159 0.07958383858203888 0.583743691444397 +v 0.8359809517860413 0.08233693987131119 0.542546272277832 +v 0.8618552684783936 0.08488533645868301 0.5 +v 0.8855868577957153 0.08722268790006638 0.4562106430530548 +v 0.907116711139679 0.08934319764375687 0.4112870991230011 +v 0.9263913631439209 0.09124158322811127 0.36534103751182556 +v 0.943362832069397 0.09291312843561172 0.31848666071891785 +v 0.957988977432251 0.09435367584228516 0.27084046602249146 +v 0.9702333807945251 0.09555964916944504 0.22252093255519867 +v 0.980065643787384 0.09652803838253021 0.1736481785774231 +v 0.9874613285064697 0.09725645184516907 0.12434370815753937 +v 0.9924020171165466 0.09774306416511536 0.07473009079694748 +v 0.994875431060791 0.09798667579889297 0.024930691346526146 +v 0.994875431060791 0.09798667579889297 -0.024930691346526146 +v 0.9924020171165466 0.09774306416511536 -0.07473009079694748 +v 0.9874613285064697 0.09725645184516907 -0.12434370815753937 +v 0.980065643787384 0.09652803838253021 -0.1736481785774231 +v 0.9702333807945251 0.09555964916944504 -0.22252093255519867 +v 0.957988977432251 0.09435367584228516 -0.27084046602249146 +v 0.943362832069397 0.09291312843561172 -0.31848666071891785 +v 0.9263913631439209 0.09124158322811127 -0.36534103751182556 +v 0.907116711139679 0.08934319764375687 -0.4112870991230011 +v 0.8855868577957153 0.08722268790006638 -0.4562106430530548 +v 0.8618552684783936 0.08488533645868301 -0.5 +v 0.8359809517860413 0.08233693987131119 -0.542546272277832 +v 0.8080282807350159 0.07958383858203888 -0.583743691444397 +v 0.7780667543411255 0.07663288712501526 -0.6234897971153259 +v 0.7461708188056946 0.0734914094209671 -0.6616858243942261 +v 0.7124197483062744 0.07016722112894058 -0.6982368230819702 +v 0.6768975257873535 0.06666858494281769 -0.7330518960952759 +v 0.6396924257278442 0.06300420314073563 -0.7660444378852844 +v 0.600896954536438 0.05918318033218384 -0.7971324920654297 +v 0.5606074929237366 0.0552150197327137 -0.826238751411438 +v 0.518924355506897 0.05110958591103554 -0.853290855884552 +v 0.47595104575157166 0.04687708616256714 -0.8782215714454651 +v 0.4317944645881653 0.04252804443240166 -0.9009688496589661 +v 0.38656437397003174 0.03807326778769493 -0.9214761853218079 +v 0.3403732180595398 0.033523835241794586 -0.9396926164627075 +v 0.29333585500717163 0.028891058638691902 -0.955572783946991 +v 0.24556919932365417 0.024186454713344574 -0.9690772891044617 +v 0.1971920132637024 0.019421719014644623 -0.9801725149154663 +v 0.14832459390163422 0.014608697034418583 -0.9888308048248291 +v 0.0990884006023407 0.009759355336427689 -0.9950307607650757 +v 0.04960586503148079 0.004885751288384199 -0.9987569451332092 +v 0.048888109624385834 0.009724450297653675 0.9987569451332092 +v 0.09765467792749405 0.019424723461270332 0.9950307607650757 +v 0.1461784541606903 0.029076702892780304 0.9888308048248291 +v 0.1943388134241104 0.03865639492869377 0.9801725149154663 +v 0.2420160174369812 0.04813998192548752 0.9690772891044617 +v 0.2890915274620056 0.05750388279557228 0.955572783946991 +v 0.33544832468032837 0.06672482192516327 0.9396926164627075 +v 0.38097113370895386 0.07577987015247345 0.9214761853218079 +v 0.425546795129776 0.08464651554822922 0.9009688496589661 +v 0.46906447410583496 0.09330271929502487 0.8782215714454651 +v 0.511415958404541 0.1017269641160965 0.853290855884552 +v 0.5524960160255432 0.10989829152822495 0.826238751411438 +v 0.5922024846076965 0.1177963986992836 0.7971324920654297 +v 0.6304365992546082 0.12540164589881897 0.7660444378852844 +v 0.6671034097671509 0.13269512355327606 0.7330518960952759 +v 0.7021116614341736 0.13965868949890137 0.6982368230819702 +v 0.735374391078949 0.14627505838871002 0.6616858243942261 +v 0.766808807849884 0.15252774953842163 0.6234897971153259 +v 0.796336829662323 0.1584012508392334 0.583743691444397 +v 0.8238850831985474 0.16388092935085297 0.542546272277832 +v 0.8493849635124207 0.16895318031311035 0.5 +v 0.8727731704711914 0.17360538244247437 0.4562106430530548 +v 0.8939915299415588 0.17782597243785858 0.4112870991230011 +v 0.9129872918128967 0.18160445988178253 0.36534103751182556 +v 0.9297131896018982 0.1849314570426941 0.31848666071891785 +v 0.9441276788711548 0.18779867887496948 0.27084046602249146 +v 0.9561949372291565 0.19019900262355804 0.22252093255519867 +v 0.9658849239349365 0.19212646782398224 0.1736481785774231 +v 0.9731736183166504 0.19357626140117645 0.12434370815753937 +v 0.9780427813529968 0.19454480707645416 0.07473009079694748 +v 0.980480432510376 0.19502969086170197 0.024930691346526146 +v 0.980480432510376 0.19502969086170197 -0.024930691346526146 +v 0.9780427813529968 0.19454480707645416 -0.07473009079694748 +v 0.9731736183166504 0.19357626140117645 -0.12434370815753937 +v 0.9658849239349365 0.19212646782398224 -0.1736481785774231 +v 0.9561949372291565 0.19019900262355804 -0.22252093255519867 +v 0.9441276788711548 0.18779867887496948 -0.27084046602249146 +v 0.9297131896018982 0.1849314570426941 -0.31848666071891785 +v 0.9129872918128967 0.18160445988178253 -0.36534103751182556 +v 0.8939915299415588 0.17782597243785858 -0.4112870991230011 +v 0.8727731704711914 0.17360538244247437 -0.4562106430530548 +v 0.8493849635124207 0.16895318031311035 -0.5 +v 0.8238850831985474 0.16388092935085297 -0.542546272277832 +v 0.796336829662323 0.1584012508392334 -0.583743691444397 +v 0.766808807849884 0.15252774953842163 -0.6234897971153259 +v 0.735374391078949 0.14627505838871002 -0.6616858243942261 +v 0.7021116614341736 0.13965868949890137 -0.6982368230819702 +v 0.6671034097671509 0.13269512355327606 -0.7330518960952759 +v 0.6304365992546082 0.12540164589881897 -0.7660444378852844 +v 0.5922024846076965 0.1177963986992836 -0.7971324920654297 +v 0.5524960160255432 0.10989829152822495 -0.826238751411438 +v 0.511415958404541 0.1017269641160965 -0.853290855884552 +v 0.46906447410583496 0.09330271929502487 -0.8782215714454651 +v 0.425546795129776 0.08464651554822922 -0.9009688496589661 +v 0.38097113370895386 0.07577987015247345 -0.9214761853218079 +v 0.33544832468032837 0.06672482192516327 -0.9396926164627075 +v 0.2890915274620056 0.05750388279557228 -0.955572783946991 +v 0.2420160174369812 0.04813998192548752 -0.9690772891044617 +v 0.1943388134241104 0.03865639492869377 -0.9801725149154663 +v 0.1461784541606903 0.029076702892780304 -0.9888308048248291 +v 0.09765467792749405 0.019424723461270332 -0.9950307607650757 +v 0.048888109624385834 0.009724450297653675 -0.9987569451332092 +v 0.04769953712821007 0.01446949690580368 0.9987569451332092 +v 0.0952804908156395 0.028903020545840263 0.9950307607650757 +v 0.14262455701828003 0.043264687061309814 0.9888308048248291 +v 0.18961404263973236 0.05751878768205643 0.9801725149154663 +v 0.23613210022449493 0.07162988930940628 0.9690772891044617 +v 0.2820631265640259 0.08556290715932846 0.955572783946991 +v 0.32729285955429077 0.09928320348262787 0.9396926164627075 +v 0.3717089295387268 0.11275666952133179 0.9214761853218079 +v 0.4152008593082428 0.12594980001449585 0.9009688496589661 +v 0.4576605260372162 0.1388297975063324 0.8782215714454651 +v 0.49898236989974976 0.15136465430259705 0.853290855884552 +v 0.5390636920928955 0.16352318227291107 0.826238751411438 +v 0.5778048038482666 0.17527516186237335 0.7971324920654297 +v 0.615109384059906 0.18659138679504395 0.7660444378852844 +v 0.650884747505188 0.19744372367858887 0.7330518960952759 +v 0.6850418448448181 0.20780517160892487 0.6982368230819702 +v 0.717495858669281 0.21764999628067017 0.6616858243942261 +v 0.7481660842895508 0.22695370018482208 0.6234897971153259 +v 0.7769762277603149 0.23569315671920776 0.583743691444397 +v 0.8038547039031982 0.24384665489196777 0.542546272277832 +v 0.8287346363067627 0.2513939142227173 0.5 +v 0.8515542149543762 0.25831615924835205 0.4562106430530548 +v 0.8722566962242126 0.26459619402885437 0.4112870991230011 +v 0.8907906413078308 0.2702183723449707 0.36534103751182556 +v 0.9071099162101746 0.275168776512146 0.31848666071891785 +v 0.9211739897727966 0.27943506836891174 0.27084046602249146 +v 0.9329478144645691 0.2830066382884979 0.22252093255519867 +v 0.9424022436141968 0.285874605178833 0.1736481785774231 +v 0.9495137333869934 0.28803184628486633 0.12434370815753937 +v 0.9542645215988159 0.2894729971885681 0.07473009079694748 +v 0.9566429257392883 0.29019445180892944 0.024930691346526146 +v 0.9566429257392883 0.29019445180892944 -0.024930691346526146 +v 0.9542645215988159 0.2894729971885681 -0.07473009079694748 +v 0.9495137333869934 0.28803184628486633 -0.12434370815753937 +v 0.9424022436141968 0.285874605178833 -0.1736481785774231 +v 0.9329478144645691 0.2830066382884979 -0.22252093255519867 +v 0.9211739897727966 0.27943506836891174 -0.27084046602249146 +v 0.9071099162101746 0.275168776512146 -0.31848666071891785 +v 0.8907906413078308 0.2702183723449707 -0.36534103751182556 +v 0.8722566962242126 0.26459619402885437 -0.4112870991230011 +v 0.8515542149543762 0.25831615924835205 -0.4562106430530548 +v 0.8287346363067627 0.2513939142227173 -0.5 +v 0.8038547039031982 0.24384665489196777 -0.542546272277832 +v 0.7769762277603149 0.23569315671920776 -0.583743691444397 +v 0.7481660842895508 0.22695370018482208 -0.6234897971153259 +v 0.717495858669281 0.21764999628067017 -0.6616858243942261 +v 0.6850418448448181 0.20780517160892487 -0.6982368230819702 +v 0.650884747505188 0.19744372367858887 -0.7330518960952759 +v 0.615109384059906 0.18659138679504395 -0.7660444378852844 +v 0.5778048038482666 0.17527516186237335 -0.7971324920654297 +v 0.5390636920928955 0.16352318227291107 -0.826238751411438 +v 0.49898236989974976 0.15136465430259705 -0.853290855884552 +v 0.4576605260372162 0.1388297975063324 -0.8782215714454651 +v 0.4152008593082428 0.12594980001449585 -0.9009688496589661 +v 0.3717089295387268 0.11275666952133179 -0.9214761853218079 +v 0.32729285955429077 0.09928320348262787 -0.9396926164627075 +v 0.2820631265640259 0.08556290715932846 -0.955572783946991 +v 0.23613210022449493 0.07162988930940628 -0.9690772891044617 +v 0.18961404263973236 0.05751878768205643 -0.9801725149154663 +v 0.14262455701828003 0.043264687061309814 -0.9888308048248291 +v 0.0952804908156395 0.028903020545840263 -0.9950307607650757 +v 0.04769953712821007 0.01446949690580368 -0.9987569451332092 +v 0.046051595360040665 0.019075194373726845 0.9987569451332092 +v 0.0919886976480484 0.03810296580195427 0.9950307607650757 +v 0.13769710063934326 0.05703600496053696 0.9888308048248291 +v 0.18306316435337067 0.07582724839448929 0.9801725149154663 +v 0.22797410190105438 0.09442996978759766 0.9690772891044617 +v 0.2723182737827301 0.11279792338609695 0.955572783946991 +v 0.31598541140556335 0.13088543713092804 0.9396926164627075 +v 0.35886695981025696 0.14864756166934967 0.9214761853218079 +v 0.4008563160896301 0.16604012250900269 0.9009688496589661 +v 0.4418490529060364 0.18301987648010254 0.8782215714454651 +v 0.48174330592155457 0.19954460859298706 0.853290855884552 +v 0.520439863204956 0.215573251247406 0.826238751411438 +v 0.5578425526618958 0.23106594383716583 0.7971324920654297 +v 0.5938583016395569 0.24598416686058044 0.7660444378852844 +v 0.6283976435661316 0.26029083132743835 0.7330518960952759 +v 0.6613747477531433 0.27395036816596985 0.6982368230819702 +v 0.6927074790000916 0.28692883253097534 0.6616858243942261 +v 0.7223181128501892 0.29919394850730896 0.6234897971153259 +v 0.7501329183578491 0.3107152283191681 0.583743691444397 +v 0.7760827541351318 0.32146400213241577 0.542546272277832 +v 0.8001031279563904 0.3314135670661926 0.5 +v 0.8221343755722046 0.34053918719291687 0.4562106430530548 +v 0.8421216011047363 0.34881818294525146 0.4112870991230011 +v 0.8600152134895325 0.3562299609184265 0.36534103751182556 +v 0.8757706880569458 0.3627561032772064 0.31848666071891785 +v 0.8893488645553589 0.3683803379535675 0.27084046602249146 +v 0.9007159471511841 0.3730887472629547 0.22252093255519867 +v 0.9098437428474426 0.3768696188926697 0.1736481785774231 +v 0.9167094826698303 0.3797135055065155 0.12434370815753937 +v 0.9212961792945862 0.3816133737564087 0.07473009079694748 +v 0.9235923886299133 0.3825644850730896 0.024930691346526146 +v 0.9235923886299133 0.3825644850730896 -0.024930691346526146 +v 0.9212961792945862 0.3816133737564087 -0.07473009079694748 +v 0.9167094826698303 0.3797135055065155 -0.12434370815753937 +v 0.9098437428474426 0.3768696188926697 -0.1736481785774231 +v 0.9007159471511841 0.3730887472629547 -0.22252093255519867 +v 0.8893488645553589 0.3683803379535675 -0.27084046602249146 +v 0.8757706880569458 0.3627561032772064 -0.31848666071891785 +v 0.8600152134895325 0.3562299609184265 -0.36534103751182556 +v 0.8421216011047363 0.34881818294525146 -0.4112870991230011 +v 0.8221343755722046 0.34053918719291687 -0.4562106430530548 +v 0.8001031279563904 0.3314135670661926 -0.5 +v 0.7760827541351318 0.32146400213241577 -0.542546272277832 +v 0.7501329183578491 0.3107152283191681 -0.583743691444397 +v 0.7223181128501892 0.29919394850730896 -0.6234897971153259 +v 0.6927074790000916 0.28692883253097534 -0.6616858243942261 +v 0.6613747477531433 0.27395036816596985 -0.6982368230819702 +v 0.6283976435661316 0.26029083132743835 -0.7330518960952759 +v 0.5938583016395569 0.24598416686058044 -0.7660444378852844 +v 0.5578425526618958 0.23106594383716583 -0.7971324920654297 +v 0.520439863204956 0.215573251247406 -0.826238751411438 +v 0.48174330592155457 0.19954460859298706 -0.853290855884552 +v 0.4418490529060364 0.18301987648010254 -0.8782215714454651 +v 0.4008563160896301 0.16604012250900269 -0.9009688496589661 +v 0.35886695981025696 0.14864756166934967 -0.9214761853218079 +v 0.31598541140556335 0.13088543713092804 -0.9396926164627075 +v 0.2723182737827301 0.11279792338609695 -0.955572783946991 +v 0.22797410190105438 0.09442996978759766 -0.9690772891044617 +v 0.18306316435337067 0.07582724839448929 -0.9801725149154663 +v 0.13769710063934326 0.05703600496053696 -0.9888308048248291 +v 0.0919886976480484 0.03810296580195427 -0.9950307607650757 +v 0.046051595360040665 0.019075194373726845 -0.9987569451332092 +v 0.043960146605968475 0.023497188463807106 0.9987569451332092 +v 0.08781100064516068 0.04693595692515373 0.9950307607650757 +v 0.1314435452222824 0.07025803625583649 0.9888308048248291 +v 0.17474929988384247 0.09340544790029526 0.9801725149154663 +v 0.2176205962896347 0.11632063239812851 0.9690772891044617 +v 0.2599508464336395 0.13894662261009216 0.955572783946991 +v 0.30163484811782837 0.16122718155384064 0.9396926164627075 +v 0.34256890416145325 0.1831068992614746 0.9214761853218079 +v 0.38265129923820496 0.20453137159347534 0.9009688496589661 +v 0.42178234457969666 0.2254473716020584 0.8782215714454651 +v 0.4598647952079773 0.2458028495311737 0.853290855884552 +v 0.4968039393424988 0.2655472457408905 0.826238751411438 +v 0.5325079560279846 0.2846314311027527 0.7971324920654297 +v 0.5668880343437195 0.30300799012184143 0.7660444378852844 +v 0.599858820438385 0.320631206035614 0.7330518960952759 +v 0.6313381791114807 0.3374572992324829 0.6982368230819702 +v 0.661247968673706 0.3534444272518158 0.6616858243942261 +v 0.6895138025283813 0.3685528039932251 0.6234897971153259 +v 0.7160654067993164 0.3827449381351471 0.583743691444397 +v 0.7408367395401001 0.39598548412323 0.542546272277832 +v 0.7637662291526794 0.40824154019355774 0.5 +v 0.7847968935966492 0.41948267817497253 0.4562106430530548 +v 0.8038763999938965 0.42968088388442993 0.4112870991230011 +v 0.820957362651825 0.4388108551502228 0.36534103751182556 +v 0.8359972834587097 0.44684985280036926 0.31848666071891785 +v 0.8489587903022766 0.453777939081192 0.27084046602249146 +v 0.8598096370697021 0.4595778286457062 0.22252093255519867 +v 0.8685228824615479 0.4642351567745209 0.1736481785774231 +v 0.8750768303871155 0.4677383303642273 0.12434370815753937 +v 0.8794552087783813 0.47007861733436584 0.07473009079694748 +v 0.8816471695899963 0.4712502062320709 0.024930691346526146 +v 0.8816471695899963 0.4712502062320709 -0.024930691346526146 +v 0.8794552087783813 0.47007861733436584 -0.07473009079694748 +v 0.8750768303871155 0.4677383303642273 -0.12434370815753937 +v 0.8685228824615479 0.4642351567745209 -0.1736481785774231 +v 0.8598096370697021 0.4595778286457062 -0.22252093255519867 +v 0.8489587903022766 0.453777939081192 -0.27084046602249146 +v 0.8359972834587097 0.44684985280036926 -0.31848666071891785 +v 0.820957362651825 0.4388108551502228 -0.36534103751182556 +v 0.8038763999938965 0.42968088388442993 -0.4112870991230011 +v 0.7847968935966492 0.41948267817497253 -0.4562106430530548 +v 0.7637662291526794 0.40824154019355774 -0.5 +v 0.7408367395401001 0.39598548412323 -0.542546272277832 +v 0.7160654067993164 0.3827449381351471 -0.583743691444397 +v 0.6895138025283813 0.3685528039932251 -0.6234897971153259 +v 0.661247968673706 0.3534444272518158 -0.6616858243942261 +v 0.6313381791114807 0.3374572992324829 -0.6982368230819702 +v 0.599858820438385 0.320631206035614 -0.7330518960952759 +v 0.5668880343437195 0.30300799012184143 -0.7660444378852844 +v 0.5325079560279846 0.2846314311027527 -0.7971324920654297 +v 0.4968039393424988 0.2655472457408905 -0.826238751411438 +v 0.4598647952079773 0.2458028495311737 -0.853290855884552 +v 0.42178234457969666 0.2254473716020584 -0.8782215714454651 +v 0.38265129923820496 0.20453137159347534 -0.9009688496589661 +v 0.34256890416145325 0.1831068992614746 -0.9214761853218079 +v 0.30163484811782837 0.16122718155384064 -0.9396926164627075 +v 0.2599508464336395 0.13894662261009216 -0.955572783946991 +v 0.2176205962896347 0.11632063239812851 -0.9690772891044617 +v 0.17474929988384247 0.09340544790029526 -0.9801725149154663 +v 0.1314435452222824 0.07025803625583649 -0.9888308048248291 +v 0.08781100064516068 0.04693595692515373 -0.9950307607650757 +v 0.043960146605968475 0.023497188463807106 -0.9987569451332092 +v 0.04144534096121788 0.0276928897947073 0.9987569451332092 +v 0.0827876403927803 0.05531693249940872 0.9950307607650757 +v 0.12392411381006241 0.08280344307422638 0.9888308048248291 +v 0.16475249826908112 0.11008410155773163 0.9801725149154663 +v 0.20517127215862274 0.13709107041358948 0.9690772891044617 +v 0.24507996439933777 0.16375720500946045 0.955572783946991 +v 0.28437936305999756 0.19001621007919312 0.9396926164627075 +v 0.32297173142433167 0.21580280363559723 0.9214761853218079 +v 0.3607611358165741 0.24105289578437805 0.9009688496589661 +v 0.39765363931655884 0.2657036781311035 0.8782215714454651 +v 0.43355754017829895 0.2896938920021057 0.853290855884552 +v 0.4683835208415985 0.3129638433456421 0.826238751411438 +v 0.5020450353622437 0.3354557454586029 0.7971324920654297 +v 0.534458339214325 0.35711365938186646 0.7660444378852844 +v 0.5655429363250732 0.3778837323188782 0.7330518960952759 +v 0.5952215194702148 0.3977143168449402 0.6982368230819702 +v 0.6234202980995178 0.41655611991882324 0.6616858243942261 +v 0.6500691175460815 0.4343622922897339 0.6234897971153259 +v 0.6751017570495605 0.4510885775089264 0.583743691444397 +v 0.6984560489654541 0.4666934013366699 0.542546272277832 +v 0.7200738191604614 0.4811379313468933 0.5 +v 0.7399013638496399 0.4943862855434418 0.4562106430530548 +v 0.7578893899917603 0.5064055323600769 0.4112870991230011 +v 0.7739932537078857 0.5171657204627991 0.36534103751182556 +v 0.7881727814674377 0.5266402363777161 0.31848666071891785 +v 0.8003928065299988 0.5348053574562073 0.27084046602249146 +v 0.8106229305267334 0.5416409373283386 0.22252093255519867 +v 0.8188377022743225 0.5471298694610596 0.1736481785774231 +v 0.8250167369842529 0.5512585639953613 0.12434370815753937 +v 0.8291446566581726 0.5540167689323425 0.07473009079694748 +v 0.8312111496925354 0.5553975701332092 0.024930691346526146 +v 0.8312111496925354 0.5553975701332092 -0.024930691346526146 +v 0.8291446566581726 0.5540167689323425 -0.07473009079694748 +v 0.8250167369842529 0.5512585639953613 -0.12434370815753937 +v 0.8188377022743225 0.5471298694610596 -0.1736481785774231 +v 0.8106229305267334 0.5416409373283386 -0.22252093255519867 +v 0.8003928065299988 0.5348053574562073 -0.27084046602249146 +v 0.7881727814674377 0.5266402363777161 -0.31848666071891785 +v 0.7739932537078857 0.5171657204627991 -0.36534103751182556 +v 0.7578893899917603 0.5064055323600769 -0.4112870991230011 +v 0.7399013638496399 0.4943862855434418 -0.4562106430530548 +v 0.7200738191604614 0.4811379313468933 -0.5 +v 0.6984560489654541 0.4666934013366699 -0.542546272277832 +v 0.6751017570495605 0.4510885775089264 -0.583743691444397 +v 0.6500691175460815 0.4343622922897339 -0.6234897971153259 +v 0.6234202980995178 0.41655611991882324 -0.6616858243942261 +v 0.5952215194702148 0.3977143168449402 -0.6982368230819702 +v 0.5655429363250732 0.3778837323188782 -0.7330518960952759 +v 0.534458339214325 0.35711365938186646 -0.7660444378852844 +v 0.5020450353622437 0.3354557454586029 -0.7971324920654297 +v 0.4683835208415985 0.3129638433456421 -0.826238751411438 +v 0.43355754017829895 0.2896938920021057 -0.853290855884552 +v 0.39765363931655884 0.2657036781311035 -0.8782215714454651 +v 0.3607611358165741 0.24105289578437805 -0.9009688496589661 +v 0.32297173142433167 0.21580280363559723 -0.9214761853218079 +v 0.28437936305999756 0.19001621007919312 -0.9396926164627075 +v 0.24507996439933777 0.16375720500946045 -0.955572783946991 +v 0.20517127215862274 0.13709107041358948 -0.9690772891044617 +v 0.16475249826908112 0.11008410155773163 -0.9801725149154663 +v 0.12392411381006241 0.08280344307422638 -0.9888308048248291 +v 0.0827876403927803 0.05531693249940872 -0.9950307607650757 +v 0.04144534096121788 0.0276928897947073 -0.9987569451332092 +v 0.03853138908743858 0.03162189573049545 0.9987569451332092 +v 0.07696698606014252 0.06316517293453217 0.9950307607650757 +v 0.11521122604608536 0.09455141425132751 0.9888308048248291 +v 0.15316903591156006 0.12570258975028992 0.9801725149154663 +v 0.19074605405330658 0.15654124319553375 0.9690772891044617 +v 0.2278488278388977 0.18699070811271667 0.955572783946991 +v 0.2643851339817047 0.21697528660297394 0.9396926164627075 +v 0.3002641499042511 0.24642042815685272 0.9214761853218079 +v 0.3353966772556305 0.27525293827056885 0.9009688496589661 +v 0.3696953356266022 0.3034011125564575 0.8782215714454651 +v 0.40307486057281494 0.3307949900627136 0.853290855884552 +v 0.43545228242874146 0.3573664724826813 0.826238751411438 +v 0.46674713492393494 0.38304945826530457 0.7971324920654297 +v 0.49688154458999634 0.40778014063835144 0.7660444378852844 +v 0.5257806181907654 0.4314970076084137 0.7330518960952759 +v 0.5533725619316101 0.45414111018180847 0.6982368230819702 +v 0.5795887112617493 0.4756561517715454 0.6616858243942261 +v 0.6043639183044434 0.4959886372089386 0.6234897971153259 +v 0.6276365518569946 0.5150880217552185 0.583743691444397 +v 0.649348795413971 0.5329068303108215 0.542546272277832 +v 0.6694467067718506 0.5494006872177124 0.5 +v 0.6878802180290222 0.5645287036895752 0.4562106430530548 +v 0.7046035528182983 0.5782532095909119 0.4112870991230011 +v 0.7195751667022705 0.5905400514602661 0.36534103751182556 +v 0.7327577471733093 0.6013587713241577 0.31848666071891785 +v 0.7441186308860779 0.6106823682785034 0.27084046602249146 +v 0.7536294460296631 0.6184877157211304 0.22252093255519867 +v 0.7612667083740234 0.6247554421424866 0.1736481785774231 +v 0.767011284828186 0.6294698715209961 0.12434370815753937 +v 0.7708489298820496 0.6326193809509277 0.07473009079694748 +v 0.7727701663970947 0.6341961026191711 0.024930691346526146 +v 0.7727701663970947 0.6341961026191711 -0.024930691346526146 +v 0.7708489298820496 0.6326193809509277 -0.07473009079694748 +v 0.767011284828186 0.6294698715209961 -0.12434370815753937 +v 0.7612667083740234 0.6247554421424866 -0.1736481785774231 +v 0.7536294460296631 0.6184877157211304 -0.22252093255519867 +v 0.7441186308860779 0.6106823682785034 -0.27084046602249146 +v 0.7327577471733093 0.6013587713241577 -0.31848666071891785 +v 0.7195751667022705 0.5905400514602661 -0.36534103751182556 +v 0.7046035528182983 0.5782532095909119 -0.4112870991230011 +v 0.6878802180290222 0.5645287036895752 -0.4562106430530548 +v 0.6694467067718506 0.5494006872177124 -0.5 +v 0.649348795413971 0.5329068303108215 -0.542546272277832 +v 0.6276365518569946 0.5150880217552185 -0.583743691444397 +v 0.6043639183044434 0.4959886372089386 -0.6234897971153259 +v 0.5795887112617493 0.4756561517715454 -0.6616858243942261 +v 0.5533725619316101 0.45414111018180847 -0.6982368230819702 +v 0.5257806181907654 0.4314970076084137 -0.7330518960952759 +v 0.49688154458999634 0.40778014063835144 -0.7660444378852844 +v 0.46674713492393494 0.38304945826530457 -0.7971324920654297 +v 0.43545228242874146 0.3573664724826813 -0.826238751411438 +v 0.40307486057281494 0.3307949900627136 -0.853290855884552 +v 0.3696953356266022 0.3034011125564575 -0.8782215714454651 +v 0.3353966772556305 0.27525293827056885 -0.9009688496589661 +v 0.3002641499042511 0.24642042815685272 -0.9214761853218079 +v 0.2643851339817047 0.21697528660297394 -0.9396926164627075 +v 0.2278488278388977 0.18699070811271667 -0.955572783946991 +v 0.19074605405330658 0.15654124319553375 -0.9690772891044617 +v 0.15316903591156006 0.12570258975028992 -0.9801725149154663 +v 0.11521122604608536 0.09455141425132751 -0.9888308048248291 +v 0.07696698606014252 0.06316517293453217 -0.9950307607650757 +v 0.03853138908743858 0.03162189573049545 -0.9987569451332092 +v 0.035246364772319794 0.035246364772319794 0.9987569451332092 +v 0.07040509581565857 0.07040509581565857 0.9950307607650757 +v 0.10538879781961441 0.10538879781961441 0.9888308048248291 +v 0.14011047780513763 0.14011047780513763 0.9801725149154663 +v 0.17448383569717407 0.17448383569717407 0.9690772891044617 +v 0.20842337608337402 0.20842337608337402 0.955572783946991 +v 0.2418447583913803 0.2418447583913803 0.9396926164627075 +v 0.27466487884521484 0.27466487884521484 0.9214761853218079 +v 0.3068021237850189 0.3068021237850189 0.9009688496589661 +v 0.3381766378879547 0.3381766378879547 0.8782215714454651 +v 0.36871036887168884 0.36871036887168884 0.853290855884552 +v 0.39832744002342224 0.39832744002342224 0.826238751411438 +v 0.4269541800022125 0.4269541800022125 0.7971324920654297 +v 0.45451948046684265 0.45451948046684265 0.7660444378852844 +v 0.48095476627349854 0.48095476627349854 0.7330518960952759 +v 0.5061942934989929 0.5061942934989929 0.6982368230819702 +v 0.5301753878593445 0.5301753878593445 0.6616858243942261 +v 0.5528383255004883 0.5528383255004883 0.6234897971153259 +v 0.5741268992424011 0.5741268992424011 0.583743691444397 +v 0.5939880013465881 0.5939880013465881 0.542546272277832 +v 0.6123724579811096 0.6123724579811096 0.5 +v 0.6292343735694885 0.6292343735694885 0.4562106430530548 +v 0.6445319652557373 0.6445319652557373 0.4112870991230011 +v 0.6582271456718445 0.6582271456718445 0.36534103751182556 +v 0.6702858805656433 0.6702858805656433 0.31848666071891785 +v 0.680678129196167 0.680678129196167 0.27084046602249146 +v 0.6893781423568726 0.6893781423568726 0.22252093255519867 +v 0.6963642239570618 0.6963642239570618 0.1736481785774231 +v 0.7016190886497498 0.7016190886497498 0.12434370815753937 +v 0.7051295638084412 0.7051295638084412 0.07473009079694748 +v 0.7068870067596436 0.7068870067596436 0.024930691346526146 +v 0.7068870067596436 0.7068870067596436 -0.024930691346526146 +v 0.7051295638084412 0.7051295638084412 -0.07473009079694748 +v 0.7016190886497498 0.7016190886497498 -0.12434370815753937 +v 0.6963642239570618 0.6963642239570618 -0.1736481785774231 +v 0.6893781423568726 0.6893781423568726 -0.22252093255519867 +v 0.680678129196167 0.680678129196167 -0.27084046602249146 +v 0.6702858805656433 0.6702858805656433 -0.31848666071891785 +v 0.6582271456718445 0.6582271456718445 -0.36534103751182556 +v 0.6445319652557373 0.6445319652557373 -0.4112870991230011 +v 0.6292343735694885 0.6292343735694885 -0.4562106430530548 +v 0.6123724579811096 0.6123724579811096 -0.5 +v 0.5939880013465881 0.5939880013465881 -0.542546272277832 +v 0.5741268992424011 0.5741268992424011 -0.583743691444397 +v 0.5528383255004883 0.5528383255004883 -0.6234897971153259 +v 0.5301753878593445 0.5301753878593445 -0.6616858243942261 +v 0.5061942934989929 0.5061942934989929 -0.6982368230819702 +v 0.48095476627349854 0.48095476627349854 -0.7330518960952759 +v 0.45451948046684265 0.45451948046684265 -0.7660444378852844 +v 0.4269541800022125 0.4269541800022125 -0.7971324920654297 +v 0.39832744002342224 0.39832744002342224 -0.826238751411438 +v 0.36871036887168884 0.36871036887168884 -0.853290855884552 +v 0.3381766378879547 0.3381766378879547 -0.8782215714454651 +v 0.3068021237850189 0.3068021237850189 -0.9009688496589661 +v 0.27466487884521484 0.27466487884521484 -0.9214761853218079 +v 0.2418447583913803 0.2418447583913803 -0.9396926164627075 +v 0.20842337608337402 0.20842337608337402 -0.955572783946991 +v 0.17448383569717407 0.17448383569717407 -0.9690772891044617 +v 0.14011047780513763 0.14011047780513763 -0.9801725149154663 +v 0.10538879781961441 0.10538879781961441 -0.9888308048248291 +v 0.07040509581565857 0.07040509581565857 -0.9950307607650757 +v 0.035246364772319794 0.035246364772319794 -0.9987569451332092 +v 0.03162189573049545 0.03853138908743858 0.9987569451332092 +v 0.06316517293453217 0.07696698606014252 0.9950307607650757 +v 0.09455141425132751 0.11521122604608536 0.9888308048248291 +v 0.12570258975028992 0.15316903591156006 0.9801725149154663 +v 0.15654124319553375 0.19074605405330658 0.9690772891044617 +v 0.18699070811271667 0.2278488278388977 0.955572783946991 +v 0.21697528660297394 0.2643851339817047 0.9396926164627075 +v 0.24642042815685272 0.3002641499042511 0.9214761853218079 +v 0.27525293827056885 0.3353966772556305 0.9009688496589661 +v 0.3034011125564575 0.3696953356266022 0.8782215714454651 +v 0.3307949900627136 0.40307486057281494 0.853290855884552 +v 0.3573664724826813 0.43545228242874146 0.826238751411438 +v 0.38304945826530457 0.46674713492393494 0.7971324920654297 +v 0.40778014063835144 0.49688154458999634 0.7660444378852844 +v 0.4314970076084137 0.5257806181907654 0.7330518960952759 +v 0.45414111018180847 0.5533725619316101 0.6982368230819702 +v 0.4756561517715454 0.5795887112617493 0.6616858243942261 +v 0.4959886372089386 0.6043639183044434 0.6234897971153259 +v 0.5150880217552185 0.6276365518569946 0.583743691444397 +v 0.5329068303108215 0.649348795413971 0.542546272277832 +v 0.5494006872177124 0.6694467067718506 0.5 +v 0.5645287036895752 0.6878802180290222 0.4562106430530548 +v 0.5782532095909119 0.7046035528182983 0.4112870991230011 +v 0.5905400514602661 0.7195751667022705 0.36534103751182556 +v 0.6013587713241577 0.7327577471733093 0.31848666071891785 +v 0.6106823682785034 0.7441186308860779 0.27084046602249146 +v 0.6184877157211304 0.7536294460296631 0.22252093255519867 +v 0.6247554421424866 0.7612667083740234 0.1736481785774231 +v 0.6294698715209961 0.767011284828186 0.12434370815753937 +v 0.6326193809509277 0.7708489298820496 0.07473009079694748 +v 0.6341961026191711 0.7727701663970947 0.024930691346526146 +v 0.6341961026191711 0.7727701663970947 -0.024930691346526146 +v 0.6326193809509277 0.7708489298820496 -0.07473009079694748 +v 0.6294698715209961 0.767011284828186 -0.12434370815753937 +v 0.6247554421424866 0.7612667083740234 -0.1736481785774231 +v 0.6184877157211304 0.7536294460296631 -0.22252093255519867 +v 0.6106823682785034 0.7441186308860779 -0.27084046602249146 +v 0.6013587713241577 0.7327577471733093 -0.31848666071891785 +v 0.5905400514602661 0.7195751667022705 -0.36534103751182556 +v 0.5782532095909119 0.7046035528182983 -0.4112870991230011 +v 0.5645287036895752 0.6878802180290222 -0.4562106430530548 +v 0.5494006872177124 0.6694467067718506 -0.5 +v 0.5329068303108215 0.649348795413971 -0.542546272277832 +v 0.5150880217552185 0.6276365518569946 -0.583743691444397 +v 0.4959886372089386 0.6043639183044434 -0.6234897971153259 +v 0.4756561517715454 0.5795887112617493 -0.6616858243942261 +v 0.45414111018180847 0.5533725619316101 -0.6982368230819702 +v 0.4314970076084137 0.5257806181907654 -0.7330518960952759 +v 0.40778014063835144 0.49688154458999634 -0.7660444378852844 +v 0.38304945826530457 0.46674713492393494 -0.7971324920654297 +v 0.3573664724826813 0.43545228242874146 -0.826238751411438 +v 0.3307949900627136 0.40307486057281494 -0.853290855884552 +v 0.3034011125564575 0.3696953356266022 -0.8782215714454651 +v 0.27525293827056885 0.3353966772556305 -0.9009688496589661 +v 0.24642042815685272 0.3002641499042511 -0.9214761853218079 +v 0.21697528660297394 0.2643851339817047 -0.9396926164627075 +v 0.18699070811271667 0.2278488278388977 -0.955572783946991 +v 0.15654124319553375 0.19074605405330658 -0.9690772891044617 +v 0.12570258975028992 0.15316903591156006 -0.9801725149154663 +v 0.09455141425132751 0.11521122604608536 -0.9888308048248291 +v 0.06316517293453217 0.07696698606014252 -0.9950307607650757 +v 0.03162189573049545 0.03853138908743858 -0.9987569451332092 +v 0.0276928897947073 0.04144534096121788 0.9987569451332092 +v 0.05531693249940872 0.0827876403927803 0.9950307607650757 +v 0.08280344307422638 0.12392411381006241 0.9888308048248291 +v 0.11008410155773163 0.16475249826908112 0.9801725149154663 +v 0.13709107041358948 0.20517127215862274 0.9690772891044617 +v 0.16375720500946045 0.24507996439933777 0.955572783946991 +v 0.19001621007919312 0.28437936305999756 0.9396926164627075 +v 0.21580280363559723 0.32297173142433167 0.9214761853218079 +v 0.24105289578437805 0.3607611358165741 0.9009688496589661 +v 0.2657036781311035 0.39765363931655884 0.8782215714454651 +v 0.2896938920021057 0.43355754017829895 0.853290855884552 +v 0.3129638433456421 0.4683835208415985 0.826238751411438 +v 0.3354557454586029 0.5020450353622437 0.7971324920654297 +v 0.35711365938186646 0.534458339214325 0.7660444378852844 +v 0.3778837323188782 0.5655429363250732 0.7330518960952759 +v 0.3977143168449402 0.5952215194702148 0.6982368230819702 +v 0.41655611991882324 0.6234202980995178 0.6616858243942261 +v 0.4343622922897339 0.6500691175460815 0.6234897971153259 +v 0.4510885775089264 0.6751017570495605 0.583743691444397 +v 0.4666934013366699 0.6984560489654541 0.542546272277832 +v 0.4811379313468933 0.7200738191604614 0.5 +v 0.4943862855434418 0.7399013638496399 0.4562106430530548 +v 0.5064055323600769 0.7578893899917603 0.4112870991230011 +v 0.5171657204627991 0.7739932537078857 0.36534103751182556 +v 0.5266402363777161 0.7881727814674377 0.31848666071891785 +v 0.5348053574562073 0.8003928065299988 0.27084046602249146 +v 0.5416409373283386 0.8106229305267334 0.22252093255519867 +v 0.5471298694610596 0.8188377022743225 0.1736481785774231 +v 0.5512585639953613 0.8250167369842529 0.12434370815753937 +v 0.5540167689323425 0.8291446566581726 0.07473009079694748 +v 0.5553975701332092 0.8312111496925354 0.024930691346526146 +v 0.5553975701332092 0.8312111496925354 -0.024930691346526146 +v 0.5540167689323425 0.8291446566581726 -0.07473009079694748 +v 0.5512585639953613 0.8250167369842529 -0.12434370815753937 +v 0.5471298694610596 0.8188377022743225 -0.1736481785774231 +v 0.5416409373283386 0.8106229305267334 -0.22252093255519867 +v 0.5348053574562073 0.8003928065299988 -0.27084046602249146 +v 0.5266402363777161 0.7881727814674377 -0.31848666071891785 +v 0.5171657204627991 0.7739932537078857 -0.36534103751182556 +v 0.5064055323600769 0.7578893899917603 -0.4112870991230011 +v 0.4943862855434418 0.7399013638496399 -0.4562106430530548 +v 0.4811379313468933 0.7200738191604614 -0.5 +v 0.4666934013366699 0.6984560489654541 -0.542546272277832 +v 0.4510885775089264 0.6751017570495605 -0.583743691444397 +v 0.4343622922897339 0.6500691175460815 -0.6234897971153259 +v 0.41655611991882324 0.6234202980995178 -0.6616858243942261 +v 0.3977143168449402 0.5952215194702148 -0.6982368230819702 +v 0.3778837323188782 0.5655429363250732 -0.7330518960952759 +v 0.35711365938186646 0.534458339214325 -0.7660444378852844 +v 0.3354557454586029 0.5020450353622437 -0.7971324920654297 +v 0.3129638433456421 0.4683835208415985 -0.826238751411438 +v 0.2896938920021057 0.43355754017829895 -0.853290855884552 +v 0.2657036781311035 0.39765363931655884 -0.8782215714454651 +v 0.24105289578437805 0.3607611358165741 -0.9009688496589661 +v 0.21580280363559723 0.32297173142433167 -0.9214761853218079 +v 0.19001621007919312 0.28437936305999756 -0.9396926164627075 +v 0.16375720500946045 0.24507996439933777 -0.955572783946991 +v 0.13709107041358948 0.20517127215862274 -0.9690772891044617 +v 0.11008410155773163 0.16475249826908112 -0.9801725149154663 +v 0.08280344307422638 0.12392411381006241 -0.9888308048248291 +v 0.05531693249940872 0.0827876403927803 -0.9950307607650757 +v 0.0276928897947073 0.04144534096121788 -0.9987569451332092 +v 0.023497188463807106 0.043960146605968475 0.9987569451332092 +v 0.04693595692515373 0.08781100064516068 0.9950307607650757 +v 0.07025803625583649 0.1314435452222824 0.9888308048248291 +v 0.09340544790029526 0.17474929988384247 0.9801725149154663 +v 0.11632063239812851 0.2176205962896347 0.9690772891044617 +v 0.13894662261009216 0.2599508464336395 0.955572783946991 +v 0.16122718155384064 0.30163484811782837 0.9396926164627075 +v 0.1831068992614746 0.34256890416145325 0.9214761853218079 +v 0.20453137159347534 0.38265129923820496 0.9009688496589661 +v 0.2254473716020584 0.42178234457969666 0.8782215714454651 +v 0.2458028495311737 0.4598647952079773 0.853290855884552 +v 0.2655472457408905 0.4968039393424988 0.826238751411438 +v 0.2846314311027527 0.5325079560279846 0.7971324920654297 +v 0.30300799012184143 0.5668880343437195 0.7660444378852844 +v 0.320631206035614 0.599858820438385 0.7330518960952759 +v 0.3374572992324829 0.6313381791114807 0.6982368230819702 +v 0.3534444272518158 0.661247968673706 0.6616858243942261 +v 0.3685528039932251 0.6895138025283813 0.6234897971153259 +v 0.3827449381351471 0.7160654067993164 0.583743691444397 +v 0.39598548412323 0.7408367395401001 0.542546272277832 +v 0.40824154019355774 0.7637662291526794 0.5 +v 0.41948267817497253 0.7847968935966492 0.4562106430530548 +v 0.42968088388442993 0.8038763999938965 0.4112870991230011 +v 0.4388108551502228 0.820957362651825 0.36534103751182556 +v 0.44684985280036926 0.8359972834587097 0.31848666071891785 +v 0.453777939081192 0.8489587903022766 0.27084046602249146 +v 0.4595778286457062 0.8598096370697021 0.22252093255519867 +v 0.4642351567745209 0.8685228824615479 0.1736481785774231 +v 0.4677383303642273 0.8750768303871155 0.12434370815753937 +v 0.47007861733436584 0.8794552087783813 0.07473009079694748 +v 0.4712502062320709 0.8816471695899963 0.024930691346526146 +v 0.4712502062320709 0.8816471695899963 -0.024930691346526146 +v 0.47007861733436584 0.8794552087783813 -0.07473009079694748 +v 0.4677383303642273 0.8750768303871155 -0.12434370815753937 +v 0.4642351567745209 0.8685228824615479 -0.1736481785774231 +v 0.4595778286457062 0.8598096370697021 -0.22252093255519867 +v 0.453777939081192 0.8489587903022766 -0.27084046602249146 +v 0.44684985280036926 0.8359972834587097 -0.31848666071891785 +v 0.4388108551502228 0.820957362651825 -0.36534103751182556 +v 0.42968088388442993 0.8038763999938965 -0.4112870991230011 +v 0.41948267817497253 0.7847968935966492 -0.4562106430530548 +v 0.40824154019355774 0.7637662291526794 -0.5 +v 0.39598548412323 0.7408367395401001 -0.542546272277832 +v 0.3827449381351471 0.7160654067993164 -0.583743691444397 +v 0.3685528039932251 0.6895138025283813 -0.6234897971153259 +v 0.3534444272518158 0.661247968673706 -0.6616858243942261 +v 0.3374572992324829 0.6313381791114807 -0.6982368230819702 +v 0.320631206035614 0.599858820438385 -0.7330518960952759 +v 0.30300799012184143 0.5668880343437195 -0.7660444378852844 +v 0.2846314311027527 0.5325079560279846 -0.7971324920654297 +v 0.2655472457408905 0.4968039393424988 -0.826238751411438 +v 0.2458028495311737 0.4598647952079773 -0.853290855884552 +v 0.2254473716020584 0.42178234457969666 -0.8782215714454651 +v 0.20453137159347534 0.38265129923820496 -0.9009688496589661 +v 0.1831068992614746 0.34256890416145325 -0.9214761853218079 +v 0.16122718155384064 0.30163484811782837 -0.9396926164627075 +v 0.13894662261009216 0.2599508464336395 -0.955572783946991 +v 0.11632063239812851 0.2176205962896347 -0.9690772891044617 +v 0.09340544790029526 0.17474929988384247 -0.9801725149154663 +v 0.07025803625583649 0.1314435452222824 -0.9888308048248291 +v 0.04693595692515373 0.08781100064516068 -0.9950307607650757 +v 0.023497188463807106 0.043960146605968475 -0.9987569451332092 +v 0.019075194373726845 0.046051595360040665 0.9987569451332092 +v 0.03810296580195427 0.0919886976480484 0.9950307607650757 +v 0.05703600496053696 0.13769710063934326 0.9888308048248291 +v 0.07582724839448929 0.18306316435337067 0.9801725149154663 +v 0.09442996978759766 0.22797410190105438 0.9690772891044617 +v 0.11279792338609695 0.2723182737827301 0.955572783946991 +v 0.13088543713092804 0.31598541140556335 0.9396926164627075 +v 0.14864756166934967 0.35886695981025696 0.9214761853218079 +v 0.16604012250900269 0.4008563160896301 0.9009688496589661 +v 0.18301987648010254 0.4418490529060364 0.8782215714454651 +v 0.19954460859298706 0.48174330592155457 0.853290855884552 +v 0.215573251247406 0.520439863204956 0.826238751411438 +v 0.23106594383716583 0.5578425526618958 0.7971324920654297 +v 0.24598416686058044 0.5938583016395569 0.7660444378852844 +v 0.26029083132743835 0.6283976435661316 0.7330518960952759 +v 0.27395036816596985 0.6613747477531433 0.6982368230819702 +v 0.28692883253097534 0.6927074790000916 0.6616858243942261 +v 0.29919394850730896 0.7223181128501892 0.6234897971153259 +v 0.3107152283191681 0.7501329183578491 0.583743691444397 +v 0.32146400213241577 0.7760827541351318 0.542546272277832 +v 0.3314135670661926 0.8001031279563904 0.5 +v 0.34053918719291687 0.8221343755722046 0.4562106430530548 +v 0.34881818294525146 0.8421216011047363 0.4112870991230011 +v 0.3562299609184265 0.8600152134895325 0.36534103751182556 +v 0.3627561032772064 0.8757706880569458 0.31848666071891785 +v 0.3683803379535675 0.8893488645553589 0.27084046602249146 +v 0.3730887472629547 0.9007159471511841 0.22252093255519867 +v 0.3768696188926697 0.9098437428474426 0.1736481785774231 +v 0.3797135055065155 0.9167094826698303 0.12434370815753937 +v 0.3816133737564087 0.9212961792945862 0.07473009079694748 +v 0.3825644850730896 0.9235923886299133 0.024930691346526146 +v 0.3825644850730896 0.9235923886299133 -0.024930691346526146 +v 0.3816133737564087 0.9212961792945862 -0.07473009079694748 +v 0.3797135055065155 0.9167094826698303 -0.12434370815753937 +v 0.3768696188926697 0.9098437428474426 -0.1736481785774231 +v 0.3730887472629547 0.9007159471511841 -0.22252093255519867 +v 0.3683803379535675 0.8893488645553589 -0.27084046602249146 +v 0.3627561032772064 0.8757706880569458 -0.31848666071891785 +v 0.3562299609184265 0.8600152134895325 -0.36534103751182556 +v 0.34881818294525146 0.8421216011047363 -0.4112870991230011 +v 0.34053918719291687 0.8221343755722046 -0.4562106430530548 +v 0.3314135670661926 0.8001031279563904 -0.5 +v 0.32146400213241577 0.7760827541351318 -0.542546272277832 +v 0.3107152283191681 0.7501329183578491 -0.583743691444397 +v 0.29919394850730896 0.7223181128501892 -0.6234897971153259 +v 0.28692883253097534 0.6927074790000916 -0.6616858243942261 +v 0.27395036816596985 0.6613747477531433 -0.6982368230819702 +v 0.26029083132743835 0.6283976435661316 -0.7330518960952759 +v 0.24598416686058044 0.5938583016395569 -0.7660444378852844 +v 0.23106594383716583 0.5578425526618958 -0.7971324920654297 +v 0.215573251247406 0.520439863204956 -0.826238751411438 +v 0.19954460859298706 0.48174330592155457 -0.853290855884552 +v 0.18301987648010254 0.4418490529060364 -0.8782215714454651 +v 0.16604012250900269 0.4008563160896301 -0.9009688496589661 +v 0.14864756166934967 0.35886695981025696 -0.9214761853218079 +v 0.13088543713092804 0.31598541140556335 -0.9396926164627075 +v 0.11279792338609695 0.2723182737827301 -0.955572783946991 +v 0.09442996978759766 0.22797410190105438 -0.9690772891044617 +v 0.07582724839448929 0.18306316435337067 -0.9801725149154663 +v 0.05703600496053696 0.13769710063934326 -0.9888308048248291 +v 0.03810296580195427 0.0919886976480484 -0.9950307607650757 +v 0.019075194373726845 0.046051595360040665 -0.9987569451332092 +v 0.01446949690580368 0.04769953712821007 0.9987569451332092 +v 0.028903020545840263 0.0952804908156395 0.9950307607650757 +v 0.043264687061309814 0.14262455701828003 0.9888308048248291 +v 0.05751878768205643 0.18961404263973236 0.9801725149154663 +v 0.07162988930940628 0.23613210022449493 0.9690772891044617 +v 0.08556290715932846 0.2820631265640259 0.955572783946991 +v 0.09928320348262787 0.32729285955429077 0.9396926164627075 +v 0.11275666952133179 0.3717089295387268 0.9214761853218079 +v 0.12594980001449585 0.4152008593082428 0.9009688496589661 +v 0.1388297975063324 0.4576605260372162 0.8782215714454651 +v 0.15136465430259705 0.49898236989974976 0.853290855884552 +v 0.16352318227291107 0.5390636920928955 0.826238751411438 +v 0.17527516186237335 0.5778048038482666 0.7971324920654297 +v 0.18659138679504395 0.615109384059906 0.7660444378852844 +v 0.19744372367858887 0.650884747505188 0.7330518960952759 +v 0.20780517160892487 0.6850418448448181 0.6982368230819702 +v 0.21764999628067017 0.717495858669281 0.6616858243942261 +v 0.22695370018482208 0.7481660842895508 0.6234897971153259 +v 0.23569315671920776 0.7769762277603149 0.583743691444397 +v 0.24384665489196777 0.8038547039031982 0.542546272277832 +v 0.2513939142227173 0.8287346363067627 0.5 +v 0.25831615924835205 0.8515542149543762 0.4562106430530548 +v 0.26459619402885437 0.8722566962242126 0.4112870991230011 +v 0.2702183723449707 0.8907906413078308 0.36534103751182556 +v 0.275168776512146 0.9071099162101746 0.31848666071891785 +v 0.27943506836891174 0.9211739897727966 0.27084046602249146 +v 0.2830066382884979 0.9329478144645691 0.22252093255519867 +v 0.285874605178833 0.9424022436141968 0.1736481785774231 +v 0.28803184628486633 0.9495137333869934 0.12434370815753937 +v 0.2894729971885681 0.9542645215988159 0.07473009079694748 +v 0.29019445180892944 0.9566429257392883 0.024930691346526146 +v 0.29019445180892944 0.9566429257392883 -0.024930691346526146 +v 0.2894729971885681 0.9542645215988159 -0.07473009079694748 +v 0.28803184628486633 0.9495137333869934 -0.12434370815753937 +v 0.285874605178833 0.9424022436141968 -0.1736481785774231 +v 0.2830066382884979 0.9329478144645691 -0.22252093255519867 +v 0.27943506836891174 0.9211739897727966 -0.27084046602249146 +v 0.275168776512146 0.9071099162101746 -0.31848666071891785 +v 0.2702183723449707 0.8907906413078308 -0.36534103751182556 +v 0.26459619402885437 0.8722566962242126 -0.4112870991230011 +v 0.25831615924835205 0.8515542149543762 -0.4562106430530548 +v 0.2513939142227173 0.8287346363067627 -0.5 +v 0.24384665489196777 0.8038547039031982 -0.542546272277832 +v 0.23569315671920776 0.7769762277603149 -0.583743691444397 +v 0.22695370018482208 0.7481660842895508 -0.6234897971153259 +v 0.21764999628067017 0.717495858669281 -0.6616858243942261 +v 0.20780517160892487 0.6850418448448181 -0.6982368230819702 +v 0.19744372367858887 0.650884747505188 -0.7330518960952759 +v 0.18659138679504395 0.615109384059906 -0.7660444378852844 +v 0.17527516186237335 0.5778048038482666 -0.7971324920654297 +v 0.16352318227291107 0.5390636920928955 -0.826238751411438 +v 0.15136465430259705 0.49898236989974976 -0.853290855884552 +v 0.1388297975063324 0.4576605260372162 -0.8782215714454651 +v 0.12594980001449585 0.4152008593082428 -0.9009688496589661 +v 0.11275666952133179 0.3717089295387268 -0.9214761853218079 +v 0.09928320348262787 0.32729285955429077 -0.9396926164627075 +v 0.08556290715932846 0.2820631265640259 -0.955572783946991 +v 0.07162988930940628 0.23613210022449493 -0.9690772891044617 +v 0.05751878768205643 0.18961404263973236 -0.9801725149154663 +v 0.043264687061309814 0.14262455701828003 -0.9888308048248291 +v 0.028903020545840263 0.0952804908156395 -0.9950307607650757 +v 0.01446949690580368 0.04769953712821007 -0.9987569451332092 +v 0.009724450297653675 0.048888109624385834 0.9987569451332092 +v 0.019424723461270332 0.09765467792749405 0.9950307607650757 +v 0.029076702892780304 0.1461784541606903 0.9888308048248291 +v 0.03865639492869377 0.1943388134241104 0.9801725149154663 +v 0.04813998192548752 0.2420160174369812 0.9690772891044617 +v 0.05750388279557228 0.2890915274620056 0.955572783946991 +v 0.06672482192516327 0.33544832468032837 0.9396926164627075 +v 0.07577987015247345 0.38097113370895386 0.9214761853218079 +v 0.08464651554822922 0.425546795129776 0.9009688496589661 +v 0.09330271929502487 0.46906447410583496 0.8782215714454651 +v 0.1017269641160965 0.511415958404541 0.853290855884552 +v 0.10989829152822495 0.5524960160255432 0.826238751411438 +v 0.1177963986992836 0.5922024846076965 0.7971324920654297 +v 0.12540164589881897 0.6304365992546082 0.7660444378852844 +v 0.13269512355327606 0.6671034097671509 0.7330518960952759 +v 0.13965868949890137 0.7021116614341736 0.6982368230819702 +v 0.14627505838871002 0.735374391078949 0.6616858243942261 +v 0.15252774953842163 0.766808807849884 0.6234897971153259 +v 0.1584012508392334 0.796336829662323 0.583743691444397 +v 0.16388092935085297 0.8238850831985474 0.542546272277832 +v 0.16895318031311035 0.8493849635124207 0.5 +v 0.17360538244247437 0.8727731704711914 0.4562106430530548 +v 0.17782597243785858 0.8939915299415588 0.4112870991230011 +v 0.18160445988178253 0.9129872918128967 0.36534103751182556 +v 0.1849314570426941 0.9297131896018982 0.31848666071891785 +v 0.18779867887496948 0.9441276788711548 0.27084046602249146 +v 0.19019900262355804 0.9561949372291565 0.22252093255519867 +v 0.19212646782398224 0.9658849239349365 0.1736481785774231 +v 0.19357626140117645 0.9731736183166504 0.12434370815753937 +v 0.19454480707645416 0.9780427813529968 0.07473009079694748 +v 0.19502969086170197 0.980480432510376 0.024930691346526146 +v 0.19502969086170197 0.980480432510376 -0.024930691346526146 +v 0.19454480707645416 0.9780427813529968 -0.07473009079694748 +v 0.19357626140117645 0.9731736183166504 -0.12434370815753937 +v 0.19212646782398224 0.9658849239349365 -0.1736481785774231 +v 0.19019900262355804 0.9561949372291565 -0.22252093255519867 +v 0.18779867887496948 0.9441276788711548 -0.27084046602249146 +v 0.1849314570426941 0.9297131896018982 -0.31848666071891785 +v 0.18160445988178253 0.9129872918128967 -0.36534103751182556 +v 0.17782597243785858 0.8939915299415588 -0.4112870991230011 +v 0.17360538244247437 0.8727731704711914 -0.4562106430530548 +v 0.16895318031311035 0.8493849635124207 -0.5 +v 0.16388092935085297 0.8238850831985474 -0.542546272277832 +v 0.1584012508392334 0.796336829662323 -0.583743691444397 +v 0.15252774953842163 0.766808807849884 -0.6234897971153259 +v 0.14627505838871002 0.735374391078949 -0.6616858243942261 +v 0.13965868949890137 0.7021116614341736 -0.6982368230819702 +v 0.13269512355327606 0.6671034097671509 -0.7330518960952759 +v 0.12540164589881897 0.6304365992546082 -0.7660444378852844 +v 0.1177963986992836 0.5922024846076965 -0.7971324920654297 +v 0.10989829152822495 0.5524960160255432 -0.826238751411438 +v 0.1017269641160965 0.511415958404541 -0.853290855884552 +v 0.09330271929502487 0.46906447410583496 -0.8782215714454651 +v 0.08464651554822922 0.425546795129776 -0.9009688496589661 +v 0.07577987015247345 0.38097113370895386 -0.9214761853218079 +v 0.06672482192516327 0.33544832468032837 -0.9396926164627075 +v 0.05750388279557228 0.2890915274620056 -0.955572783946991 +v 0.04813998192548752 0.2420160174369812 -0.9690772891044617 +v 0.03865639492869377 0.1943388134241104 -0.9801725149154663 +v 0.029076702892780304 0.1461784541606903 -0.9888308048248291 +v 0.019424723461270332 0.09765467792749405 -0.9950307607650757 +v 0.009724450297653675 0.048888109624385834 -0.9987569451332092 +v 0.004885751288384199 0.04960586503148079 0.9987569451332092 +v 0.009759355336427689 0.0990884006023407 0.9950307607650757 +v 0.014608697034418583 0.14832459390163422 0.9888308048248291 +v 0.019421719014644623 0.1971920132637024 0.9801725149154663 +v 0.024186454713344574 0.24556919932365417 0.9690772891044617 +v 0.028891058638691902 0.29333585500717163 0.955572783946991 +v 0.033523835241794586 0.3403732180595398 0.9396926164627075 +v 0.03807326778769493 0.38656437397003174 0.9214761853218079 +v 0.04252804443240166 0.4317944645881653 0.9009688496589661 +v 0.04687708616256714 0.47595104575157166 0.8782215714454651 +v 0.05110958591103554 0.518924355506897 0.853290855884552 +v 0.0552150197327137 0.5606074929237366 0.826238751411438 +v 0.05918318033218384 0.600896954536438 0.7971324920654297 +v 0.06300420314073563 0.6396924257278442 0.7660444378852844 +v 0.06666858494281769 0.6768975257873535 0.7330518960952759 +v 0.07016722112894058 0.7124197483062744 0.6982368230819702 +v 0.0734914094209671 0.7461708188056946 0.6616858243942261 +v 0.07663288712501526 0.7780667543411255 0.6234897971153259 +v 0.07958383858203888 0.8080282807350159 0.583743691444397 +v 0.08233693987131119 0.8359809517860413 0.542546272277832 +v 0.08488533645868301 0.8618552684783936 0.5 +v 0.08722268790006638 0.8855868577957153 0.4562106430530548 +v 0.08934319764375687 0.907116711139679 0.4112870991230011 +v 0.09124158322811127 0.9263913631439209 0.36534103751182556 +v 0.09291312843561172 0.943362832069397 0.31848666071891785 +v 0.09435367584228516 0.957988977432251 0.27084046602249146 +v 0.09555964916944504 0.9702333807945251 0.22252093255519867 +v 0.09652803838253021 0.980065643787384 0.1736481785774231 +v 0.09725645184516907 0.9874613285064697 0.12434370815753937 +v 0.09774306416511536 0.9924020171165466 0.07473009079694748 +v 0.09798667579889297 0.994875431060791 0.024930691346526146 +v 0.09798667579889297 0.994875431060791 -0.024930691346526146 +v 0.09774306416511536 0.9924020171165466 -0.07473009079694748 +v 0.09725645184516907 0.9874613285064697 -0.12434370815753937 +v 0.09652803838253021 0.980065643787384 -0.1736481785774231 +v 0.09555964916944504 0.9702333807945251 -0.22252093255519867 +v 0.09435367584228516 0.957988977432251 -0.27084046602249146 +v 0.09291312843561172 0.943362832069397 -0.31848666071891785 +v 0.09124158322811127 0.9263913631439209 -0.36534103751182556 +v 0.08934319764375687 0.907116711139679 -0.4112870991230011 +v 0.08722268790006638 0.8855868577957153 -0.4562106430530548 +v 0.08488533645868301 0.8618552684783936 -0.5 +v 0.08233693987131119 0.8359809517860413 -0.542546272277832 +v 0.07958383858203888 0.8080282807350159 -0.583743691444397 +v 0.07663288712501526 0.7780667543411255 -0.6234897971153259 +v 0.0734914094209671 0.7461708188056946 -0.6616858243942261 +v 0.07016722112894058 0.7124197483062744 -0.6982368230819702 +v 0.06666858494281769 0.6768975257873535 -0.7330518960952759 +v 0.06300420314073563 0.6396924257278442 -0.7660444378852844 +v 0.05918318033218384 0.600896954536438 -0.7971324920654297 +v 0.0552150197327137 0.5606074929237366 -0.826238751411438 +v 0.05110958591103554 0.518924355506897 -0.853290855884552 +v 0.04687708616256714 0.47595104575157166 -0.8782215714454651 +v 0.04252804443240166 0.4317944645881653 -0.9009688496589661 +v 0.03807326778769493 0.38656437397003174 -0.9214761853218079 +v 0.033523835241794586 0.3403732180595398 -0.9396926164627075 +v 0.028891058638691902 0.29333585500717163 -0.955572783946991 +v 0.024186454713344574 0.24556919932365417 -0.9690772891044617 +v 0.019421719014644623 0.1971920132637024 -0.9801725149154663 +v 0.014608697034418583 0.14832459390163422 -0.9888308048248291 +v 0.009759355336427689 0.0990884006023407 -0.9950307607650757 +v 0.004885751288384199 0.04960586503148079 -0.9987569451332092 +v 3.0521802414446084e-18 0.04984588548541069 0.9987569451332092 +v 6.096772341539962e-18 0.09956784546375275 0.9950307607650757 +v 9.126206770500587e-18 0.1490422636270523 0.9888308048248291 +v 1.2132952048849189e-17 0.19814614951610565 0.9801725149154663 +v 1.510953253179982e-17 0.2467574030160904 0.9690772891044617 +v 1.804854826159258e-17 0.29475516080856323 0.955572783946991 +v 2.0942694406238013e-17 0.3420201539993286 0.9396926164627075 +v 2.378477118568446e-17 0.3884347975254059 0.9214761853218079 +v 2.656771613186194e-17 0.4338837265968323 0.9009688496589661 +v 2.9284609051765835e-17 0.4782539904117584 0.8782215714454651 +v 3.192869849723649e-17 0.5214352011680603 0.853290855884552 +v 3.449340507368165e-17 0.5633200407028198 0.826238751411438 +v 3.697235618166222e-17 0.6038044095039368 0.7971324920654297 +v 3.9359390979975894e-17 0.6427876353263855 0.7660444378852844 +v 4.1648567003102095e-17 0.6801727414131165 0.7330518960952759 +v 4.383420317459381e-17 0.7158668637275696 0.6982368230819702 +v 4.5910856646020435e-17 0.7497811913490295 0.6616858243942261 +v 4.7873372427804546e-17 0.7818315029144287 0.6234897971153259 +v 4.9716863536887175e-17 0.8119379878044128 0.583743691444397 +v 5.143675401011968e-17 0.8400259017944336 0.542546272277832 +v 5.302876236065149e-17 0.8660253882408142 0.5 +v 5.4488934665154596e-17 0.8898718357086182 0.4562106430530548 +v 5.581363463765622e-17 0.9115058779716492 0.4112870991230011 +v 5.699958002548575e-17 0.9308737516403198 0.36534103751182556 +v 5.804380621332779e-17 0.9479273557662964 0.31848666071891785 +v 5.894373239767118e-17 0.9626242518424988 0.27084046602249146 +v 5.969711526469467e-17 0.9749279022216797 0.22252093255519867 +v 6.030208207749146e-17 0.9848077297210693 0.1736481785774231 +v 6.075713067606913e-17 0.9922391772270203 0.12434370815753937 +v 6.106112285990482e-17 0.9972038269042969 0.07473009079694748 +v 6.121331085772477e-17 0.9996891617774963 0.024930691346526146 +v 6.121331085772477e-17 0.9996891617774963 -0.024930691346526146 +v 6.106112285990482e-17 0.9972038269042969 -0.07473009079694748 +v 6.075713067606913e-17 0.9922391772270203 -0.12434370815753937 +v 6.030208207749146e-17 0.9848077297210693 -0.1736481785774231 +v 5.969711526469467e-17 0.9749279022216797 -0.22252093255519867 +v 5.894373239767118e-17 0.9626242518424988 -0.27084046602249146 +v 5.804380621332779e-17 0.9479273557662964 -0.31848666071891785 +v 5.699958002548575e-17 0.9308737516403198 -0.36534103751182556 +v 5.581363463765622e-17 0.9115058779716492 -0.4112870991230011 +v 5.4488934665154596e-17 0.8898718357086182 -0.4562106430530548 +v 5.302876236065149e-17 0.8660253882408142 -0.5 +v 5.143675401011968e-17 0.8400259017944336 -0.542546272277832 +v 4.9716863536887175e-17 0.8119379878044128 -0.583743691444397 +v 4.7873372427804546e-17 0.7818315029144287 -0.6234897971153259 +v 4.5910856646020435e-17 0.7497811913490295 -0.6616858243942261 +v 4.383420317459381e-17 0.7158668637275696 -0.6982368230819702 +v 4.1648567003102095e-17 0.6801727414131165 -0.7330518960952759 +v 3.9359390979975894e-17 0.6427876353263855 -0.7660444378852844 +v 3.697235618166222e-17 0.6038044095039368 -0.7971324920654297 +v 3.449340507368165e-17 0.5633200407028198 -0.826238751411438 +v 3.192869849723649e-17 0.5214352011680603 -0.853290855884552 +v 2.9284609051765835e-17 0.4782539904117584 -0.8782215714454651 +v 2.656771613186194e-17 0.4338837265968323 -0.9009688496589661 +v 2.378477118568446e-17 0.3884347975254059 -0.9214761853218079 +v 2.0942694406238013e-17 0.3420201539993286 -0.9396926164627075 +v 1.804854826159258e-17 0.29475516080856323 -0.955572783946991 +v 1.510953253179982e-17 0.2467574030160904 -0.9690772891044617 +v 1.2132952048849189e-17 0.19814614951610565 -0.9801725149154663 +v 9.126206770500587e-18 0.1490422636270523 -0.9888308048248291 +v 6.096772341539962e-18 0.09956784546375275 -0.9950307607650757 +v 3.0521802414446084e-18 0.04984588548541069 -0.9987569451332092 +v -0.004885751288384199 0.04960586503148079 0.9987569451332092 +v -0.009759355336427689 0.0990884006023407 0.9950307607650757 +v -0.014608697034418583 0.14832459390163422 0.9888308048248291 +v -0.019421719014644623 0.1971920132637024 0.9801725149154663 +v -0.024186454713344574 0.24556919932365417 0.9690772891044617 +v -0.028891058638691902 0.29333585500717163 0.955572783946991 +v -0.033523835241794586 0.3403732180595398 0.9396926164627075 +v -0.03807326778769493 0.38656437397003174 0.9214761853218079 +v -0.04252804443240166 0.4317944645881653 0.9009688496589661 +v -0.04687708616256714 0.47595104575157166 0.8782215714454651 +v -0.05110958591103554 0.518924355506897 0.853290855884552 +v -0.0552150197327137 0.5606074929237366 0.826238751411438 +v -0.05918318033218384 0.600896954536438 0.7971324920654297 +v -0.06300420314073563 0.6396924257278442 0.7660444378852844 +v -0.06666858494281769 0.6768975257873535 0.7330518960952759 +v -0.07016722112894058 0.7124197483062744 0.6982368230819702 +v -0.0734914094209671 0.7461708188056946 0.6616858243942261 +v -0.07663288712501526 0.7780667543411255 0.6234897971153259 +v -0.07958383858203888 0.8080282807350159 0.583743691444397 +v -0.08233693987131119 0.8359809517860413 0.542546272277832 +v -0.08488533645868301 0.8618552684783936 0.5 +v -0.08722268790006638 0.8855868577957153 0.4562106430530548 +v -0.08934319764375687 0.907116711139679 0.4112870991230011 +v -0.09124158322811127 0.9263913631439209 0.36534103751182556 +v -0.09291312843561172 0.943362832069397 0.31848666071891785 +v -0.09435367584228516 0.957988977432251 0.27084046602249146 +v -0.09555964916944504 0.9702333807945251 0.22252093255519867 +v -0.09652803838253021 0.980065643787384 0.1736481785774231 +v -0.09725645184516907 0.9874613285064697 0.12434370815753937 +v -0.09774306416511536 0.9924020171165466 0.07473009079694748 +v -0.09798667579889297 0.994875431060791 0.024930691346526146 +v -0.09798667579889297 0.994875431060791 -0.024930691346526146 +v -0.09774306416511536 0.9924020171165466 -0.07473009079694748 +v -0.09725645184516907 0.9874613285064697 -0.12434370815753937 +v -0.09652803838253021 0.980065643787384 -0.1736481785774231 +v -0.09555964916944504 0.9702333807945251 -0.22252093255519867 +v -0.09435367584228516 0.957988977432251 -0.27084046602249146 +v -0.09291312843561172 0.943362832069397 -0.31848666071891785 +v -0.09124158322811127 0.9263913631439209 -0.36534103751182556 +v -0.08934319764375687 0.907116711139679 -0.4112870991230011 +v -0.08722268790006638 0.8855868577957153 -0.4562106430530548 +v -0.08488533645868301 0.8618552684783936 -0.5 +v -0.08233693987131119 0.8359809517860413 -0.542546272277832 +v -0.07958383858203888 0.8080282807350159 -0.583743691444397 +v -0.07663288712501526 0.7780667543411255 -0.6234897971153259 +v -0.0734914094209671 0.7461708188056946 -0.6616858243942261 +v -0.07016722112894058 0.7124197483062744 -0.6982368230819702 +v -0.06666858494281769 0.6768975257873535 -0.7330518960952759 +v -0.06300420314073563 0.6396924257278442 -0.7660444378852844 +v -0.05918318033218384 0.600896954536438 -0.7971324920654297 +v -0.0552150197327137 0.5606074929237366 -0.826238751411438 +v -0.05110958591103554 0.518924355506897 -0.853290855884552 +v -0.04687708616256714 0.47595104575157166 -0.8782215714454651 +v -0.04252804443240166 0.4317944645881653 -0.9009688496589661 +v -0.03807326778769493 0.38656437397003174 -0.9214761853218079 +v -0.033523835241794586 0.3403732180595398 -0.9396926164627075 +v -0.028891058638691902 0.29333585500717163 -0.955572783946991 +v -0.024186454713344574 0.24556919932365417 -0.9690772891044617 +v -0.019421719014644623 0.1971920132637024 -0.9801725149154663 +v -0.014608697034418583 0.14832459390163422 -0.9888308048248291 +v -0.009759355336427689 0.0990884006023407 -0.9950307607650757 +v -0.004885751288384199 0.04960586503148079 -0.9987569451332092 +v -0.009724450297653675 0.048888109624385834 0.9987569451332092 +v -0.019424723461270332 0.09765467792749405 0.9950307607650757 +v -0.029076702892780304 0.1461784541606903 0.9888308048248291 +v -0.03865639492869377 0.1943388134241104 0.9801725149154663 +v -0.04813998192548752 0.2420160174369812 0.9690772891044617 +v -0.05750388279557228 0.2890915274620056 0.955572783946991 +v -0.06672482192516327 0.33544832468032837 0.9396926164627075 +v -0.07577987015247345 0.38097113370895386 0.9214761853218079 +v -0.08464651554822922 0.425546795129776 0.9009688496589661 +v -0.09330271929502487 0.46906447410583496 0.8782215714454651 +v -0.1017269641160965 0.511415958404541 0.853290855884552 +v -0.10989829152822495 0.5524960160255432 0.826238751411438 +v -0.1177963986992836 0.5922024846076965 0.7971324920654297 +v -0.12540164589881897 0.6304365992546082 0.7660444378852844 +v -0.13269512355327606 0.6671034097671509 0.7330518960952759 +v -0.13965868949890137 0.7021116614341736 0.6982368230819702 +v -0.14627505838871002 0.735374391078949 0.6616858243942261 +v -0.15252774953842163 0.766808807849884 0.6234897971153259 +v -0.1584012508392334 0.796336829662323 0.583743691444397 +v -0.16388092935085297 0.8238850831985474 0.542546272277832 +v -0.16895318031311035 0.8493849635124207 0.5 +v -0.17360538244247437 0.8727731704711914 0.4562106430530548 +v -0.17782597243785858 0.8939915299415588 0.4112870991230011 +v -0.18160445988178253 0.9129872918128967 0.36534103751182556 +v -0.1849314570426941 0.9297131896018982 0.31848666071891785 +v -0.18779867887496948 0.9441276788711548 0.27084046602249146 +v -0.19019900262355804 0.9561949372291565 0.22252093255519867 +v -0.19212646782398224 0.9658849239349365 0.1736481785774231 +v -0.19357626140117645 0.9731736183166504 0.12434370815753937 +v -0.19454480707645416 0.9780427813529968 0.07473009079694748 +v -0.19502969086170197 0.980480432510376 0.024930691346526146 +v -0.19502969086170197 0.980480432510376 -0.024930691346526146 +v -0.19454480707645416 0.9780427813529968 -0.07473009079694748 +v -0.19357626140117645 0.9731736183166504 -0.12434370815753937 +v -0.19212646782398224 0.9658849239349365 -0.1736481785774231 +v -0.19019900262355804 0.9561949372291565 -0.22252093255519867 +v -0.18779867887496948 0.9441276788711548 -0.27084046602249146 +v -0.1849314570426941 0.9297131896018982 -0.31848666071891785 +v -0.18160445988178253 0.9129872918128967 -0.36534103751182556 +v -0.17782597243785858 0.8939915299415588 -0.4112870991230011 +v -0.17360538244247437 0.8727731704711914 -0.4562106430530548 +v -0.16895318031311035 0.8493849635124207 -0.5 +v -0.16388092935085297 0.8238850831985474 -0.542546272277832 +v -0.1584012508392334 0.796336829662323 -0.583743691444397 +v -0.15252774953842163 0.766808807849884 -0.6234897971153259 +v -0.14627505838871002 0.735374391078949 -0.6616858243942261 +v -0.13965868949890137 0.7021116614341736 -0.6982368230819702 +v -0.13269512355327606 0.6671034097671509 -0.7330518960952759 +v -0.12540164589881897 0.6304365992546082 -0.7660444378852844 +v -0.1177963986992836 0.5922024846076965 -0.7971324920654297 +v -0.10989829152822495 0.5524960160255432 -0.826238751411438 +v -0.1017269641160965 0.511415958404541 -0.853290855884552 +v -0.09330271929502487 0.46906447410583496 -0.8782215714454651 +v -0.08464651554822922 0.425546795129776 -0.9009688496589661 +v -0.07577987015247345 0.38097113370895386 -0.9214761853218079 +v -0.06672482192516327 0.33544832468032837 -0.9396926164627075 +v -0.05750388279557228 0.2890915274620056 -0.955572783946991 +v -0.04813998192548752 0.2420160174369812 -0.9690772891044617 +v -0.03865639492869377 0.1943388134241104 -0.9801725149154663 +v -0.029076702892780304 0.1461784541606903 -0.9888308048248291 +v -0.019424723461270332 0.09765467792749405 -0.9950307607650757 +v -0.009724450297653675 0.048888109624385834 -0.9987569451332092 +v -0.01446949690580368 0.04769953712821007 0.9987569451332092 +v -0.028903020545840263 0.0952804908156395 0.9950307607650757 +v -0.043264687061309814 0.14262455701828003 0.9888308048248291 +v -0.05751878768205643 0.18961404263973236 0.9801725149154663 +v -0.07162988930940628 0.23613210022449493 0.9690772891044617 +v -0.08556290715932846 0.2820631265640259 0.955572783946991 +v -0.09928320348262787 0.32729285955429077 0.9396926164627075 +v -0.11275666952133179 0.3717089295387268 0.9214761853218079 +v -0.12594980001449585 0.4152008593082428 0.9009688496589661 +v -0.1388297975063324 0.4576605260372162 0.8782215714454651 +v -0.15136465430259705 0.49898236989974976 0.853290855884552 +v -0.16352318227291107 0.5390636920928955 0.826238751411438 +v -0.17527516186237335 0.5778048038482666 0.7971324920654297 +v -0.18659138679504395 0.615109384059906 0.7660444378852844 +v -0.19744372367858887 0.650884747505188 0.7330518960952759 +v -0.20780517160892487 0.6850418448448181 0.6982368230819702 +v -0.21764999628067017 0.717495858669281 0.6616858243942261 +v -0.22695370018482208 0.7481660842895508 0.6234897971153259 +v -0.23569315671920776 0.7769762277603149 0.583743691444397 +v -0.24384665489196777 0.8038547039031982 0.542546272277832 +v -0.2513939142227173 0.8287346363067627 0.5 +v -0.25831615924835205 0.8515542149543762 0.4562106430530548 +v -0.26459619402885437 0.8722566962242126 0.4112870991230011 +v -0.2702183723449707 0.8907906413078308 0.36534103751182556 +v -0.275168776512146 0.9071099162101746 0.31848666071891785 +v -0.27943506836891174 0.9211739897727966 0.27084046602249146 +v -0.2830066382884979 0.9329478144645691 0.22252093255519867 +v -0.285874605178833 0.9424022436141968 0.1736481785774231 +v -0.28803184628486633 0.9495137333869934 0.12434370815753937 +v -0.2894729971885681 0.9542645215988159 0.07473009079694748 +v -0.29019445180892944 0.9566429257392883 0.024930691346526146 +v -0.29019445180892944 0.9566429257392883 -0.024930691346526146 +v -0.2894729971885681 0.9542645215988159 -0.07473009079694748 +v -0.28803184628486633 0.9495137333869934 -0.12434370815753937 +v -0.285874605178833 0.9424022436141968 -0.1736481785774231 +v -0.2830066382884979 0.9329478144645691 -0.22252093255519867 +v -0.27943506836891174 0.9211739897727966 -0.27084046602249146 +v -0.275168776512146 0.9071099162101746 -0.31848666071891785 +v -0.2702183723449707 0.8907906413078308 -0.36534103751182556 +v -0.26459619402885437 0.8722566962242126 -0.4112870991230011 +v -0.25831615924835205 0.8515542149543762 -0.4562106430530548 +v -0.2513939142227173 0.8287346363067627 -0.5 +v -0.24384665489196777 0.8038547039031982 -0.542546272277832 +v -0.23569315671920776 0.7769762277603149 -0.583743691444397 +v -0.22695370018482208 0.7481660842895508 -0.6234897971153259 +v -0.21764999628067017 0.717495858669281 -0.6616858243942261 +v -0.20780517160892487 0.6850418448448181 -0.6982368230819702 +v -0.19744372367858887 0.650884747505188 -0.7330518960952759 +v -0.18659138679504395 0.615109384059906 -0.7660444378852844 +v -0.17527516186237335 0.5778048038482666 -0.7971324920654297 +v -0.16352318227291107 0.5390636920928955 -0.826238751411438 +v -0.15136465430259705 0.49898236989974976 -0.853290855884552 +v -0.1388297975063324 0.4576605260372162 -0.8782215714454651 +v -0.12594980001449585 0.4152008593082428 -0.9009688496589661 +v -0.11275666952133179 0.3717089295387268 -0.9214761853218079 +v -0.09928320348262787 0.32729285955429077 -0.9396926164627075 +v -0.08556290715932846 0.2820631265640259 -0.955572783946991 +v -0.07162988930940628 0.23613210022449493 -0.9690772891044617 +v -0.05751878768205643 0.18961404263973236 -0.9801725149154663 +v -0.043264687061309814 0.14262455701828003 -0.9888308048248291 +v -0.028903020545840263 0.0952804908156395 -0.9950307607650757 +v -0.01446949690580368 0.04769953712821007 -0.9987569451332092 +v -0.019075194373726845 0.046051595360040665 0.9987569451332092 +v -0.03810296580195427 0.0919886976480484 0.9950307607650757 +v -0.05703600496053696 0.13769710063934326 0.9888308048248291 +v -0.07582724839448929 0.18306316435337067 0.9801725149154663 +v -0.09442996978759766 0.22797410190105438 0.9690772891044617 +v -0.11279792338609695 0.2723182737827301 0.955572783946991 +v -0.13088543713092804 0.31598541140556335 0.9396926164627075 +v -0.14864756166934967 0.35886695981025696 0.9214761853218079 +v -0.16604012250900269 0.4008563160896301 0.9009688496589661 +v -0.18301987648010254 0.4418490529060364 0.8782215714454651 +v -0.19954460859298706 0.48174330592155457 0.853290855884552 +v -0.215573251247406 0.520439863204956 0.826238751411438 +v -0.23106594383716583 0.5578425526618958 0.7971324920654297 +v -0.24598416686058044 0.5938583016395569 0.7660444378852844 +v -0.26029083132743835 0.6283976435661316 0.7330518960952759 +v -0.27395036816596985 0.6613747477531433 0.6982368230819702 +v -0.28692883253097534 0.6927074790000916 0.6616858243942261 +v -0.29919394850730896 0.7223181128501892 0.6234897971153259 +v -0.3107152283191681 0.7501329183578491 0.583743691444397 +v -0.32146400213241577 0.7760827541351318 0.542546272277832 +v -0.3314135670661926 0.8001031279563904 0.5 +v -0.34053918719291687 0.8221343755722046 0.4562106430530548 +v -0.34881818294525146 0.8421216011047363 0.4112870991230011 +v -0.3562299609184265 0.8600152134895325 0.36534103751182556 +v -0.3627561032772064 0.8757706880569458 0.31848666071891785 +v -0.3683803379535675 0.8893488645553589 0.27084046602249146 +v -0.3730887472629547 0.9007159471511841 0.22252093255519867 +v -0.3768696188926697 0.9098437428474426 0.1736481785774231 +v -0.3797135055065155 0.9167094826698303 0.12434370815753937 +v -0.3816133737564087 0.9212961792945862 0.07473009079694748 +v -0.3825644850730896 0.9235923886299133 0.024930691346526146 +v -0.3825644850730896 0.9235923886299133 -0.024930691346526146 +v -0.3816133737564087 0.9212961792945862 -0.07473009079694748 +v -0.3797135055065155 0.9167094826698303 -0.12434370815753937 +v -0.3768696188926697 0.9098437428474426 -0.1736481785774231 +v -0.3730887472629547 0.9007159471511841 -0.22252093255519867 +v -0.3683803379535675 0.8893488645553589 -0.27084046602249146 +v -0.3627561032772064 0.8757706880569458 -0.31848666071891785 +v -0.3562299609184265 0.8600152134895325 -0.36534103751182556 +v -0.34881818294525146 0.8421216011047363 -0.4112870991230011 +v -0.34053918719291687 0.8221343755722046 -0.4562106430530548 +v -0.3314135670661926 0.8001031279563904 -0.5 +v -0.32146400213241577 0.7760827541351318 -0.542546272277832 +v -0.3107152283191681 0.7501329183578491 -0.583743691444397 +v -0.29919394850730896 0.7223181128501892 -0.6234897971153259 +v -0.28692883253097534 0.6927074790000916 -0.6616858243942261 +v -0.27395036816596985 0.6613747477531433 -0.6982368230819702 +v -0.26029083132743835 0.6283976435661316 -0.7330518960952759 +v -0.24598416686058044 0.5938583016395569 -0.7660444378852844 +v -0.23106594383716583 0.5578425526618958 -0.7971324920654297 +v -0.215573251247406 0.520439863204956 -0.826238751411438 +v -0.19954460859298706 0.48174330592155457 -0.853290855884552 +v -0.18301987648010254 0.4418490529060364 -0.8782215714454651 +v -0.16604012250900269 0.4008563160896301 -0.9009688496589661 +v -0.14864756166934967 0.35886695981025696 -0.9214761853218079 +v -0.13088543713092804 0.31598541140556335 -0.9396926164627075 +v -0.11279792338609695 0.2723182737827301 -0.955572783946991 +v -0.09442996978759766 0.22797410190105438 -0.9690772891044617 +v -0.07582724839448929 0.18306316435337067 -0.9801725149154663 +v -0.05703600496053696 0.13769710063934326 -0.9888308048248291 +v -0.03810296580195427 0.0919886976480484 -0.9950307607650757 +v -0.019075194373726845 0.046051595360040665 -0.9987569451332092 +v -0.023497188463807106 0.043960146605968475 0.9987569451332092 +v -0.04693595692515373 0.08781100064516068 0.9950307607650757 +v -0.07025803625583649 0.1314435452222824 0.9888308048248291 +v -0.09340544790029526 0.17474929988384247 0.9801725149154663 +v -0.11632063239812851 0.2176205962896347 0.9690772891044617 +v -0.13894662261009216 0.2599508464336395 0.955572783946991 +v -0.16122718155384064 0.30163484811782837 0.9396926164627075 +v -0.1831068992614746 0.34256890416145325 0.9214761853218079 +v -0.20453137159347534 0.38265129923820496 0.9009688496589661 +v -0.2254473716020584 0.42178234457969666 0.8782215714454651 +v -0.2458028495311737 0.4598647952079773 0.853290855884552 +v -0.2655472457408905 0.4968039393424988 0.826238751411438 +v -0.2846314311027527 0.5325079560279846 0.7971324920654297 +v -0.30300799012184143 0.5668880343437195 0.7660444378852844 +v -0.320631206035614 0.599858820438385 0.7330518960952759 +v -0.3374572992324829 0.6313381791114807 0.6982368230819702 +v -0.3534444272518158 0.661247968673706 0.6616858243942261 +v -0.3685528039932251 0.6895138025283813 0.6234897971153259 +v -0.3827449381351471 0.7160654067993164 0.583743691444397 +v -0.39598548412323 0.7408367395401001 0.542546272277832 +v -0.40824154019355774 0.7637662291526794 0.5 +v -0.41948267817497253 0.7847968935966492 0.4562106430530548 +v -0.42968088388442993 0.8038763999938965 0.4112870991230011 +v -0.4388108551502228 0.820957362651825 0.36534103751182556 +v -0.44684985280036926 0.8359972834587097 0.31848666071891785 +v -0.453777939081192 0.8489587903022766 0.27084046602249146 +v -0.4595778286457062 0.8598096370697021 0.22252093255519867 +v -0.4642351567745209 0.8685228824615479 0.1736481785774231 +v -0.4677383303642273 0.8750768303871155 0.12434370815753937 +v -0.47007861733436584 0.8794552087783813 0.07473009079694748 +v -0.4712502062320709 0.8816471695899963 0.024930691346526146 +v -0.4712502062320709 0.8816471695899963 -0.024930691346526146 +v -0.47007861733436584 0.8794552087783813 -0.07473009079694748 +v -0.4677383303642273 0.8750768303871155 -0.12434370815753937 +v -0.4642351567745209 0.8685228824615479 -0.1736481785774231 +v -0.4595778286457062 0.8598096370697021 -0.22252093255519867 +v -0.453777939081192 0.8489587903022766 -0.27084046602249146 +v -0.44684985280036926 0.8359972834587097 -0.31848666071891785 +v -0.4388108551502228 0.820957362651825 -0.36534103751182556 +v -0.42968088388442993 0.8038763999938965 -0.4112870991230011 +v -0.41948267817497253 0.7847968935966492 -0.4562106430530548 +v -0.40824154019355774 0.7637662291526794 -0.5 +v -0.39598548412323 0.7408367395401001 -0.542546272277832 +v -0.3827449381351471 0.7160654067993164 -0.583743691444397 +v -0.3685528039932251 0.6895138025283813 -0.6234897971153259 +v -0.3534444272518158 0.661247968673706 -0.6616858243942261 +v -0.3374572992324829 0.6313381791114807 -0.6982368230819702 +v -0.320631206035614 0.599858820438385 -0.7330518960952759 +v -0.30300799012184143 0.5668880343437195 -0.7660444378852844 +v -0.2846314311027527 0.5325079560279846 -0.7971324920654297 +v -0.2655472457408905 0.4968039393424988 -0.826238751411438 +v -0.2458028495311737 0.4598647952079773 -0.853290855884552 +v -0.2254473716020584 0.42178234457969666 -0.8782215714454651 +v -0.20453137159347534 0.38265129923820496 -0.9009688496589661 +v -0.1831068992614746 0.34256890416145325 -0.9214761853218079 +v -0.16122718155384064 0.30163484811782837 -0.9396926164627075 +v -0.13894662261009216 0.2599508464336395 -0.955572783946991 +v -0.11632063239812851 0.2176205962896347 -0.9690772891044617 +v -0.09340544790029526 0.17474929988384247 -0.9801725149154663 +v -0.07025803625583649 0.1314435452222824 -0.9888308048248291 +v -0.04693595692515373 0.08781100064516068 -0.9950307607650757 +v -0.023497188463807106 0.043960146605968475 -0.9987569451332092 +v -0.0276928897947073 0.04144534096121788 0.9987569451332092 +v -0.05531693249940872 0.0827876403927803 0.9950307607650757 +v -0.08280344307422638 0.12392411381006241 0.9888308048248291 +v -0.11008410155773163 0.16475249826908112 0.9801725149154663 +v -0.13709107041358948 0.20517127215862274 0.9690772891044617 +v -0.16375720500946045 0.24507996439933777 0.955572783946991 +v -0.19001621007919312 0.28437936305999756 0.9396926164627075 +v -0.21580280363559723 0.32297173142433167 0.9214761853218079 +v -0.24105289578437805 0.3607611358165741 0.9009688496589661 +v -0.2657036781311035 0.39765363931655884 0.8782215714454651 +v -0.2896938920021057 0.43355754017829895 0.853290855884552 +v -0.3129638433456421 0.4683835208415985 0.826238751411438 +v -0.3354557454586029 0.5020450353622437 0.7971324920654297 +v -0.35711365938186646 0.534458339214325 0.7660444378852844 +v -0.3778837323188782 0.5655429363250732 0.7330518960952759 +v -0.3977143168449402 0.5952215194702148 0.6982368230819702 +v -0.41655611991882324 0.6234202980995178 0.6616858243942261 +v -0.4343622922897339 0.6500691175460815 0.6234897971153259 +v -0.4510885775089264 0.6751017570495605 0.583743691444397 +v -0.4666934013366699 0.6984560489654541 0.542546272277832 +v -0.4811379313468933 0.7200738191604614 0.5 +v -0.4943862855434418 0.7399013638496399 0.4562106430530548 +v -0.5064055323600769 0.7578893899917603 0.4112870991230011 +v -0.5171657204627991 0.7739932537078857 0.36534103751182556 +v -0.5266402363777161 0.7881727814674377 0.31848666071891785 +v -0.5348053574562073 0.8003928065299988 0.27084046602249146 +v -0.5416409373283386 0.8106229305267334 0.22252093255519867 +v -0.5471298694610596 0.8188377022743225 0.1736481785774231 +v -0.5512585639953613 0.8250167369842529 0.12434370815753937 +v -0.5540167689323425 0.8291446566581726 0.07473009079694748 +v -0.5553975701332092 0.8312111496925354 0.024930691346526146 +v -0.5553975701332092 0.8312111496925354 -0.024930691346526146 +v -0.5540167689323425 0.8291446566581726 -0.07473009079694748 +v -0.5512585639953613 0.8250167369842529 -0.12434370815753937 +v -0.5471298694610596 0.8188377022743225 -0.1736481785774231 +v -0.5416409373283386 0.8106229305267334 -0.22252093255519867 +v -0.5348053574562073 0.8003928065299988 -0.27084046602249146 +v -0.5266402363777161 0.7881727814674377 -0.31848666071891785 +v -0.5171657204627991 0.7739932537078857 -0.36534103751182556 +v -0.5064055323600769 0.7578893899917603 -0.4112870991230011 +v -0.4943862855434418 0.7399013638496399 -0.4562106430530548 +v -0.4811379313468933 0.7200738191604614 -0.5 +v -0.4666934013366699 0.6984560489654541 -0.542546272277832 +v -0.4510885775089264 0.6751017570495605 -0.583743691444397 +v -0.4343622922897339 0.6500691175460815 -0.6234897971153259 +v -0.41655611991882324 0.6234202980995178 -0.6616858243942261 +v -0.3977143168449402 0.5952215194702148 -0.6982368230819702 +v -0.3778837323188782 0.5655429363250732 -0.7330518960952759 +v -0.35711365938186646 0.534458339214325 -0.7660444378852844 +v -0.3354557454586029 0.5020450353622437 -0.7971324920654297 +v -0.3129638433456421 0.4683835208415985 -0.826238751411438 +v -0.2896938920021057 0.43355754017829895 -0.853290855884552 +v -0.2657036781311035 0.39765363931655884 -0.8782215714454651 +v -0.24105289578437805 0.3607611358165741 -0.9009688496589661 +v -0.21580280363559723 0.32297173142433167 -0.9214761853218079 +v -0.19001621007919312 0.28437936305999756 -0.9396926164627075 +v -0.16375720500946045 0.24507996439933777 -0.955572783946991 +v -0.13709107041358948 0.20517127215862274 -0.9690772891044617 +v -0.11008410155773163 0.16475249826908112 -0.9801725149154663 +v -0.08280344307422638 0.12392411381006241 -0.9888308048248291 +v -0.05531693249940872 0.0827876403927803 -0.9950307607650757 +v -0.0276928897947073 0.04144534096121788 -0.9987569451332092 +v -0.03162189573049545 0.03853138908743858 0.9987569451332092 +v -0.06316517293453217 0.07696698606014252 0.9950307607650757 +v -0.09455141425132751 0.11521122604608536 0.9888308048248291 +v -0.12570258975028992 0.15316903591156006 0.9801725149154663 +v -0.15654124319553375 0.19074605405330658 0.9690772891044617 +v -0.18699070811271667 0.2278488278388977 0.955572783946991 +v -0.21697528660297394 0.2643851339817047 0.9396926164627075 +v -0.24642042815685272 0.3002641499042511 0.9214761853218079 +v -0.27525293827056885 0.3353966772556305 0.9009688496589661 +v -0.3034011125564575 0.3696953356266022 0.8782215714454651 +v -0.3307949900627136 0.40307486057281494 0.853290855884552 +v -0.3573664724826813 0.43545228242874146 0.826238751411438 +v -0.38304945826530457 0.46674713492393494 0.7971324920654297 +v -0.40778014063835144 0.49688154458999634 0.7660444378852844 +v -0.4314970076084137 0.5257806181907654 0.7330518960952759 +v -0.45414111018180847 0.5533725619316101 0.6982368230819702 +v -0.4756561517715454 0.5795887112617493 0.6616858243942261 +v -0.4959886372089386 0.6043639183044434 0.6234897971153259 +v -0.5150880217552185 0.6276365518569946 0.583743691444397 +v -0.5329068303108215 0.649348795413971 0.542546272277832 +v -0.5494006872177124 0.6694467067718506 0.5 +v -0.5645287036895752 0.6878802180290222 0.4562106430530548 +v -0.5782532095909119 0.7046035528182983 0.4112870991230011 +v -0.5905400514602661 0.7195751667022705 0.36534103751182556 +v -0.6013587713241577 0.7327577471733093 0.31848666071891785 +v -0.6106823682785034 0.7441186308860779 0.27084046602249146 +v -0.6184877157211304 0.7536294460296631 0.22252093255519867 +v -0.6247554421424866 0.7612667083740234 0.1736481785774231 +v -0.6294698715209961 0.767011284828186 0.12434370815753937 +v -0.6326193809509277 0.7708489298820496 0.07473009079694748 +v -0.6341961026191711 0.7727701663970947 0.024930691346526146 +v -0.6341961026191711 0.7727701663970947 -0.024930691346526146 +v -0.6326193809509277 0.7708489298820496 -0.07473009079694748 +v -0.6294698715209961 0.767011284828186 -0.12434370815753937 +v -0.6247554421424866 0.7612667083740234 -0.1736481785774231 +v -0.6184877157211304 0.7536294460296631 -0.22252093255519867 +v -0.6106823682785034 0.7441186308860779 -0.27084046602249146 +v -0.6013587713241577 0.7327577471733093 -0.31848666071891785 +v -0.5905400514602661 0.7195751667022705 -0.36534103751182556 +v -0.5782532095909119 0.7046035528182983 -0.4112870991230011 +v -0.5645287036895752 0.6878802180290222 -0.4562106430530548 +v -0.5494006872177124 0.6694467067718506 -0.5 +v -0.5329068303108215 0.649348795413971 -0.542546272277832 +v -0.5150880217552185 0.6276365518569946 -0.583743691444397 +v -0.4959886372089386 0.6043639183044434 -0.6234897971153259 +v -0.4756561517715454 0.5795887112617493 -0.6616858243942261 +v -0.45414111018180847 0.5533725619316101 -0.6982368230819702 +v -0.4314970076084137 0.5257806181907654 -0.7330518960952759 +v -0.40778014063835144 0.49688154458999634 -0.7660444378852844 +v -0.38304945826530457 0.46674713492393494 -0.7971324920654297 +v -0.3573664724826813 0.43545228242874146 -0.826238751411438 +v -0.3307949900627136 0.40307486057281494 -0.853290855884552 +v -0.3034011125564575 0.3696953356266022 -0.8782215714454651 +v -0.27525293827056885 0.3353966772556305 -0.9009688496589661 +v -0.24642042815685272 0.3002641499042511 -0.9214761853218079 +v -0.21697528660297394 0.2643851339817047 -0.9396926164627075 +v -0.18699070811271667 0.2278488278388977 -0.955572783946991 +v -0.15654124319553375 0.19074605405330658 -0.9690772891044617 +v -0.12570258975028992 0.15316903591156006 -0.9801725149154663 +v -0.09455141425132751 0.11521122604608536 -0.9888308048248291 +v -0.06316517293453217 0.07696698606014252 -0.9950307607650757 +v -0.03162189573049545 0.03853138908743858 -0.9987569451332092 +v -0.035246364772319794 0.035246364772319794 0.9987569451332092 +v -0.07040509581565857 0.07040509581565857 0.9950307607650757 +v -0.10538879781961441 0.10538879781961441 0.9888308048248291 +v -0.14011047780513763 0.14011047780513763 0.9801725149154663 +v -0.17448383569717407 0.17448383569717407 0.9690772891044617 +v -0.20842337608337402 0.20842337608337402 0.955572783946991 +v -0.2418447583913803 0.2418447583913803 0.9396926164627075 +v -0.27466487884521484 0.27466487884521484 0.9214761853218079 +v -0.3068021237850189 0.3068021237850189 0.9009688496589661 +v -0.3381766378879547 0.3381766378879547 0.8782215714454651 +v -0.36871036887168884 0.36871036887168884 0.853290855884552 +v -0.39832744002342224 0.39832744002342224 0.826238751411438 +v -0.4269541800022125 0.4269541800022125 0.7971324920654297 +v -0.45451948046684265 0.45451948046684265 0.7660444378852844 +v -0.48095476627349854 0.48095476627349854 0.7330518960952759 +v -0.5061942934989929 0.5061942934989929 0.6982368230819702 +v -0.5301753878593445 0.5301753878593445 0.6616858243942261 +v -0.5528383255004883 0.5528383255004883 0.6234897971153259 +v -0.5741268992424011 0.5741268992424011 0.583743691444397 +v -0.5939880013465881 0.5939880013465881 0.542546272277832 +v -0.6123724579811096 0.6123724579811096 0.5 +v -0.6292343735694885 0.6292343735694885 0.4562106430530548 +v -0.6445319652557373 0.6445319652557373 0.4112870991230011 +v -0.6582271456718445 0.6582271456718445 0.36534103751182556 +v -0.6702858805656433 0.6702858805656433 0.31848666071891785 +v -0.680678129196167 0.680678129196167 0.27084046602249146 +v -0.6893781423568726 0.6893781423568726 0.22252093255519867 +v -0.6963642239570618 0.6963642239570618 0.1736481785774231 +v -0.7016190886497498 0.7016190886497498 0.12434370815753937 +v -0.7051295638084412 0.7051295638084412 0.07473009079694748 +v -0.7068870067596436 0.7068870067596436 0.024930691346526146 +v -0.7068870067596436 0.7068870067596436 -0.024930691346526146 +v -0.7051295638084412 0.7051295638084412 -0.07473009079694748 +v -0.7016190886497498 0.7016190886497498 -0.12434370815753937 +v -0.6963642239570618 0.6963642239570618 -0.1736481785774231 +v -0.6893781423568726 0.6893781423568726 -0.22252093255519867 +v -0.680678129196167 0.680678129196167 -0.27084046602249146 +v -0.6702858805656433 0.6702858805656433 -0.31848666071891785 +v -0.6582271456718445 0.6582271456718445 -0.36534103751182556 +v -0.6445319652557373 0.6445319652557373 -0.4112870991230011 +v -0.6292343735694885 0.6292343735694885 -0.4562106430530548 +v -0.6123724579811096 0.6123724579811096 -0.5 +v -0.5939880013465881 0.5939880013465881 -0.542546272277832 +v -0.5741268992424011 0.5741268992424011 -0.583743691444397 +v -0.5528383255004883 0.5528383255004883 -0.6234897971153259 +v -0.5301753878593445 0.5301753878593445 -0.6616858243942261 +v -0.5061942934989929 0.5061942934989929 -0.6982368230819702 +v -0.48095476627349854 0.48095476627349854 -0.7330518960952759 +v -0.45451948046684265 0.45451948046684265 -0.7660444378852844 +v -0.4269541800022125 0.4269541800022125 -0.7971324920654297 +v -0.39832744002342224 0.39832744002342224 -0.826238751411438 +v -0.36871036887168884 0.36871036887168884 -0.853290855884552 +v -0.3381766378879547 0.3381766378879547 -0.8782215714454651 +v -0.3068021237850189 0.3068021237850189 -0.9009688496589661 +v -0.27466487884521484 0.27466487884521484 -0.9214761853218079 +v -0.2418447583913803 0.2418447583913803 -0.9396926164627075 +v -0.20842337608337402 0.20842337608337402 -0.955572783946991 +v -0.17448383569717407 0.17448383569717407 -0.9690772891044617 +v -0.14011047780513763 0.14011047780513763 -0.9801725149154663 +v -0.10538879781961441 0.10538879781961441 -0.9888308048248291 +v -0.07040509581565857 0.07040509581565857 -0.9950307607650757 +v -0.035246364772319794 0.035246364772319794 -0.9987569451332092 +v -0.03853138908743858 0.03162189573049545 0.9987569451332092 +v -0.07696698606014252 0.06316517293453217 0.9950307607650757 +v -0.11521122604608536 0.09455141425132751 0.9888308048248291 +v -0.15316903591156006 0.12570258975028992 0.9801725149154663 +v -0.19074605405330658 0.15654124319553375 0.9690772891044617 +v -0.2278488278388977 0.18699070811271667 0.955572783946991 +v -0.2643851339817047 0.21697528660297394 0.9396926164627075 +v -0.3002641499042511 0.24642042815685272 0.9214761853218079 +v -0.3353966772556305 0.27525293827056885 0.9009688496589661 +v -0.3696953356266022 0.3034011125564575 0.8782215714454651 +v -0.40307486057281494 0.3307949900627136 0.853290855884552 +v -0.43545228242874146 0.3573664724826813 0.826238751411438 +v -0.46674713492393494 0.38304945826530457 0.7971324920654297 +v -0.49688154458999634 0.40778014063835144 0.7660444378852844 +v -0.5257806181907654 0.4314970076084137 0.7330518960952759 +v -0.5533725619316101 0.45414111018180847 0.6982368230819702 +v -0.5795887112617493 0.4756561517715454 0.6616858243942261 +v -0.6043639183044434 0.4959886372089386 0.6234897971153259 +v -0.6276365518569946 0.5150880217552185 0.583743691444397 +v -0.649348795413971 0.5329068303108215 0.542546272277832 +v -0.6694467067718506 0.5494006872177124 0.5 +v -0.6878802180290222 0.5645287036895752 0.4562106430530548 +v -0.7046035528182983 0.5782532095909119 0.4112870991230011 +v -0.7195751667022705 0.5905400514602661 0.36534103751182556 +v -0.7327577471733093 0.6013587713241577 0.31848666071891785 +v -0.7441186308860779 0.6106823682785034 0.27084046602249146 +v -0.7536294460296631 0.6184877157211304 0.22252093255519867 +v -0.7612667083740234 0.6247554421424866 0.1736481785774231 +v -0.767011284828186 0.6294698715209961 0.12434370815753937 +v -0.7708489298820496 0.6326193809509277 0.07473009079694748 +v -0.7727701663970947 0.6341961026191711 0.024930691346526146 +v -0.7727701663970947 0.6341961026191711 -0.024930691346526146 +v -0.7708489298820496 0.6326193809509277 -0.07473009079694748 +v -0.767011284828186 0.6294698715209961 -0.12434370815753937 +v -0.7612667083740234 0.6247554421424866 -0.1736481785774231 +v -0.7536294460296631 0.6184877157211304 -0.22252093255519867 +v -0.7441186308860779 0.6106823682785034 -0.27084046602249146 +v -0.7327577471733093 0.6013587713241577 -0.31848666071891785 +v -0.7195751667022705 0.5905400514602661 -0.36534103751182556 +v -0.7046035528182983 0.5782532095909119 -0.4112870991230011 +v -0.6878802180290222 0.5645287036895752 -0.4562106430530548 +v -0.6694467067718506 0.5494006872177124 -0.5 +v -0.649348795413971 0.5329068303108215 -0.542546272277832 +v -0.6276365518569946 0.5150880217552185 -0.583743691444397 +v -0.6043639183044434 0.4959886372089386 -0.6234897971153259 +v -0.5795887112617493 0.4756561517715454 -0.6616858243942261 +v -0.5533725619316101 0.45414111018180847 -0.6982368230819702 +v -0.5257806181907654 0.4314970076084137 -0.7330518960952759 +v -0.49688154458999634 0.40778014063835144 -0.7660444378852844 +v -0.46674713492393494 0.38304945826530457 -0.7971324920654297 +v -0.43545228242874146 0.3573664724826813 -0.826238751411438 +v -0.40307486057281494 0.3307949900627136 -0.853290855884552 +v -0.3696953356266022 0.3034011125564575 -0.8782215714454651 +v -0.3353966772556305 0.27525293827056885 -0.9009688496589661 +v -0.3002641499042511 0.24642042815685272 -0.9214761853218079 +v -0.2643851339817047 0.21697528660297394 -0.9396926164627075 +v -0.2278488278388977 0.18699070811271667 -0.955572783946991 +v -0.19074605405330658 0.15654124319553375 -0.9690772891044617 +v -0.15316903591156006 0.12570258975028992 -0.9801725149154663 +v -0.11521122604608536 0.09455141425132751 -0.9888308048248291 +v -0.07696698606014252 0.06316517293453217 -0.9950307607650757 +v -0.03853138908743858 0.03162189573049545 -0.9987569451332092 +v -0.04144534096121788 0.0276928897947073 0.9987569451332092 +v -0.0827876403927803 0.05531693249940872 0.9950307607650757 +v -0.12392411381006241 0.08280344307422638 0.9888308048248291 +v -0.16475249826908112 0.11008410155773163 0.9801725149154663 +v -0.20517127215862274 0.13709107041358948 0.9690772891044617 +v -0.24507996439933777 0.16375720500946045 0.955572783946991 +v -0.28437936305999756 0.19001621007919312 0.9396926164627075 +v -0.32297173142433167 0.21580280363559723 0.9214761853218079 +v -0.3607611358165741 0.24105289578437805 0.9009688496589661 +v -0.39765363931655884 0.2657036781311035 0.8782215714454651 +v -0.43355754017829895 0.2896938920021057 0.853290855884552 +v -0.4683835208415985 0.3129638433456421 0.826238751411438 +v -0.5020450353622437 0.3354557454586029 0.7971324920654297 +v -0.534458339214325 0.35711365938186646 0.7660444378852844 +v -0.5655429363250732 0.3778837323188782 0.7330518960952759 +v -0.5952215194702148 0.3977143168449402 0.6982368230819702 +v -0.6234202980995178 0.41655611991882324 0.6616858243942261 +v -0.6500691175460815 0.4343622922897339 0.6234897971153259 +v -0.6751017570495605 0.4510885775089264 0.583743691444397 +v -0.6984560489654541 0.4666934013366699 0.542546272277832 +v -0.7200738191604614 0.4811379313468933 0.5 +v -0.7399013638496399 0.4943862855434418 0.4562106430530548 +v -0.7578893899917603 0.5064055323600769 0.4112870991230011 +v -0.7739932537078857 0.5171657204627991 0.36534103751182556 +v -0.7881727814674377 0.5266402363777161 0.31848666071891785 +v -0.8003928065299988 0.5348053574562073 0.27084046602249146 +v -0.8106229305267334 0.5416409373283386 0.22252093255519867 +v -0.8188377022743225 0.5471298694610596 0.1736481785774231 +v -0.8250167369842529 0.5512585639953613 0.12434370815753937 +v -0.8291446566581726 0.5540167689323425 0.07473009079694748 +v -0.8312111496925354 0.5553975701332092 0.024930691346526146 +v -0.8312111496925354 0.5553975701332092 -0.024930691346526146 +v -0.8291446566581726 0.5540167689323425 -0.07473009079694748 +v -0.8250167369842529 0.5512585639953613 -0.12434370815753937 +v -0.8188377022743225 0.5471298694610596 -0.1736481785774231 +v -0.8106229305267334 0.5416409373283386 -0.22252093255519867 +v -0.8003928065299988 0.5348053574562073 -0.27084046602249146 +v -0.7881727814674377 0.5266402363777161 -0.31848666071891785 +v -0.7739932537078857 0.5171657204627991 -0.36534103751182556 +v -0.7578893899917603 0.5064055323600769 -0.4112870991230011 +v -0.7399013638496399 0.4943862855434418 -0.4562106430530548 +v -0.7200738191604614 0.4811379313468933 -0.5 +v -0.6984560489654541 0.4666934013366699 -0.542546272277832 +v -0.6751017570495605 0.4510885775089264 -0.583743691444397 +v -0.6500691175460815 0.4343622922897339 -0.6234897971153259 +v -0.6234202980995178 0.41655611991882324 -0.6616858243942261 +v -0.5952215194702148 0.3977143168449402 -0.6982368230819702 +v -0.5655429363250732 0.3778837323188782 -0.7330518960952759 +v -0.534458339214325 0.35711365938186646 -0.7660444378852844 +v -0.5020450353622437 0.3354557454586029 -0.7971324920654297 +v -0.4683835208415985 0.3129638433456421 -0.826238751411438 +v -0.43355754017829895 0.2896938920021057 -0.853290855884552 +v -0.39765363931655884 0.2657036781311035 -0.8782215714454651 +v -0.3607611358165741 0.24105289578437805 -0.9009688496589661 +v -0.32297173142433167 0.21580280363559723 -0.9214761853218079 +v -0.28437936305999756 0.19001621007919312 -0.9396926164627075 +v -0.24507996439933777 0.16375720500946045 -0.955572783946991 +v -0.20517127215862274 0.13709107041358948 -0.9690772891044617 +v -0.16475249826908112 0.11008410155773163 -0.9801725149154663 +v -0.12392411381006241 0.08280344307422638 -0.9888308048248291 +v -0.0827876403927803 0.05531693249940872 -0.9950307607650757 +v -0.04144534096121788 0.0276928897947073 -0.9987569451332092 +v -0.043960146605968475 0.023497188463807106 0.9987569451332092 +v -0.08781100064516068 0.04693595692515373 0.9950307607650757 +v -0.1314435452222824 0.07025803625583649 0.9888308048248291 +v -0.17474929988384247 0.09340544790029526 0.9801725149154663 +v -0.2176205962896347 0.11632063239812851 0.9690772891044617 +v -0.2599508464336395 0.13894662261009216 0.955572783946991 +v -0.30163484811782837 0.16122718155384064 0.9396926164627075 +v -0.34256890416145325 0.1831068992614746 0.9214761853218079 +v -0.38265129923820496 0.20453137159347534 0.9009688496589661 +v -0.42178234457969666 0.2254473716020584 0.8782215714454651 +v -0.4598647952079773 0.2458028495311737 0.853290855884552 +v -0.4968039393424988 0.2655472457408905 0.826238751411438 +v -0.5325079560279846 0.2846314311027527 0.7971324920654297 +v -0.5668880343437195 0.30300799012184143 0.7660444378852844 +v -0.599858820438385 0.320631206035614 0.7330518960952759 +v -0.6313381791114807 0.3374572992324829 0.6982368230819702 +v -0.661247968673706 0.3534444272518158 0.6616858243942261 +v -0.6895138025283813 0.3685528039932251 0.6234897971153259 +v -0.7160654067993164 0.3827449381351471 0.583743691444397 +v -0.7408367395401001 0.39598548412323 0.542546272277832 +v -0.7637662291526794 0.40824154019355774 0.5 +v -0.7847968935966492 0.41948267817497253 0.4562106430530548 +v -0.8038763999938965 0.42968088388442993 0.4112870991230011 +v -0.820957362651825 0.4388108551502228 0.36534103751182556 +v -0.8359972834587097 0.44684985280036926 0.31848666071891785 +v -0.8489587903022766 0.453777939081192 0.27084046602249146 +v -0.8598096370697021 0.4595778286457062 0.22252093255519867 +v -0.8685228824615479 0.4642351567745209 0.1736481785774231 +v -0.8750768303871155 0.4677383303642273 0.12434370815753937 +v -0.8794552087783813 0.47007861733436584 0.07473009079694748 +v -0.8816471695899963 0.4712502062320709 0.024930691346526146 +v -0.8816471695899963 0.4712502062320709 -0.024930691346526146 +v -0.8794552087783813 0.47007861733436584 -0.07473009079694748 +v -0.8750768303871155 0.4677383303642273 -0.12434370815753937 +v -0.8685228824615479 0.4642351567745209 -0.1736481785774231 +v -0.8598096370697021 0.4595778286457062 -0.22252093255519867 +v -0.8489587903022766 0.453777939081192 -0.27084046602249146 +v -0.8359972834587097 0.44684985280036926 -0.31848666071891785 +v -0.820957362651825 0.4388108551502228 -0.36534103751182556 +v -0.8038763999938965 0.42968088388442993 -0.4112870991230011 +v -0.7847968935966492 0.41948267817497253 -0.4562106430530548 +v -0.7637662291526794 0.40824154019355774 -0.5 +v -0.7408367395401001 0.39598548412323 -0.542546272277832 +v -0.7160654067993164 0.3827449381351471 -0.583743691444397 +v -0.6895138025283813 0.3685528039932251 -0.6234897971153259 +v -0.661247968673706 0.3534444272518158 -0.6616858243942261 +v -0.6313381791114807 0.3374572992324829 -0.6982368230819702 +v -0.599858820438385 0.320631206035614 -0.7330518960952759 +v -0.5668880343437195 0.30300799012184143 -0.7660444378852844 +v -0.5325079560279846 0.2846314311027527 -0.7971324920654297 +v -0.4968039393424988 0.2655472457408905 -0.826238751411438 +v -0.4598647952079773 0.2458028495311737 -0.853290855884552 +v -0.42178234457969666 0.2254473716020584 -0.8782215714454651 +v -0.38265129923820496 0.20453137159347534 -0.9009688496589661 +v -0.34256890416145325 0.1831068992614746 -0.9214761853218079 +v -0.30163484811782837 0.16122718155384064 -0.9396926164627075 +v -0.2599508464336395 0.13894662261009216 -0.955572783946991 +v -0.2176205962896347 0.11632063239812851 -0.9690772891044617 +v -0.17474929988384247 0.09340544790029526 -0.9801725149154663 +v -0.1314435452222824 0.07025803625583649 -0.9888308048248291 +v -0.08781100064516068 0.04693595692515373 -0.9950307607650757 +v -0.043960146605968475 0.023497188463807106 -0.9987569451332092 +v -0.046051595360040665 0.019075194373726845 0.9987569451332092 +v -0.0919886976480484 0.03810296580195427 0.9950307607650757 +v -0.13769710063934326 0.05703600496053696 0.9888308048248291 +v -0.18306316435337067 0.07582724839448929 0.9801725149154663 +v -0.22797410190105438 0.09442996978759766 0.9690772891044617 +v -0.2723182737827301 0.11279792338609695 0.955572783946991 +v -0.31598541140556335 0.13088543713092804 0.9396926164627075 +v -0.35886695981025696 0.14864756166934967 0.9214761853218079 +v -0.4008563160896301 0.16604012250900269 0.9009688496589661 +v -0.4418490529060364 0.18301987648010254 0.8782215714454651 +v -0.48174330592155457 0.19954460859298706 0.853290855884552 +v -0.520439863204956 0.215573251247406 0.826238751411438 +v -0.5578425526618958 0.23106594383716583 0.7971324920654297 +v -0.5938583016395569 0.24598416686058044 0.7660444378852844 +v -0.6283976435661316 0.26029083132743835 0.7330518960952759 +v -0.6613747477531433 0.27395036816596985 0.6982368230819702 +v -0.6927074790000916 0.28692883253097534 0.6616858243942261 +v -0.7223181128501892 0.29919394850730896 0.6234897971153259 +v -0.7501329183578491 0.3107152283191681 0.583743691444397 +v -0.7760827541351318 0.32146400213241577 0.542546272277832 +v -0.8001031279563904 0.3314135670661926 0.5 +v -0.8221343755722046 0.34053918719291687 0.4562106430530548 +v -0.8421216011047363 0.34881818294525146 0.4112870991230011 +v -0.8600152134895325 0.3562299609184265 0.36534103751182556 +v -0.8757706880569458 0.3627561032772064 0.31848666071891785 +v -0.8893488645553589 0.3683803379535675 0.27084046602249146 +v -0.9007159471511841 0.3730887472629547 0.22252093255519867 +v -0.9098437428474426 0.3768696188926697 0.1736481785774231 +v -0.9167094826698303 0.3797135055065155 0.12434370815753937 +v -0.9212961792945862 0.3816133737564087 0.07473009079694748 +v -0.9235923886299133 0.3825644850730896 0.024930691346526146 +v -0.9235923886299133 0.3825644850730896 -0.024930691346526146 +v -0.9212961792945862 0.3816133737564087 -0.07473009079694748 +v -0.9167094826698303 0.3797135055065155 -0.12434370815753937 +v -0.9098437428474426 0.3768696188926697 -0.1736481785774231 +v -0.9007159471511841 0.3730887472629547 -0.22252093255519867 +v -0.8893488645553589 0.3683803379535675 -0.27084046602249146 +v -0.8757706880569458 0.3627561032772064 -0.31848666071891785 +v -0.8600152134895325 0.3562299609184265 -0.36534103751182556 +v -0.8421216011047363 0.34881818294525146 -0.4112870991230011 +v -0.8221343755722046 0.34053918719291687 -0.4562106430530548 +v -0.8001031279563904 0.3314135670661926 -0.5 +v -0.7760827541351318 0.32146400213241577 -0.542546272277832 +v -0.7501329183578491 0.3107152283191681 -0.583743691444397 +v -0.7223181128501892 0.29919394850730896 -0.6234897971153259 +v -0.6927074790000916 0.28692883253097534 -0.6616858243942261 +v -0.6613747477531433 0.27395036816596985 -0.6982368230819702 +v -0.6283976435661316 0.26029083132743835 -0.7330518960952759 +v -0.5938583016395569 0.24598416686058044 -0.7660444378852844 +v -0.5578425526618958 0.23106594383716583 -0.7971324920654297 +v -0.520439863204956 0.215573251247406 -0.826238751411438 +v -0.48174330592155457 0.19954460859298706 -0.853290855884552 +v -0.4418490529060364 0.18301987648010254 -0.8782215714454651 +v -0.4008563160896301 0.16604012250900269 -0.9009688496589661 +v -0.35886695981025696 0.14864756166934967 -0.9214761853218079 +v -0.31598541140556335 0.13088543713092804 -0.9396926164627075 +v -0.2723182737827301 0.11279792338609695 -0.955572783946991 +v -0.22797410190105438 0.09442996978759766 -0.9690772891044617 +v -0.18306316435337067 0.07582724839448929 -0.9801725149154663 +v -0.13769710063934326 0.05703600496053696 -0.9888308048248291 +v -0.0919886976480484 0.03810296580195427 -0.9950307607650757 +v -0.046051595360040665 0.019075194373726845 -0.9987569451332092 +v -0.04769953712821007 0.01446949690580368 0.9987569451332092 +v -0.0952804908156395 0.028903020545840263 0.9950307607650757 +v -0.14262455701828003 0.043264687061309814 0.9888308048248291 +v -0.18961404263973236 0.05751878768205643 0.9801725149154663 +v -0.23613210022449493 0.07162988930940628 0.9690772891044617 +v -0.2820631265640259 0.08556290715932846 0.955572783946991 +v -0.32729285955429077 0.09928320348262787 0.9396926164627075 +v -0.3717089295387268 0.11275666952133179 0.9214761853218079 +v -0.4152008593082428 0.12594980001449585 0.9009688496589661 +v -0.4576605260372162 0.1388297975063324 0.8782215714454651 +v -0.49898236989974976 0.15136465430259705 0.853290855884552 +v -0.5390636920928955 0.16352318227291107 0.826238751411438 +v -0.5778048038482666 0.17527516186237335 0.7971324920654297 +v -0.615109384059906 0.18659138679504395 0.7660444378852844 +v -0.650884747505188 0.19744372367858887 0.7330518960952759 +v -0.6850418448448181 0.20780517160892487 0.6982368230819702 +v -0.717495858669281 0.21764999628067017 0.6616858243942261 +v -0.7481660842895508 0.22695370018482208 0.6234897971153259 +v -0.7769762277603149 0.23569315671920776 0.583743691444397 +v -0.8038547039031982 0.24384665489196777 0.542546272277832 +v -0.8287346363067627 0.2513939142227173 0.5 +v -0.8515542149543762 0.25831615924835205 0.4562106430530548 +v -0.8722566962242126 0.26459619402885437 0.4112870991230011 +v -0.8907906413078308 0.2702183723449707 0.36534103751182556 +v -0.9071099162101746 0.275168776512146 0.31848666071891785 +v -0.9211739897727966 0.27943506836891174 0.27084046602249146 +v -0.9329478144645691 0.2830066382884979 0.22252093255519867 +v -0.9424022436141968 0.285874605178833 0.1736481785774231 +v -0.9495137333869934 0.28803184628486633 0.12434370815753937 +v -0.9542645215988159 0.2894729971885681 0.07473009079694748 +v -0.9566429257392883 0.29019445180892944 0.024930691346526146 +v -0.9566429257392883 0.29019445180892944 -0.024930691346526146 +v -0.9542645215988159 0.2894729971885681 -0.07473009079694748 +v -0.9495137333869934 0.28803184628486633 -0.12434370815753937 +v -0.9424022436141968 0.285874605178833 -0.1736481785774231 +v -0.9329478144645691 0.2830066382884979 -0.22252093255519867 +v -0.9211739897727966 0.27943506836891174 -0.27084046602249146 +v -0.9071099162101746 0.275168776512146 -0.31848666071891785 +v -0.8907906413078308 0.2702183723449707 -0.36534103751182556 +v -0.8722566962242126 0.26459619402885437 -0.4112870991230011 +v -0.8515542149543762 0.25831615924835205 -0.4562106430530548 +v -0.8287346363067627 0.2513939142227173 -0.5 +v -0.8038547039031982 0.24384665489196777 -0.542546272277832 +v -0.7769762277603149 0.23569315671920776 -0.583743691444397 +v -0.7481660842895508 0.22695370018482208 -0.6234897971153259 +v -0.717495858669281 0.21764999628067017 -0.6616858243942261 +v -0.6850418448448181 0.20780517160892487 -0.6982368230819702 +v -0.650884747505188 0.19744372367858887 -0.7330518960952759 +v -0.615109384059906 0.18659138679504395 -0.7660444378852844 +v -0.5778048038482666 0.17527516186237335 -0.7971324920654297 +v -0.5390636920928955 0.16352318227291107 -0.826238751411438 +v -0.49898236989974976 0.15136465430259705 -0.853290855884552 +v -0.4576605260372162 0.1388297975063324 -0.8782215714454651 +v -0.4152008593082428 0.12594980001449585 -0.9009688496589661 +v -0.3717089295387268 0.11275666952133179 -0.9214761853218079 +v -0.32729285955429077 0.09928320348262787 -0.9396926164627075 +v -0.2820631265640259 0.08556290715932846 -0.955572783946991 +v -0.23613210022449493 0.07162988930940628 -0.9690772891044617 +v -0.18961404263973236 0.05751878768205643 -0.9801725149154663 +v -0.14262455701828003 0.043264687061309814 -0.9888308048248291 +v -0.0952804908156395 0.028903020545840263 -0.9950307607650757 +v -0.04769953712821007 0.01446949690580368 -0.9987569451332092 +v -0.048888109624385834 0.009724450297653675 0.9987569451332092 +v -0.09765467792749405 0.019424723461270332 0.9950307607650757 +v -0.1461784541606903 0.029076702892780304 0.9888308048248291 +v -0.1943388134241104 0.03865639492869377 0.9801725149154663 +v -0.2420160174369812 0.04813998192548752 0.9690772891044617 +v -0.2890915274620056 0.05750388279557228 0.955572783946991 +v -0.33544832468032837 0.06672482192516327 0.9396926164627075 +v -0.38097113370895386 0.07577987015247345 0.9214761853218079 +v -0.425546795129776 0.08464651554822922 0.9009688496589661 +v -0.46906447410583496 0.09330271929502487 0.8782215714454651 +v -0.511415958404541 0.1017269641160965 0.853290855884552 +v -0.5524960160255432 0.10989829152822495 0.826238751411438 +v -0.5922024846076965 0.1177963986992836 0.7971324920654297 +v -0.6304365992546082 0.12540164589881897 0.7660444378852844 +v -0.6671034097671509 0.13269512355327606 0.7330518960952759 +v -0.7021116614341736 0.13965868949890137 0.6982368230819702 +v -0.735374391078949 0.14627505838871002 0.6616858243942261 +v -0.766808807849884 0.15252774953842163 0.6234897971153259 +v -0.796336829662323 0.1584012508392334 0.583743691444397 +v -0.8238850831985474 0.16388092935085297 0.542546272277832 +v -0.8493849635124207 0.16895318031311035 0.5 +v -0.8727731704711914 0.17360538244247437 0.4562106430530548 +v -0.8939915299415588 0.17782597243785858 0.4112870991230011 +v -0.9129872918128967 0.18160445988178253 0.36534103751182556 +v -0.9297131896018982 0.1849314570426941 0.31848666071891785 +v -0.9441276788711548 0.18779867887496948 0.27084046602249146 +v -0.9561949372291565 0.19019900262355804 0.22252093255519867 +v -0.9658849239349365 0.19212646782398224 0.1736481785774231 +v -0.9731736183166504 0.19357626140117645 0.12434370815753937 +v -0.9780427813529968 0.19454480707645416 0.07473009079694748 +v -0.980480432510376 0.19502969086170197 0.024930691346526146 +v -0.980480432510376 0.19502969086170197 -0.024930691346526146 +v -0.9780427813529968 0.19454480707645416 -0.07473009079694748 +v -0.9731736183166504 0.19357626140117645 -0.12434370815753937 +v -0.9658849239349365 0.19212646782398224 -0.1736481785774231 +v -0.9561949372291565 0.19019900262355804 -0.22252093255519867 +v -0.9441276788711548 0.18779867887496948 -0.27084046602249146 +v -0.9297131896018982 0.1849314570426941 -0.31848666071891785 +v -0.9129872918128967 0.18160445988178253 -0.36534103751182556 +v -0.8939915299415588 0.17782597243785858 -0.4112870991230011 +v -0.8727731704711914 0.17360538244247437 -0.4562106430530548 +v -0.8493849635124207 0.16895318031311035 -0.5 +v -0.8238850831985474 0.16388092935085297 -0.542546272277832 +v -0.796336829662323 0.1584012508392334 -0.583743691444397 +v -0.766808807849884 0.15252774953842163 -0.6234897971153259 +v -0.735374391078949 0.14627505838871002 -0.6616858243942261 +v -0.7021116614341736 0.13965868949890137 -0.6982368230819702 +v -0.6671034097671509 0.13269512355327606 -0.7330518960952759 +v -0.6304365992546082 0.12540164589881897 -0.7660444378852844 +v -0.5922024846076965 0.1177963986992836 -0.7971324920654297 +v -0.5524960160255432 0.10989829152822495 -0.826238751411438 +v -0.511415958404541 0.1017269641160965 -0.853290855884552 +v -0.46906447410583496 0.09330271929502487 -0.8782215714454651 +v -0.425546795129776 0.08464651554822922 -0.9009688496589661 +v -0.38097113370895386 0.07577987015247345 -0.9214761853218079 +v -0.33544832468032837 0.06672482192516327 -0.9396926164627075 +v -0.2890915274620056 0.05750388279557228 -0.955572783946991 +v -0.2420160174369812 0.04813998192548752 -0.9690772891044617 +v -0.1943388134241104 0.03865639492869377 -0.9801725149154663 +v -0.1461784541606903 0.029076702892780304 -0.9888308048248291 +v -0.09765467792749405 0.019424723461270332 -0.9950307607650757 +v -0.048888109624385834 0.009724450297653675 -0.9987569451332092 +v -0.04960586503148079 0.004885751288384199 0.9987569451332092 +v -0.0990884006023407 0.009759355336427689 0.9950307607650757 +v -0.14832459390163422 0.014608697034418583 0.9888308048248291 +v -0.1971920132637024 0.019421719014644623 0.9801725149154663 +v -0.24556919932365417 0.024186454713344574 0.9690772891044617 +v -0.29333585500717163 0.028891058638691902 0.955572783946991 +v -0.3403732180595398 0.033523835241794586 0.9396926164627075 +v -0.38656437397003174 0.03807326778769493 0.9214761853218079 +v -0.4317944645881653 0.04252804443240166 0.9009688496589661 +v -0.47595104575157166 0.04687708616256714 0.8782215714454651 +v -0.518924355506897 0.05110958591103554 0.853290855884552 +v -0.5606074929237366 0.0552150197327137 0.826238751411438 +v -0.600896954536438 0.05918318033218384 0.7971324920654297 +v -0.6396924257278442 0.06300420314073563 0.7660444378852844 +v -0.6768975257873535 0.06666858494281769 0.7330518960952759 +v -0.7124197483062744 0.07016722112894058 0.6982368230819702 +v -0.7461708188056946 0.0734914094209671 0.6616858243942261 +v -0.7780667543411255 0.07663288712501526 0.6234897971153259 +v -0.8080282807350159 0.07958383858203888 0.583743691444397 +v -0.8359809517860413 0.08233693987131119 0.542546272277832 +v -0.8618552684783936 0.08488533645868301 0.5 +v -0.8855868577957153 0.08722268790006638 0.4562106430530548 +v -0.907116711139679 0.08934319764375687 0.4112870991230011 +v -0.9263913631439209 0.09124158322811127 0.36534103751182556 +v -0.943362832069397 0.09291312843561172 0.31848666071891785 +v -0.957988977432251 0.09435367584228516 0.27084046602249146 +v -0.9702333807945251 0.09555964916944504 0.22252093255519867 +v -0.980065643787384 0.09652803838253021 0.1736481785774231 +v -0.9874613285064697 0.09725645184516907 0.12434370815753937 +v -0.9924020171165466 0.09774306416511536 0.07473009079694748 +v -0.994875431060791 0.09798667579889297 0.024930691346526146 +v -0.994875431060791 0.09798667579889297 -0.024930691346526146 +v -0.9924020171165466 0.09774306416511536 -0.07473009079694748 +v -0.9874613285064697 0.09725645184516907 -0.12434370815753937 +v -0.980065643787384 0.09652803838253021 -0.1736481785774231 +v -0.9702333807945251 0.09555964916944504 -0.22252093255519867 +v -0.957988977432251 0.09435367584228516 -0.27084046602249146 +v -0.943362832069397 0.09291312843561172 -0.31848666071891785 +v -0.9263913631439209 0.09124158322811127 -0.36534103751182556 +v -0.907116711139679 0.08934319764375687 -0.4112870991230011 +v -0.8855868577957153 0.08722268790006638 -0.4562106430530548 +v -0.8618552684783936 0.08488533645868301 -0.5 +v -0.8359809517860413 0.08233693987131119 -0.542546272277832 +v -0.8080282807350159 0.07958383858203888 -0.583743691444397 +v -0.7780667543411255 0.07663288712501526 -0.6234897971153259 +v -0.7461708188056946 0.0734914094209671 -0.6616858243942261 +v -0.7124197483062744 0.07016722112894058 -0.6982368230819702 +v -0.6768975257873535 0.06666858494281769 -0.7330518960952759 +v -0.6396924257278442 0.06300420314073563 -0.7660444378852844 +v -0.600896954536438 0.05918318033218384 -0.7971324920654297 +v -0.5606074929237366 0.0552150197327137 -0.826238751411438 +v -0.518924355506897 0.05110958591103554 -0.853290855884552 +v -0.47595104575157166 0.04687708616256714 -0.8782215714454651 +v -0.4317944645881653 0.04252804443240166 -0.9009688496589661 +v -0.38656437397003174 0.03807326778769493 -0.9214761853218079 +v -0.3403732180595398 0.033523835241794586 -0.9396926164627075 +v -0.29333585500717163 0.028891058638691902 -0.955572783946991 +v -0.24556919932365417 0.024186454713344574 -0.9690772891044617 +v -0.1971920132637024 0.019421719014644623 -0.9801725149154663 +v -0.14832459390163422 0.014608697034418583 -0.9888308048248291 +v -0.0990884006023407 0.009759355336427689 -0.9950307607650757 +v -0.04960586503148079 0.004885751288384199 -0.9987569451332092 +v -0.04984588548541069 6.104360482889217e-18 0.9987569451332092 +v -0.09956784546375275 1.2193544683079923e-17 0.9950307607650757 +v -0.1490422636270523 1.8252413541001174e-17 0.9888308048248291 +v -0.19814614951610565 2.4265904097698378e-17 0.9801725149154663 +v -0.2467574030160904 3.021906506359964e-17 0.9690772891044617 +v -0.29475516080856323 3.609709652318516e-17 0.955572783946991 +v -0.3420201539993286 4.1885388812476027e-17 0.9396926164627075 +v -0.3884347975254059 4.756954237136892e-17 0.9214761853218079 +v -0.4338837265968323 5.313543226372388e-17 0.9009688496589661 +v -0.4782539904117584 5.856921810353167e-17 0.8782215714454651 +v -0.5214352011680603 6.385739699447298e-17 0.853290855884552 +v -0.5633200407028198 6.89868101473633e-17 0.826238751411438 +v -0.6038044095039368 7.394471236332444e-17 0.7971324920654297 +v -0.6427876353263855 7.871878195995179e-17 0.7660444378852844 +v -0.6801727414131165 8.329713400620419e-17 0.7330518960952759 +v -0.7158668637275696 8.766840634918762e-17 0.6982368230819702 +v -0.7497811913490295 9.182171329204087e-17 0.6616858243942261 +v -0.7818315029144287 9.574674485560909e-17 0.6234897971153259 +v -0.8119379878044128 9.943372707377435e-17 0.583743691444397 +v -0.8400259017944336 1.0287350802023936e-16 0.542546272277832 +v -0.8660253882408142 1.0605752472130298e-16 0.5 +v -0.8898718357086182 1.0897786933030919e-16 0.4562106430530548 +v -0.9115058779716492 1.1162726927531244e-16 0.4112870991230011 +v -0.9308737516403198 1.139991600509715e-16 0.36534103751182556 +v -0.9479273557662964 1.1608761242665557e-16 0.31848666071891785 +v -0.9626242518424988 1.1788746479534236e-16 0.27084046602249146 +v -0.9749279022216797 1.1939423052938935e-16 0.22252093255519867 +v -0.9848077297210693 1.2060416415498291e-16 0.1736481785774231 +v -0.9922391772270203 1.2151426135213826e-16 0.12434370815753937 +v -0.9972038269042969 1.2212224571980963e-16 0.07473009079694748 +v -0.9996891617774963 1.2242662171544955e-16 0.024930691346526146 +v -0.9996891617774963 1.2242662171544955e-16 -0.024930691346526146 +v -0.9972038269042969 1.2212224571980963e-16 -0.07473009079694748 +v -0.9922391772270203 1.2151426135213826e-16 -0.12434370815753937 +v -0.9848077297210693 1.2060416415498291e-16 -0.1736481785774231 +v -0.9749279022216797 1.1939423052938935e-16 -0.22252093255519867 +v -0.9626242518424988 1.1788746479534236e-16 -0.27084046602249146 +v -0.9479273557662964 1.1608761242665557e-16 -0.31848666071891785 +v -0.9308737516403198 1.139991600509715e-16 -0.36534103751182556 +v -0.9115058779716492 1.1162726927531244e-16 -0.4112870991230011 +v -0.8898718357086182 1.0897786933030919e-16 -0.4562106430530548 +v -0.8660253882408142 1.0605752472130298e-16 -0.5 +v -0.8400259017944336 1.0287350802023936e-16 -0.542546272277832 +v -0.8119379878044128 9.943372707377435e-17 -0.583743691444397 +v -0.7818315029144287 9.574674485560909e-17 -0.6234897971153259 +v -0.7497811913490295 9.182171329204087e-17 -0.6616858243942261 +v -0.7158668637275696 8.766840634918762e-17 -0.6982368230819702 +v -0.6801727414131165 8.329713400620419e-17 -0.7330518960952759 +v -0.6427876353263855 7.871878195995179e-17 -0.7660444378852844 +v -0.6038044095039368 7.394471236332444e-17 -0.7971324920654297 +v -0.5633200407028198 6.89868101473633e-17 -0.826238751411438 +v -0.5214352011680603 6.385739699447298e-17 -0.853290855884552 +v -0.4782539904117584 5.856921810353167e-17 -0.8782215714454651 +v -0.4338837265968323 5.313543226372388e-17 -0.9009688496589661 +v -0.3884347975254059 4.756954237136892e-17 -0.9214761853218079 +v -0.3420201539993286 4.1885388812476027e-17 -0.9396926164627075 +v -0.29475516080856323 3.609709652318516e-17 -0.955572783946991 +v -0.2467574030160904 3.021906506359964e-17 -0.9690772891044617 +v -0.19814614951610565 2.4265904097698378e-17 -0.9801725149154663 +v -0.1490422636270523 1.8252413541001174e-17 -0.9888308048248291 +v -0.09956784546375275 1.2193544683079923e-17 -0.9950307607650757 +v -0.04984588548541069 6.104360482889217e-18 -0.9987569451332092 +v -0.04960586503148079 -0.004885751288384199 0.9987569451332092 +v -0.0990884006023407 -0.009759355336427689 0.9950307607650757 +v -0.14832459390163422 -0.014608697034418583 0.9888308048248291 +v -0.1971920132637024 -0.019421719014644623 0.9801725149154663 +v -0.24556919932365417 -0.024186454713344574 0.9690772891044617 +v -0.29333585500717163 -0.028891058638691902 0.955572783946991 +v -0.3403732180595398 -0.033523835241794586 0.9396926164627075 +v -0.38656437397003174 -0.03807326778769493 0.9214761853218079 +v -0.4317944645881653 -0.04252804443240166 0.9009688496589661 +v -0.47595104575157166 -0.04687708616256714 0.8782215714454651 +v -0.518924355506897 -0.05110958591103554 0.853290855884552 +v -0.5606074929237366 -0.0552150197327137 0.826238751411438 +v -0.600896954536438 -0.05918318033218384 0.7971324920654297 +v -0.6396924257278442 -0.06300420314073563 0.7660444378852844 +v -0.6768975257873535 -0.06666858494281769 0.7330518960952759 +v -0.7124197483062744 -0.07016722112894058 0.6982368230819702 +v -0.7461708188056946 -0.0734914094209671 0.6616858243942261 +v -0.7780667543411255 -0.07663288712501526 0.6234897971153259 +v -0.8080282807350159 -0.07958383858203888 0.583743691444397 +v -0.8359809517860413 -0.08233693987131119 0.542546272277832 +v -0.8618552684783936 -0.08488533645868301 0.5 +v -0.8855868577957153 -0.08722268790006638 0.4562106430530548 +v -0.907116711139679 -0.08934319764375687 0.4112870991230011 +v -0.9263913631439209 -0.09124158322811127 0.36534103751182556 +v -0.943362832069397 -0.09291312843561172 0.31848666071891785 +v -0.957988977432251 -0.09435367584228516 0.27084046602249146 +v -0.9702333807945251 -0.09555964916944504 0.22252093255519867 +v -0.980065643787384 -0.09652803838253021 0.1736481785774231 +v -0.9874613285064697 -0.09725645184516907 0.12434370815753937 +v -0.9924020171165466 -0.09774306416511536 0.07473009079694748 +v -0.994875431060791 -0.09798667579889297 0.024930691346526146 +v -0.994875431060791 -0.09798667579889297 -0.024930691346526146 +v -0.9924020171165466 -0.09774306416511536 -0.07473009079694748 +v -0.9874613285064697 -0.09725645184516907 -0.12434370815753937 +v -0.980065643787384 -0.09652803838253021 -0.1736481785774231 +v -0.9702333807945251 -0.09555964916944504 -0.22252093255519867 +v -0.957988977432251 -0.09435367584228516 -0.27084046602249146 +v -0.943362832069397 -0.09291312843561172 -0.31848666071891785 +v -0.9263913631439209 -0.09124158322811127 -0.36534103751182556 +v -0.907116711139679 -0.08934319764375687 -0.4112870991230011 +v -0.8855868577957153 -0.08722268790006638 -0.4562106430530548 +v -0.8618552684783936 -0.08488533645868301 -0.5 +v -0.8359809517860413 -0.08233693987131119 -0.542546272277832 +v -0.8080282807350159 -0.07958383858203888 -0.583743691444397 +v -0.7780667543411255 -0.07663288712501526 -0.6234897971153259 +v -0.7461708188056946 -0.0734914094209671 -0.6616858243942261 +v -0.7124197483062744 -0.07016722112894058 -0.6982368230819702 +v -0.6768975257873535 -0.06666858494281769 -0.7330518960952759 +v -0.6396924257278442 -0.06300420314073563 -0.7660444378852844 +v -0.600896954536438 -0.05918318033218384 -0.7971324920654297 +v -0.5606074929237366 -0.0552150197327137 -0.826238751411438 +v -0.518924355506897 -0.05110958591103554 -0.853290855884552 +v -0.47595104575157166 -0.04687708616256714 -0.8782215714454651 +v -0.4317944645881653 -0.04252804443240166 -0.9009688496589661 +v -0.38656437397003174 -0.03807326778769493 -0.9214761853218079 +v -0.3403732180595398 -0.033523835241794586 -0.9396926164627075 +v -0.29333585500717163 -0.028891058638691902 -0.955572783946991 +v -0.24556919932365417 -0.024186454713344574 -0.9690772891044617 +v -0.1971920132637024 -0.019421719014644623 -0.9801725149154663 +v -0.14832459390163422 -0.014608697034418583 -0.9888308048248291 +v -0.0990884006023407 -0.009759355336427689 -0.9950307607650757 +v -0.04960586503148079 -0.004885751288384199 -0.9987569451332092 +v -0.048888109624385834 -0.009724450297653675 0.9987569451332092 +v -0.09765467792749405 -0.019424723461270332 0.9950307607650757 +v -0.1461784541606903 -0.029076702892780304 0.9888308048248291 +v -0.1943388134241104 -0.03865639492869377 0.9801725149154663 +v -0.2420160174369812 -0.04813998192548752 0.9690772891044617 +v -0.2890915274620056 -0.05750388279557228 0.955572783946991 +v -0.33544832468032837 -0.06672482192516327 0.9396926164627075 +v -0.38097113370895386 -0.07577987015247345 0.9214761853218079 +v -0.425546795129776 -0.08464651554822922 0.9009688496589661 +v -0.46906447410583496 -0.09330271929502487 0.8782215714454651 +v -0.511415958404541 -0.1017269641160965 0.853290855884552 +v -0.5524960160255432 -0.10989829152822495 0.826238751411438 +v -0.5922024846076965 -0.1177963986992836 0.7971324920654297 +v -0.6304365992546082 -0.12540164589881897 0.7660444378852844 +v -0.6671034097671509 -0.13269512355327606 0.7330518960952759 +v -0.7021116614341736 -0.13965868949890137 0.6982368230819702 +v -0.735374391078949 -0.14627505838871002 0.6616858243942261 +v -0.766808807849884 -0.15252774953842163 0.6234897971153259 +v -0.796336829662323 -0.1584012508392334 0.583743691444397 +v -0.8238850831985474 -0.16388092935085297 0.542546272277832 +v -0.8493849635124207 -0.16895318031311035 0.5 +v -0.8727731704711914 -0.17360538244247437 0.4562106430530548 +v -0.8939915299415588 -0.17782597243785858 0.4112870991230011 +v -0.9129872918128967 -0.18160445988178253 0.36534103751182556 +v -0.9297131896018982 -0.1849314570426941 0.31848666071891785 +v -0.9441276788711548 -0.18779867887496948 0.27084046602249146 +v -0.9561949372291565 -0.19019900262355804 0.22252093255519867 +v -0.9658849239349365 -0.19212646782398224 0.1736481785774231 +v -0.9731736183166504 -0.19357626140117645 0.12434370815753937 +v -0.9780427813529968 -0.19454480707645416 0.07473009079694748 +v -0.980480432510376 -0.19502969086170197 0.024930691346526146 +v -0.980480432510376 -0.19502969086170197 -0.024930691346526146 +v -0.9780427813529968 -0.19454480707645416 -0.07473009079694748 +v -0.9731736183166504 -0.19357626140117645 -0.12434370815753937 +v -0.9658849239349365 -0.19212646782398224 -0.1736481785774231 +v -0.9561949372291565 -0.19019900262355804 -0.22252093255519867 +v -0.9441276788711548 -0.18779867887496948 -0.27084046602249146 +v -0.9297131896018982 -0.1849314570426941 -0.31848666071891785 +v -0.9129872918128967 -0.18160445988178253 -0.36534103751182556 +v -0.8939915299415588 -0.17782597243785858 -0.4112870991230011 +v -0.8727731704711914 -0.17360538244247437 -0.4562106430530548 +v -0.8493849635124207 -0.16895318031311035 -0.5 +v -0.8238850831985474 -0.16388092935085297 -0.542546272277832 +v -0.796336829662323 -0.1584012508392334 -0.583743691444397 +v -0.766808807849884 -0.15252774953842163 -0.6234897971153259 +v -0.735374391078949 -0.14627505838871002 -0.6616858243942261 +v -0.7021116614341736 -0.13965868949890137 -0.6982368230819702 +v -0.6671034097671509 -0.13269512355327606 -0.7330518960952759 +v -0.6304365992546082 -0.12540164589881897 -0.7660444378852844 +v -0.5922024846076965 -0.1177963986992836 -0.7971324920654297 +v -0.5524960160255432 -0.10989829152822495 -0.826238751411438 +v -0.511415958404541 -0.1017269641160965 -0.853290855884552 +v -0.46906447410583496 -0.09330271929502487 -0.8782215714454651 +v -0.425546795129776 -0.08464651554822922 -0.9009688496589661 +v -0.38097113370895386 -0.07577987015247345 -0.9214761853218079 +v -0.33544832468032837 -0.06672482192516327 -0.9396926164627075 +v -0.2890915274620056 -0.05750388279557228 -0.955572783946991 +v -0.2420160174369812 -0.04813998192548752 -0.9690772891044617 +v -0.1943388134241104 -0.03865639492869377 -0.9801725149154663 +v -0.1461784541606903 -0.029076702892780304 -0.9888308048248291 +v -0.09765467792749405 -0.019424723461270332 -0.9950307607650757 +v -0.048888109624385834 -0.009724450297653675 -0.9987569451332092 +v -0.04769953712821007 -0.01446949690580368 0.9987569451332092 +v -0.0952804908156395 -0.028903020545840263 0.9950307607650757 +v -0.14262455701828003 -0.043264687061309814 0.9888308048248291 +v -0.18961404263973236 -0.05751878768205643 0.9801725149154663 +v -0.23613210022449493 -0.07162988930940628 0.9690772891044617 +v -0.2820631265640259 -0.08556290715932846 0.955572783946991 +v -0.32729285955429077 -0.09928320348262787 0.9396926164627075 +v -0.3717089295387268 -0.11275666952133179 0.9214761853218079 +v -0.4152008593082428 -0.12594980001449585 0.9009688496589661 +v -0.4576605260372162 -0.1388297975063324 0.8782215714454651 +v -0.49898236989974976 -0.15136465430259705 0.853290855884552 +v -0.5390636920928955 -0.16352318227291107 0.826238751411438 +v -0.5778048038482666 -0.17527516186237335 0.7971324920654297 +v -0.615109384059906 -0.18659138679504395 0.7660444378852844 +v -0.650884747505188 -0.19744372367858887 0.7330518960952759 +v -0.6850418448448181 -0.20780517160892487 0.6982368230819702 +v -0.717495858669281 -0.21764999628067017 0.6616858243942261 +v -0.7481660842895508 -0.22695370018482208 0.6234897971153259 +v -0.7769762277603149 -0.23569315671920776 0.583743691444397 +v -0.8038547039031982 -0.24384665489196777 0.542546272277832 +v -0.8287346363067627 -0.2513939142227173 0.5 +v -0.8515542149543762 -0.25831615924835205 0.4562106430530548 +v -0.8722566962242126 -0.26459619402885437 0.4112870991230011 +v -0.8907906413078308 -0.2702183723449707 0.36534103751182556 +v -0.9071099162101746 -0.275168776512146 0.31848666071891785 +v -0.9211739897727966 -0.27943506836891174 0.27084046602249146 +v -0.9329478144645691 -0.2830066382884979 0.22252093255519867 +v -0.9424022436141968 -0.285874605178833 0.1736481785774231 +v -0.9495137333869934 -0.28803184628486633 0.12434370815753937 +v -0.9542645215988159 -0.2894729971885681 0.07473009079694748 +v -0.9566429257392883 -0.29019445180892944 0.024930691346526146 +v -0.9566429257392883 -0.29019445180892944 -0.024930691346526146 +v -0.9542645215988159 -0.2894729971885681 -0.07473009079694748 +v -0.9495137333869934 -0.28803184628486633 -0.12434370815753937 +v -0.9424022436141968 -0.285874605178833 -0.1736481785774231 +v -0.9329478144645691 -0.2830066382884979 -0.22252093255519867 +v -0.9211739897727966 -0.27943506836891174 -0.27084046602249146 +v -0.9071099162101746 -0.275168776512146 -0.31848666071891785 +v -0.8907906413078308 -0.2702183723449707 -0.36534103751182556 +v -0.8722566962242126 -0.26459619402885437 -0.4112870991230011 +v -0.8515542149543762 -0.25831615924835205 -0.4562106430530548 +v -0.8287346363067627 -0.2513939142227173 -0.5 +v -0.8038547039031982 -0.24384665489196777 -0.542546272277832 +v -0.7769762277603149 -0.23569315671920776 -0.583743691444397 +v -0.7481660842895508 -0.22695370018482208 -0.6234897971153259 +v -0.717495858669281 -0.21764999628067017 -0.6616858243942261 +v -0.6850418448448181 -0.20780517160892487 -0.6982368230819702 +v -0.650884747505188 -0.19744372367858887 -0.7330518960952759 +v -0.615109384059906 -0.18659138679504395 -0.7660444378852844 +v -0.5778048038482666 -0.17527516186237335 -0.7971324920654297 +v -0.5390636920928955 -0.16352318227291107 -0.826238751411438 +v -0.49898236989974976 -0.15136465430259705 -0.853290855884552 +v -0.4576605260372162 -0.1388297975063324 -0.8782215714454651 +v -0.4152008593082428 -0.12594980001449585 -0.9009688496589661 +v -0.3717089295387268 -0.11275666952133179 -0.9214761853218079 +v -0.32729285955429077 -0.09928320348262787 -0.9396926164627075 +v -0.2820631265640259 -0.08556290715932846 -0.955572783946991 +v -0.23613210022449493 -0.07162988930940628 -0.9690772891044617 +v -0.18961404263973236 -0.05751878768205643 -0.9801725149154663 +v -0.14262455701828003 -0.043264687061309814 -0.9888308048248291 +v -0.0952804908156395 -0.028903020545840263 -0.9950307607650757 +v -0.04769953712821007 -0.01446949690580368 -0.9987569451332092 +v -0.046051595360040665 -0.019075194373726845 0.9987569451332092 +v -0.0919886976480484 -0.03810296580195427 0.9950307607650757 +v -0.13769710063934326 -0.05703600496053696 0.9888308048248291 +v -0.18306316435337067 -0.07582724839448929 0.9801725149154663 +v -0.22797410190105438 -0.09442996978759766 0.9690772891044617 +v -0.2723182737827301 -0.11279792338609695 0.955572783946991 +v -0.31598541140556335 -0.13088543713092804 0.9396926164627075 +v -0.35886695981025696 -0.14864756166934967 0.9214761853218079 +v -0.4008563160896301 -0.16604012250900269 0.9009688496589661 +v -0.4418490529060364 -0.18301987648010254 0.8782215714454651 +v -0.48174330592155457 -0.19954460859298706 0.853290855884552 +v -0.520439863204956 -0.215573251247406 0.826238751411438 +v -0.5578425526618958 -0.23106594383716583 0.7971324920654297 +v -0.5938583016395569 -0.24598416686058044 0.7660444378852844 +v -0.6283976435661316 -0.26029083132743835 0.7330518960952759 +v -0.6613747477531433 -0.27395036816596985 0.6982368230819702 +v -0.6927074790000916 -0.28692883253097534 0.6616858243942261 +v -0.7223181128501892 -0.29919394850730896 0.6234897971153259 +v -0.7501329183578491 -0.3107152283191681 0.583743691444397 +v -0.7760827541351318 -0.32146400213241577 0.542546272277832 +v -0.8001031279563904 -0.3314135670661926 0.5 +v -0.8221343755722046 -0.34053918719291687 0.4562106430530548 +v -0.8421216011047363 -0.34881818294525146 0.4112870991230011 +v -0.8600152134895325 -0.3562299609184265 0.36534103751182556 +v -0.8757706880569458 -0.3627561032772064 0.31848666071891785 +v -0.8893488645553589 -0.3683803379535675 0.27084046602249146 +v -0.9007159471511841 -0.3730887472629547 0.22252093255519867 +v -0.9098437428474426 -0.3768696188926697 0.1736481785774231 +v -0.9167094826698303 -0.3797135055065155 0.12434370815753937 +v -0.9212961792945862 -0.3816133737564087 0.07473009079694748 +v -0.9235923886299133 -0.3825644850730896 0.024930691346526146 +v -0.9235923886299133 -0.3825644850730896 -0.024930691346526146 +v -0.9212961792945862 -0.3816133737564087 -0.07473009079694748 +v -0.9167094826698303 -0.3797135055065155 -0.12434370815753937 +v -0.9098437428474426 -0.3768696188926697 -0.1736481785774231 +v -0.9007159471511841 -0.3730887472629547 -0.22252093255519867 +v -0.8893488645553589 -0.3683803379535675 -0.27084046602249146 +v -0.8757706880569458 -0.3627561032772064 -0.31848666071891785 +v -0.8600152134895325 -0.3562299609184265 -0.36534103751182556 +v -0.8421216011047363 -0.34881818294525146 -0.4112870991230011 +v -0.8221343755722046 -0.34053918719291687 -0.4562106430530548 +v -0.8001031279563904 -0.3314135670661926 -0.5 +v -0.7760827541351318 -0.32146400213241577 -0.542546272277832 +v -0.7501329183578491 -0.3107152283191681 -0.583743691444397 +v -0.7223181128501892 -0.29919394850730896 -0.6234897971153259 +v -0.6927074790000916 -0.28692883253097534 -0.6616858243942261 +v -0.6613747477531433 -0.27395036816596985 -0.6982368230819702 +v -0.6283976435661316 -0.26029083132743835 -0.7330518960952759 +v -0.5938583016395569 -0.24598416686058044 -0.7660444378852844 +v -0.5578425526618958 -0.23106594383716583 -0.7971324920654297 +v -0.520439863204956 -0.215573251247406 -0.826238751411438 +v -0.48174330592155457 -0.19954460859298706 -0.853290855884552 +v -0.4418490529060364 -0.18301987648010254 -0.8782215714454651 +v -0.4008563160896301 -0.16604012250900269 -0.9009688496589661 +v -0.35886695981025696 -0.14864756166934967 -0.9214761853218079 +v -0.31598541140556335 -0.13088543713092804 -0.9396926164627075 +v -0.2723182737827301 -0.11279792338609695 -0.955572783946991 +v -0.22797410190105438 -0.09442996978759766 -0.9690772891044617 +v -0.18306316435337067 -0.07582724839448929 -0.9801725149154663 +v -0.13769710063934326 -0.05703600496053696 -0.9888308048248291 +v -0.0919886976480484 -0.03810296580195427 -0.9950307607650757 +v -0.046051595360040665 -0.019075194373726845 -0.9987569451332092 +v -0.043960146605968475 -0.023497188463807106 0.9987569451332092 +v -0.08781100064516068 -0.04693595692515373 0.9950307607650757 +v -0.1314435452222824 -0.07025803625583649 0.9888308048248291 +v -0.17474929988384247 -0.09340544790029526 0.9801725149154663 +v -0.2176205962896347 -0.11632063239812851 0.9690772891044617 +v -0.2599508464336395 -0.13894662261009216 0.955572783946991 +v -0.30163484811782837 -0.16122718155384064 0.9396926164627075 +v -0.34256890416145325 -0.1831068992614746 0.9214761853218079 +v -0.38265129923820496 -0.20453137159347534 0.9009688496589661 +v -0.42178234457969666 -0.2254473716020584 0.8782215714454651 +v -0.4598647952079773 -0.2458028495311737 0.853290855884552 +v -0.4968039393424988 -0.2655472457408905 0.826238751411438 +v -0.5325079560279846 -0.2846314311027527 0.7971324920654297 +v -0.5668880343437195 -0.30300799012184143 0.7660444378852844 +v -0.599858820438385 -0.320631206035614 0.7330518960952759 +v -0.6313381791114807 -0.3374572992324829 0.6982368230819702 +v -0.661247968673706 -0.3534444272518158 0.6616858243942261 +v -0.6895138025283813 -0.3685528039932251 0.6234897971153259 +v -0.7160654067993164 -0.3827449381351471 0.583743691444397 +v -0.7408367395401001 -0.39598548412323 0.542546272277832 +v -0.7637662291526794 -0.40824154019355774 0.5 +v -0.7847968935966492 -0.41948267817497253 0.4562106430530548 +v -0.8038763999938965 -0.42968088388442993 0.4112870991230011 +v -0.820957362651825 -0.4388108551502228 0.36534103751182556 +v -0.8359972834587097 -0.44684985280036926 0.31848666071891785 +v -0.8489587903022766 -0.453777939081192 0.27084046602249146 +v -0.8598096370697021 -0.4595778286457062 0.22252093255519867 +v -0.8685228824615479 -0.4642351567745209 0.1736481785774231 +v -0.8750768303871155 -0.4677383303642273 0.12434370815753937 +v -0.8794552087783813 -0.47007861733436584 0.07473009079694748 +v -0.8816471695899963 -0.4712502062320709 0.024930691346526146 +v -0.8816471695899963 -0.4712502062320709 -0.024930691346526146 +v -0.8794552087783813 -0.47007861733436584 -0.07473009079694748 +v -0.8750768303871155 -0.4677383303642273 -0.12434370815753937 +v -0.8685228824615479 -0.4642351567745209 -0.1736481785774231 +v -0.8598096370697021 -0.4595778286457062 -0.22252093255519867 +v -0.8489587903022766 -0.453777939081192 -0.27084046602249146 +v -0.8359972834587097 -0.44684985280036926 -0.31848666071891785 +v -0.820957362651825 -0.4388108551502228 -0.36534103751182556 +v -0.8038763999938965 -0.42968088388442993 -0.4112870991230011 +v -0.7847968935966492 -0.41948267817497253 -0.4562106430530548 +v -0.7637662291526794 -0.40824154019355774 -0.5 +v -0.7408367395401001 -0.39598548412323 -0.542546272277832 +v -0.7160654067993164 -0.3827449381351471 -0.583743691444397 +v -0.6895138025283813 -0.3685528039932251 -0.6234897971153259 +v -0.661247968673706 -0.3534444272518158 -0.6616858243942261 +v -0.6313381791114807 -0.3374572992324829 -0.6982368230819702 +v -0.599858820438385 -0.320631206035614 -0.7330518960952759 +v -0.5668880343437195 -0.30300799012184143 -0.7660444378852844 +v -0.5325079560279846 -0.2846314311027527 -0.7971324920654297 +v -0.4968039393424988 -0.2655472457408905 -0.826238751411438 +v -0.4598647952079773 -0.2458028495311737 -0.853290855884552 +v -0.42178234457969666 -0.2254473716020584 -0.8782215714454651 +v -0.38265129923820496 -0.20453137159347534 -0.9009688496589661 +v -0.34256890416145325 -0.1831068992614746 -0.9214761853218079 +v -0.30163484811782837 -0.16122718155384064 -0.9396926164627075 +v -0.2599508464336395 -0.13894662261009216 -0.955572783946991 +v -0.2176205962896347 -0.11632063239812851 -0.9690772891044617 +v -0.17474929988384247 -0.09340544790029526 -0.9801725149154663 +v -0.1314435452222824 -0.07025803625583649 -0.9888308048248291 +v -0.08781100064516068 -0.04693595692515373 -0.9950307607650757 +v -0.043960146605968475 -0.023497188463807106 -0.9987569451332092 +v -0.04144534096121788 -0.0276928897947073 0.9987569451332092 +v -0.0827876403927803 -0.05531693249940872 0.9950307607650757 +v -0.12392411381006241 -0.08280344307422638 0.9888308048248291 +v -0.16475249826908112 -0.11008410155773163 0.9801725149154663 +v -0.20517127215862274 -0.13709107041358948 0.9690772891044617 +v -0.24507996439933777 -0.16375720500946045 0.955572783946991 +v -0.28437936305999756 -0.19001621007919312 0.9396926164627075 +v -0.32297173142433167 -0.21580280363559723 0.9214761853218079 +v -0.3607611358165741 -0.24105289578437805 0.9009688496589661 +v -0.39765363931655884 -0.2657036781311035 0.8782215714454651 +v -0.43355754017829895 -0.2896938920021057 0.853290855884552 +v -0.4683835208415985 -0.3129638433456421 0.826238751411438 +v -0.5020450353622437 -0.3354557454586029 0.7971324920654297 +v -0.534458339214325 -0.35711365938186646 0.7660444378852844 +v -0.5655429363250732 -0.3778837323188782 0.7330518960952759 +v -0.5952215194702148 -0.3977143168449402 0.6982368230819702 +v -0.6234202980995178 -0.41655611991882324 0.6616858243942261 +v -0.6500691175460815 -0.4343622922897339 0.6234897971153259 +v -0.6751017570495605 -0.4510885775089264 0.583743691444397 +v -0.6984560489654541 -0.4666934013366699 0.542546272277832 +v -0.7200738191604614 -0.4811379313468933 0.5 +v -0.7399013638496399 -0.4943862855434418 0.4562106430530548 +v -0.7578893899917603 -0.5064055323600769 0.4112870991230011 +v -0.7739932537078857 -0.5171657204627991 0.36534103751182556 +v -0.7881727814674377 -0.5266402363777161 0.31848666071891785 +v -0.8003928065299988 -0.5348053574562073 0.27084046602249146 +v -0.8106229305267334 -0.5416409373283386 0.22252093255519867 +v -0.8188377022743225 -0.5471298694610596 0.1736481785774231 +v -0.8250167369842529 -0.5512585639953613 0.12434370815753937 +v -0.8291446566581726 -0.5540167689323425 0.07473009079694748 +v -0.8312111496925354 -0.5553975701332092 0.024930691346526146 +v -0.8312111496925354 -0.5553975701332092 -0.024930691346526146 +v -0.8291446566581726 -0.5540167689323425 -0.07473009079694748 +v -0.8250167369842529 -0.5512585639953613 -0.12434370815753937 +v -0.8188377022743225 -0.5471298694610596 -0.1736481785774231 +v -0.8106229305267334 -0.5416409373283386 -0.22252093255519867 +v -0.8003928065299988 -0.5348053574562073 -0.27084046602249146 +v -0.7881727814674377 -0.5266402363777161 -0.31848666071891785 +v -0.7739932537078857 -0.5171657204627991 -0.36534103751182556 +v -0.7578893899917603 -0.5064055323600769 -0.4112870991230011 +v -0.7399013638496399 -0.4943862855434418 -0.4562106430530548 +v -0.7200738191604614 -0.4811379313468933 -0.5 +v -0.6984560489654541 -0.4666934013366699 -0.542546272277832 +v -0.6751017570495605 -0.4510885775089264 -0.583743691444397 +v -0.6500691175460815 -0.4343622922897339 -0.6234897971153259 +v -0.6234202980995178 -0.41655611991882324 -0.6616858243942261 +v -0.5952215194702148 -0.3977143168449402 -0.6982368230819702 +v -0.5655429363250732 -0.3778837323188782 -0.7330518960952759 +v -0.534458339214325 -0.35711365938186646 -0.7660444378852844 +v -0.5020450353622437 -0.3354557454586029 -0.7971324920654297 +v -0.4683835208415985 -0.3129638433456421 -0.826238751411438 +v -0.43355754017829895 -0.2896938920021057 -0.853290855884552 +v -0.39765363931655884 -0.2657036781311035 -0.8782215714454651 +v -0.3607611358165741 -0.24105289578437805 -0.9009688496589661 +v -0.32297173142433167 -0.21580280363559723 -0.9214761853218079 +v -0.28437936305999756 -0.19001621007919312 -0.9396926164627075 +v -0.24507996439933777 -0.16375720500946045 -0.955572783946991 +v -0.20517127215862274 -0.13709107041358948 -0.9690772891044617 +v -0.16475249826908112 -0.11008410155773163 -0.9801725149154663 +v -0.12392411381006241 -0.08280344307422638 -0.9888308048248291 +v -0.0827876403927803 -0.05531693249940872 -0.9950307607650757 +v -0.04144534096121788 -0.0276928897947073 -0.9987569451332092 +v -0.03853138908743858 -0.03162189573049545 0.9987569451332092 +v -0.07696698606014252 -0.06316517293453217 0.9950307607650757 +v -0.11521122604608536 -0.09455141425132751 0.9888308048248291 +v -0.15316903591156006 -0.12570258975028992 0.9801725149154663 +v -0.19074605405330658 -0.15654124319553375 0.9690772891044617 +v -0.2278488278388977 -0.18699070811271667 0.955572783946991 +v -0.2643851339817047 -0.21697528660297394 0.9396926164627075 +v -0.3002641499042511 -0.24642042815685272 0.9214761853218079 +v -0.3353966772556305 -0.27525293827056885 0.9009688496589661 +v -0.3696953356266022 -0.3034011125564575 0.8782215714454651 +v -0.40307486057281494 -0.3307949900627136 0.853290855884552 +v -0.43545228242874146 -0.3573664724826813 0.826238751411438 +v -0.46674713492393494 -0.38304945826530457 0.7971324920654297 +v -0.49688154458999634 -0.40778014063835144 0.7660444378852844 +v -0.5257806181907654 -0.4314970076084137 0.7330518960952759 +v -0.5533725619316101 -0.45414111018180847 0.6982368230819702 +v -0.5795887112617493 -0.4756561517715454 0.6616858243942261 +v -0.6043639183044434 -0.4959886372089386 0.6234897971153259 +v -0.6276365518569946 -0.5150880217552185 0.583743691444397 +v -0.649348795413971 -0.5329068303108215 0.542546272277832 +v -0.6694467067718506 -0.5494006872177124 0.5 +v -0.6878802180290222 -0.5645287036895752 0.4562106430530548 +v -0.7046035528182983 -0.5782532095909119 0.4112870991230011 +v -0.7195751667022705 -0.5905400514602661 0.36534103751182556 +v -0.7327577471733093 -0.6013587713241577 0.31848666071891785 +v -0.7441186308860779 -0.6106823682785034 0.27084046602249146 +v -0.7536294460296631 -0.6184877157211304 0.22252093255519867 +v -0.7612667083740234 -0.6247554421424866 0.1736481785774231 +v -0.767011284828186 -0.6294698715209961 0.12434370815753937 +v -0.7708489298820496 -0.6326193809509277 0.07473009079694748 +v -0.7727701663970947 -0.6341961026191711 0.024930691346526146 +v -0.7727701663970947 -0.6341961026191711 -0.024930691346526146 +v -0.7708489298820496 -0.6326193809509277 -0.07473009079694748 +v -0.767011284828186 -0.6294698715209961 -0.12434370815753937 +v -0.7612667083740234 -0.6247554421424866 -0.1736481785774231 +v -0.7536294460296631 -0.6184877157211304 -0.22252093255519867 +v -0.7441186308860779 -0.6106823682785034 -0.27084046602249146 +v -0.7327577471733093 -0.6013587713241577 -0.31848666071891785 +v -0.7195751667022705 -0.5905400514602661 -0.36534103751182556 +v -0.7046035528182983 -0.5782532095909119 -0.4112870991230011 +v -0.6878802180290222 -0.5645287036895752 -0.4562106430530548 +v -0.6694467067718506 -0.5494006872177124 -0.5 +v -0.649348795413971 -0.5329068303108215 -0.542546272277832 +v -0.6276365518569946 -0.5150880217552185 -0.583743691444397 +v -0.6043639183044434 -0.4959886372089386 -0.6234897971153259 +v -0.5795887112617493 -0.4756561517715454 -0.6616858243942261 +v -0.5533725619316101 -0.45414111018180847 -0.6982368230819702 +v -0.5257806181907654 -0.4314970076084137 -0.7330518960952759 +v -0.49688154458999634 -0.40778014063835144 -0.7660444378852844 +v -0.46674713492393494 -0.38304945826530457 -0.7971324920654297 +v -0.43545228242874146 -0.3573664724826813 -0.826238751411438 +v -0.40307486057281494 -0.3307949900627136 -0.853290855884552 +v -0.3696953356266022 -0.3034011125564575 -0.8782215714454651 +v -0.3353966772556305 -0.27525293827056885 -0.9009688496589661 +v -0.3002641499042511 -0.24642042815685272 -0.9214761853218079 +v -0.2643851339817047 -0.21697528660297394 -0.9396926164627075 +v -0.2278488278388977 -0.18699070811271667 -0.955572783946991 +v -0.19074605405330658 -0.15654124319553375 -0.9690772891044617 +v -0.15316903591156006 -0.12570258975028992 -0.9801725149154663 +v -0.11521122604608536 -0.09455141425132751 -0.9888308048248291 +v -0.07696698606014252 -0.06316517293453217 -0.9950307607650757 +v -0.03853138908743858 -0.03162189573049545 -0.9987569451332092 +v -0.035246364772319794 -0.035246364772319794 0.9987569451332092 +v -0.07040509581565857 -0.07040509581565857 0.9950307607650757 +v -0.10538879781961441 -0.10538879781961441 0.9888308048248291 +v -0.14011047780513763 -0.14011047780513763 0.9801725149154663 +v -0.17448383569717407 -0.17448383569717407 0.9690772891044617 +v -0.20842337608337402 -0.20842337608337402 0.955572783946991 +v -0.2418447583913803 -0.2418447583913803 0.9396926164627075 +v -0.27466487884521484 -0.27466487884521484 0.9214761853218079 +v -0.3068021237850189 -0.3068021237850189 0.9009688496589661 +v -0.3381766378879547 -0.3381766378879547 0.8782215714454651 +v -0.36871036887168884 -0.36871036887168884 0.853290855884552 +v -0.39832744002342224 -0.39832744002342224 0.826238751411438 +v -0.4269541800022125 -0.4269541800022125 0.7971324920654297 +v -0.45451948046684265 -0.45451948046684265 0.7660444378852844 +v -0.48095476627349854 -0.48095476627349854 0.7330518960952759 +v -0.5061942934989929 -0.5061942934989929 0.6982368230819702 +v -0.5301753878593445 -0.5301753878593445 0.6616858243942261 +v -0.5528383255004883 -0.5528383255004883 0.6234897971153259 +v -0.5741268992424011 -0.5741268992424011 0.583743691444397 +v -0.5939880013465881 -0.5939880013465881 0.542546272277832 +v -0.6123724579811096 -0.6123724579811096 0.5 +v -0.6292343735694885 -0.6292343735694885 0.4562106430530548 +v -0.6445319652557373 -0.6445319652557373 0.4112870991230011 +v -0.6582271456718445 -0.6582271456718445 0.36534103751182556 +v -0.6702858805656433 -0.6702858805656433 0.31848666071891785 +v -0.680678129196167 -0.680678129196167 0.27084046602249146 +v -0.6893781423568726 -0.6893781423568726 0.22252093255519867 +v -0.6963642239570618 -0.6963642239570618 0.1736481785774231 +v -0.7016190886497498 -0.7016190886497498 0.12434370815753937 +v -0.7051295638084412 -0.7051295638084412 0.07473009079694748 +v -0.7068870067596436 -0.7068870067596436 0.024930691346526146 +v -0.7068870067596436 -0.7068870067596436 -0.024930691346526146 +v -0.7051295638084412 -0.7051295638084412 -0.07473009079694748 +v -0.7016190886497498 -0.7016190886497498 -0.12434370815753937 +v -0.6963642239570618 -0.6963642239570618 -0.1736481785774231 +v -0.6893781423568726 -0.6893781423568726 -0.22252093255519867 +v -0.680678129196167 -0.680678129196167 -0.27084046602249146 +v -0.6702858805656433 -0.6702858805656433 -0.31848666071891785 +v -0.6582271456718445 -0.6582271456718445 -0.36534103751182556 +v -0.6445319652557373 -0.6445319652557373 -0.4112870991230011 +v -0.6292343735694885 -0.6292343735694885 -0.4562106430530548 +v -0.6123724579811096 -0.6123724579811096 -0.5 +v -0.5939880013465881 -0.5939880013465881 -0.542546272277832 +v -0.5741268992424011 -0.5741268992424011 -0.583743691444397 +v -0.5528383255004883 -0.5528383255004883 -0.6234897971153259 +v -0.5301753878593445 -0.5301753878593445 -0.6616858243942261 +v -0.5061942934989929 -0.5061942934989929 -0.6982368230819702 +v -0.48095476627349854 -0.48095476627349854 -0.7330518960952759 +v -0.45451948046684265 -0.45451948046684265 -0.7660444378852844 +v -0.4269541800022125 -0.4269541800022125 -0.7971324920654297 +v -0.39832744002342224 -0.39832744002342224 -0.826238751411438 +v -0.36871036887168884 -0.36871036887168884 -0.853290855884552 +v -0.3381766378879547 -0.3381766378879547 -0.8782215714454651 +v -0.3068021237850189 -0.3068021237850189 -0.9009688496589661 +v -0.27466487884521484 -0.27466487884521484 -0.9214761853218079 +v -0.2418447583913803 -0.2418447583913803 -0.9396926164627075 +v -0.20842337608337402 -0.20842337608337402 -0.955572783946991 +v -0.17448383569717407 -0.17448383569717407 -0.9690772891044617 +v -0.14011047780513763 -0.14011047780513763 -0.9801725149154663 +v -0.10538879781961441 -0.10538879781961441 -0.9888308048248291 +v -0.07040509581565857 -0.07040509581565857 -0.9950307607650757 +v -0.035246364772319794 -0.035246364772319794 -0.9987569451332092 +v -0.03162189573049545 -0.03853138908743858 0.9987569451332092 +v -0.06316517293453217 -0.07696698606014252 0.9950307607650757 +v -0.09455141425132751 -0.11521122604608536 0.9888308048248291 +v -0.12570258975028992 -0.15316903591156006 0.9801725149154663 +v -0.15654124319553375 -0.19074605405330658 0.9690772891044617 +v -0.18699070811271667 -0.2278488278388977 0.955572783946991 +v -0.21697528660297394 -0.2643851339817047 0.9396926164627075 +v -0.24642042815685272 -0.3002641499042511 0.9214761853218079 +v -0.27525293827056885 -0.3353966772556305 0.9009688496589661 +v -0.3034011125564575 -0.3696953356266022 0.8782215714454651 +v -0.3307949900627136 -0.40307486057281494 0.853290855884552 +v -0.3573664724826813 -0.43545228242874146 0.826238751411438 +v -0.38304945826530457 -0.46674713492393494 0.7971324920654297 +v -0.40778014063835144 -0.49688154458999634 0.7660444378852844 +v -0.4314970076084137 -0.5257806181907654 0.7330518960952759 +v -0.45414111018180847 -0.5533725619316101 0.6982368230819702 +v -0.4756561517715454 -0.5795887112617493 0.6616858243942261 +v -0.4959886372089386 -0.6043639183044434 0.6234897971153259 +v -0.5150880217552185 -0.6276365518569946 0.583743691444397 +v -0.5329068303108215 -0.649348795413971 0.542546272277832 +v -0.5494006872177124 -0.6694467067718506 0.5 +v -0.5645287036895752 -0.6878802180290222 0.4562106430530548 +v -0.5782532095909119 -0.7046035528182983 0.4112870991230011 +v -0.5905400514602661 -0.7195751667022705 0.36534103751182556 +v -0.6013587713241577 -0.7327577471733093 0.31848666071891785 +v -0.6106823682785034 -0.7441186308860779 0.27084046602249146 +v -0.6184877157211304 -0.7536294460296631 0.22252093255519867 +v -0.6247554421424866 -0.7612667083740234 0.1736481785774231 +v -0.6294698715209961 -0.767011284828186 0.12434370815753937 +v -0.6326193809509277 -0.7708489298820496 0.07473009079694748 +v -0.6341961026191711 -0.7727701663970947 0.024930691346526146 +v -0.6341961026191711 -0.7727701663970947 -0.024930691346526146 +v -0.6326193809509277 -0.7708489298820496 -0.07473009079694748 +v -0.6294698715209961 -0.767011284828186 -0.12434370815753937 +v -0.6247554421424866 -0.7612667083740234 -0.1736481785774231 +v -0.6184877157211304 -0.7536294460296631 -0.22252093255519867 +v -0.6106823682785034 -0.7441186308860779 -0.27084046602249146 +v -0.6013587713241577 -0.7327577471733093 -0.31848666071891785 +v -0.5905400514602661 -0.7195751667022705 -0.36534103751182556 +v -0.5782532095909119 -0.7046035528182983 -0.4112870991230011 +v -0.5645287036895752 -0.6878802180290222 -0.4562106430530548 +v -0.5494006872177124 -0.6694467067718506 -0.5 +v -0.5329068303108215 -0.649348795413971 -0.542546272277832 +v -0.5150880217552185 -0.6276365518569946 -0.583743691444397 +v -0.4959886372089386 -0.6043639183044434 -0.6234897971153259 +v -0.4756561517715454 -0.5795887112617493 -0.6616858243942261 +v -0.45414111018180847 -0.5533725619316101 -0.6982368230819702 +v -0.4314970076084137 -0.5257806181907654 -0.7330518960952759 +v -0.40778014063835144 -0.49688154458999634 -0.7660444378852844 +v -0.38304945826530457 -0.46674713492393494 -0.7971324920654297 +v -0.3573664724826813 -0.43545228242874146 -0.826238751411438 +v -0.3307949900627136 -0.40307486057281494 -0.853290855884552 +v -0.3034011125564575 -0.3696953356266022 -0.8782215714454651 +v -0.27525293827056885 -0.3353966772556305 -0.9009688496589661 +v -0.24642042815685272 -0.3002641499042511 -0.9214761853218079 +v -0.21697528660297394 -0.2643851339817047 -0.9396926164627075 +v -0.18699070811271667 -0.2278488278388977 -0.955572783946991 +v -0.15654124319553375 -0.19074605405330658 -0.9690772891044617 +v -0.12570258975028992 -0.15316903591156006 -0.9801725149154663 +v -0.09455141425132751 -0.11521122604608536 -0.9888308048248291 +v -0.06316517293453217 -0.07696698606014252 -0.9950307607650757 +v -0.03162189573049545 -0.03853138908743858 -0.9987569451332092 +v -0.0276928897947073 -0.04144534096121788 0.9987569451332092 +v -0.05531693249940872 -0.0827876403927803 0.9950307607650757 +v -0.08280344307422638 -0.12392411381006241 0.9888308048248291 +v -0.11008410155773163 -0.16475249826908112 0.9801725149154663 +v -0.13709107041358948 -0.20517127215862274 0.9690772891044617 +v -0.16375720500946045 -0.24507996439933777 0.955572783946991 +v -0.19001621007919312 -0.28437936305999756 0.9396926164627075 +v -0.21580280363559723 -0.32297173142433167 0.9214761853218079 +v -0.24105289578437805 -0.3607611358165741 0.9009688496589661 +v -0.2657036781311035 -0.39765363931655884 0.8782215714454651 +v -0.2896938920021057 -0.43355754017829895 0.853290855884552 +v -0.3129638433456421 -0.4683835208415985 0.826238751411438 +v -0.3354557454586029 -0.5020450353622437 0.7971324920654297 +v -0.35711365938186646 -0.534458339214325 0.7660444378852844 +v -0.3778837323188782 -0.5655429363250732 0.7330518960952759 +v -0.3977143168449402 -0.5952215194702148 0.6982368230819702 +v -0.41655611991882324 -0.6234202980995178 0.6616858243942261 +v -0.4343622922897339 -0.6500691175460815 0.6234897971153259 +v -0.4510885775089264 -0.6751017570495605 0.583743691444397 +v -0.4666934013366699 -0.6984560489654541 0.542546272277832 +v -0.4811379313468933 -0.7200738191604614 0.5 +v -0.4943862855434418 -0.7399013638496399 0.4562106430530548 +v -0.5064055323600769 -0.7578893899917603 0.4112870991230011 +v -0.5171657204627991 -0.7739932537078857 0.36534103751182556 +v -0.5266402363777161 -0.7881727814674377 0.31848666071891785 +v -0.5348053574562073 -0.8003928065299988 0.27084046602249146 +v -0.5416409373283386 -0.8106229305267334 0.22252093255519867 +v -0.5471298694610596 -0.8188377022743225 0.1736481785774231 +v -0.5512585639953613 -0.8250167369842529 0.12434370815753937 +v -0.5540167689323425 -0.8291446566581726 0.07473009079694748 +v -0.5553975701332092 -0.8312111496925354 0.024930691346526146 +v -0.5553975701332092 -0.8312111496925354 -0.024930691346526146 +v -0.5540167689323425 -0.8291446566581726 -0.07473009079694748 +v -0.5512585639953613 -0.8250167369842529 -0.12434370815753937 +v -0.5471298694610596 -0.8188377022743225 -0.1736481785774231 +v -0.5416409373283386 -0.8106229305267334 -0.22252093255519867 +v -0.5348053574562073 -0.8003928065299988 -0.27084046602249146 +v -0.5266402363777161 -0.7881727814674377 -0.31848666071891785 +v -0.5171657204627991 -0.7739932537078857 -0.36534103751182556 +v -0.5064055323600769 -0.7578893899917603 -0.4112870991230011 +v -0.4943862855434418 -0.7399013638496399 -0.4562106430530548 +v -0.4811379313468933 -0.7200738191604614 -0.5 +v -0.4666934013366699 -0.6984560489654541 -0.542546272277832 +v -0.4510885775089264 -0.6751017570495605 -0.583743691444397 +v -0.4343622922897339 -0.6500691175460815 -0.6234897971153259 +v -0.41655611991882324 -0.6234202980995178 -0.6616858243942261 +v -0.3977143168449402 -0.5952215194702148 -0.6982368230819702 +v -0.3778837323188782 -0.5655429363250732 -0.7330518960952759 +v -0.35711365938186646 -0.534458339214325 -0.7660444378852844 +v -0.3354557454586029 -0.5020450353622437 -0.7971324920654297 +v -0.3129638433456421 -0.4683835208415985 -0.826238751411438 +v -0.2896938920021057 -0.43355754017829895 -0.853290855884552 +v -0.2657036781311035 -0.39765363931655884 -0.8782215714454651 +v -0.24105289578437805 -0.3607611358165741 -0.9009688496589661 +v -0.21580280363559723 -0.32297173142433167 -0.9214761853218079 +v -0.19001621007919312 -0.28437936305999756 -0.9396926164627075 +v -0.16375720500946045 -0.24507996439933777 -0.955572783946991 +v -0.13709107041358948 -0.20517127215862274 -0.9690772891044617 +v -0.11008410155773163 -0.16475249826908112 -0.9801725149154663 +v -0.08280344307422638 -0.12392411381006241 -0.9888308048248291 +v -0.05531693249940872 -0.0827876403927803 -0.9950307607650757 +v -0.0276928897947073 -0.04144534096121788 -0.9987569451332092 +v -0.023497188463807106 -0.043960146605968475 0.9987569451332092 +v -0.04693595692515373 -0.08781100064516068 0.9950307607650757 +v -0.07025803625583649 -0.1314435452222824 0.9888308048248291 +v -0.09340544790029526 -0.17474929988384247 0.9801725149154663 +v -0.11632063239812851 -0.2176205962896347 0.9690772891044617 +v -0.13894662261009216 -0.2599508464336395 0.955572783946991 +v -0.16122718155384064 -0.30163484811782837 0.9396926164627075 +v -0.1831068992614746 -0.34256890416145325 0.9214761853218079 +v -0.20453137159347534 -0.38265129923820496 0.9009688496589661 +v -0.2254473716020584 -0.42178234457969666 0.8782215714454651 +v -0.2458028495311737 -0.4598647952079773 0.853290855884552 +v -0.2655472457408905 -0.4968039393424988 0.826238751411438 +v -0.2846314311027527 -0.5325079560279846 0.7971324920654297 +v -0.30300799012184143 -0.5668880343437195 0.7660444378852844 +v -0.320631206035614 -0.599858820438385 0.7330518960952759 +v -0.3374572992324829 -0.6313381791114807 0.6982368230819702 +v -0.3534444272518158 -0.661247968673706 0.6616858243942261 +v -0.3685528039932251 -0.6895138025283813 0.6234897971153259 +v -0.3827449381351471 -0.7160654067993164 0.583743691444397 +v -0.39598548412323 -0.7408367395401001 0.542546272277832 +v -0.40824154019355774 -0.7637662291526794 0.5 +v -0.41948267817497253 -0.7847968935966492 0.4562106430530548 +v -0.42968088388442993 -0.8038763999938965 0.4112870991230011 +v -0.4388108551502228 -0.820957362651825 0.36534103751182556 +v -0.44684985280036926 -0.8359972834587097 0.31848666071891785 +v -0.453777939081192 -0.8489587903022766 0.27084046602249146 +v -0.4595778286457062 -0.8598096370697021 0.22252093255519867 +v -0.4642351567745209 -0.8685228824615479 0.1736481785774231 +v -0.4677383303642273 -0.8750768303871155 0.12434370815753937 +v -0.47007861733436584 -0.8794552087783813 0.07473009079694748 +v -0.4712502062320709 -0.8816471695899963 0.024930691346526146 +v -0.4712502062320709 -0.8816471695899963 -0.024930691346526146 +v -0.47007861733436584 -0.8794552087783813 -0.07473009079694748 +v -0.4677383303642273 -0.8750768303871155 -0.12434370815753937 +v -0.4642351567745209 -0.8685228824615479 -0.1736481785774231 +v -0.4595778286457062 -0.8598096370697021 -0.22252093255519867 +v -0.453777939081192 -0.8489587903022766 -0.27084046602249146 +v -0.44684985280036926 -0.8359972834587097 -0.31848666071891785 +v -0.4388108551502228 -0.820957362651825 -0.36534103751182556 +v -0.42968088388442993 -0.8038763999938965 -0.4112870991230011 +v -0.41948267817497253 -0.7847968935966492 -0.4562106430530548 +v -0.40824154019355774 -0.7637662291526794 -0.5 +v -0.39598548412323 -0.7408367395401001 -0.542546272277832 +v -0.3827449381351471 -0.7160654067993164 -0.583743691444397 +v -0.3685528039932251 -0.6895138025283813 -0.6234897971153259 +v -0.3534444272518158 -0.661247968673706 -0.6616858243942261 +v -0.3374572992324829 -0.6313381791114807 -0.6982368230819702 +v -0.320631206035614 -0.599858820438385 -0.7330518960952759 +v -0.30300799012184143 -0.5668880343437195 -0.7660444378852844 +v -0.2846314311027527 -0.5325079560279846 -0.7971324920654297 +v -0.2655472457408905 -0.4968039393424988 -0.826238751411438 +v -0.2458028495311737 -0.4598647952079773 -0.853290855884552 +v -0.2254473716020584 -0.42178234457969666 -0.8782215714454651 +v -0.20453137159347534 -0.38265129923820496 -0.9009688496589661 +v -0.1831068992614746 -0.34256890416145325 -0.9214761853218079 +v -0.16122718155384064 -0.30163484811782837 -0.9396926164627075 +v -0.13894662261009216 -0.2599508464336395 -0.955572783946991 +v -0.11632063239812851 -0.2176205962896347 -0.9690772891044617 +v -0.09340544790029526 -0.17474929988384247 -0.9801725149154663 +v -0.07025803625583649 -0.1314435452222824 -0.9888308048248291 +v -0.04693595692515373 -0.08781100064516068 -0.9950307607650757 +v -0.023497188463807106 -0.043960146605968475 -0.9987569451332092 +v -0.019075194373726845 -0.046051595360040665 0.9987569451332092 +v -0.03810296580195427 -0.0919886976480484 0.9950307607650757 +v -0.05703600496053696 -0.13769710063934326 0.9888308048248291 +v -0.07582724839448929 -0.18306316435337067 0.9801725149154663 +v -0.09442996978759766 -0.22797410190105438 0.9690772891044617 +v -0.11279792338609695 -0.2723182737827301 0.955572783946991 +v -0.13088543713092804 -0.31598541140556335 0.9396926164627075 +v -0.14864756166934967 -0.35886695981025696 0.9214761853218079 +v -0.16604012250900269 -0.4008563160896301 0.9009688496589661 +v -0.18301987648010254 -0.4418490529060364 0.8782215714454651 +v -0.19954460859298706 -0.48174330592155457 0.853290855884552 +v -0.215573251247406 -0.520439863204956 0.826238751411438 +v -0.23106594383716583 -0.5578425526618958 0.7971324920654297 +v -0.24598416686058044 -0.5938583016395569 0.7660444378852844 +v -0.26029083132743835 -0.6283976435661316 0.7330518960952759 +v -0.27395036816596985 -0.6613747477531433 0.6982368230819702 +v -0.28692883253097534 -0.6927074790000916 0.6616858243942261 +v -0.29919394850730896 -0.7223181128501892 0.6234897971153259 +v -0.3107152283191681 -0.7501329183578491 0.583743691444397 +v -0.32146400213241577 -0.7760827541351318 0.542546272277832 +v -0.3314135670661926 -0.8001031279563904 0.5 +v -0.34053918719291687 -0.8221343755722046 0.4562106430530548 +v -0.34881818294525146 -0.8421216011047363 0.4112870991230011 +v -0.3562299609184265 -0.8600152134895325 0.36534103751182556 +v -0.3627561032772064 -0.8757706880569458 0.31848666071891785 +v -0.3683803379535675 -0.8893488645553589 0.27084046602249146 +v -0.3730887472629547 -0.9007159471511841 0.22252093255519867 +v -0.3768696188926697 -0.9098437428474426 0.1736481785774231 +v -0.3797135055065155 -0.9167094826698303 0.12434370815753937 +v -0.3816133737564087 -0.9212961792945862 0.07473009079694748 +v -0.3825644850730896 -0.9235923886299133 0.024930691346526146 +v -0.3825644850730896 -0.9235923886299133 -0.024930691346526146 +v -0.3816133737564087 -0.9212961792945862 -0.07473009079694748 +v -0.3797135055065155 -0.9167094826698303 -0.12434370815753937 +v -0.3768696188926697 -0.9098437428474426 -0.1736481785774231 +v -0.3730887472629547 -0.9007159471511841 -0.22252093255519867 +v -0.3683803379535675 -0.8893488645553589 -0.27084046602249146 +v -0.3627561032772064 -0.8757706880569458 -0.31848666071891785 +v -0.3562299609184265 -0.8600152134895325 -0.36534103751182556 +v -0.34881818294525146 -0.8421216011047363 -0.4112870991230011 +v -0.34053918719291687 -0.8221343755722046 -0.4562106430530548 +v -0.3314135670661926 -0.8001031279563904 -0.5 +v -0.32146400213241577 -0.7760827541351318 -0.542546272277832 +v -0.3107152283191681 -0.7501329183578491 -0.583743691444397 +v -0.29919394850730896 -0.7223181128501892 -0.6234897971153259 +v -0.28692883253097534 -0.6927074790000916 -0.6616858243942261 +v -0.27395036816596985 -0.6613747477531433 -0.6982368230819702 +v -0.26029083132743835 -0.6283976435661316 -0.7330518960952759 +v -0.24598416686058044 -0.5938583016395569 -0.7660444378852844 +v -0.23106594383716583 -0.5578425526618958 -0.7971324920654297 +v -0.215573251247406 -0.520439863204956 -0.826238751411438 +v -0.19954460859298706 -0.48174330592155457 -0.853290855884552 +v -0.18301987648010254 -0.4418490529060364 -0.8782215714454651 +v -0.16604012250900269 -0.4008563160896301 -0.9009688496589661 +v -0.14864756166934967 -0.35886695981025696 -0.9214761853218079 +v -0.13088543713092804 -0.31598541140556335 -0.9396926164627075 +v -0.11279792338609695 -0.2723182737827301 -0.955572783946991 +v -0.09442996978759766 -0.22797410190105438 -0.9690772891044617 +v -0.07582724839448929 -0.18306316435337067 -0.9801725149154663 +v -0.05703600496053696 -0.13769710063934326 -0.9888308048248291 +v -0.03810296580195427 -0.0919886976480484 -0.9950307607650757 +v -0.019075194373726845 -0.046051595360040665 -0.9987569451332092 +v -0.01446949690580368 -0.04769953712821007 0.9987569451332092 +v -0.028903020545840263 -0.0952804908156395 0.9950307607650757 +v -0.043264687061309814 -0.14262455701828003 0.9888308048248291 +v -0.05751878768205643 -0.18961404263973236 0.9801725149154663 +v -0.07162988930940628 -0.23613210022449493 0.9690772891044617 +v -0.08556290715932846 -0.2820631265640259 0.955572783946991 +v -0.09928320348262787 -0.32729285955429077 0.9396926164627075 +v -0.11275666952133179 -0.3717089295387268 0.9214761853218079 +v -0.12594980001449585 -0.4152008593082428 0.9009688496589661 +v -0.1388297975063324 -0.4576605260372162 0.8782215714454651 +v -0.15136465430259705 -0.49898236989974976 0.853290855884552 +v -0.16352318227291107 -0.5390636920928955 0.826238751411438 +v -0.17527516186237335 -0.5778048038482666 0.7971324920654297 +v -0.18659138679504395 -0.615109384059906 0.7660444378852844 +v -0.19744372367858887 -0.650884747505188 0.7330518960952759 +v -0.20780517160892487 -0.6850418448448181 0.6982368230819702 +v -0.21764999628067017 -0.717495858669281 0.6616858243942261 +v -0.22695370018482208 -0.7481660842895508 0.6234897971153259 +v -0.23569315671920776 -0.7769762277603149 0.583743691444397 +v -0.24384665489196777 -0.8038547039031982 0.542546272277832 +v -0.2513939142227173 -0.8287346363067627 0.5 +v -0.25831615924835205 -0.8515542149543762 0.4562106430530548 +v -0.26459619402885437 -0.8722566962242126 0.4112870991230011 +v -0.2702183723449707 -0.8907906413078308 0.36534103751182556 +v -0.275168776512146 -0.9071099162101746 0.31848666071891785 +v -0.27943506836891174 -0.9211739897727966 0.27084046602249146 +v -0.2830066382884979 -0.9329478144645691 0.22252093255519867 +v -0.285874605178833 -0.9424022436141968 0.1736481785774231 +v -0.28803184628486633 -0.9495137333869934 0.12434370815753937 +v -0.2894729971885681 -0.9542645215988159 0.07473009079694748 +v -0.29019445180892944 -0.9566429257392883 0.024930691346526146 +v -0.29019445180892944 -0.9566429257392883 -0.024930691346526146 +v -0.2894729971885681 -0.9542645215988159 -0.07473009079694748 +v -0.28803184628486633 -0.9495137333869934 -0.12434370815753937 +v -0.285874605178833 -0.9424022436141968 -0.1736481785774231 +v -0.2830066382884979 -0.9329478144645691 -0.22252093255519867 +v -0.27943506836891174 -0.9211739897727966 -0.27084046602249146 +v -0.275168776512146 -0.9071099162101746 -0.31848666071891785 +v -0.2702183723449707 -0.8907906413078308 -0.36534103751182556 +v -0.26459619402885437 -0.8722566962242126 -0.4112870991230011 +v -0.25831615924835205 -0.8515542149543762 -0.4562106430530548 +v -0.2513939142227173 -0.8287346363067627 -0.5 +v -0.24384665489196777 -0.8038547039031982 -0.542546272277832 +v -0.23569315671920776 -0.7769762277603149 -0.583743691444397 +v -0.22695370018482208 -0.7481660842895508 -0.6234897971153259 +v -0.21764999628067017 -0.717495858669281 -0.6616858243942261 +v -0.20780517160892487 -0.6850418448448181 -0.6982368230819702 +v -0.19744372367858887 -0.650884747505188 -0.7330518960952759 +v -0.18659138679504395 -0.615109384059906 -0.7660444378852844 +v -0.17527516186237335 -0.5778048038482666 -0.7971324920654297 +v -0.16352318227291107 -0.5390636920928955 -0.826238751411438 +v -0.15136465430259705 -0.49898236989974976 -0.853290855884552 +v -0.1388297975063324 -0.4576605260372162 -0.8782215714454651 +v -0.12594980001449585 -0.4152008593082428 -0.9009688496589661 +v -0.11275666952133179 -0.3717089295387268 -0.9214761853218079 +v -0.09928320348262787 -0.32729285955429077 -0.9396926164627075 +v -0.08556290715932846 -0.2820631265640259 -0.955572783946991 +v -0.07162988930940628 -0.23613210022449493 -0.9690772891044617 +v -0.05751878768205643 -0.18961404263973236 -0.9801725149154663 +v -0.043264687061309814 -0.14262455701828003 -0.9888308048248291 +v -0.028903020545840263 -0.0952804908156395 -0.9950307607650757 +v -0.01446949690580368 -0.04769953712821007 -0.9987569451332092 +v -0.009724450297653675 -0.048888109624385834 0.9987569451332092 +v -0.019424723461270332 -0.09765467792749405 0.9950307607650757 +v -0.029076702892780304 -0.1461784541606903 0.9888308048248291 +v -0.03865639492869377 -0.1943388134241104 0.9801725149154663 +v -0.04813998192548752 -0.2420160174369812 0.9690772891044617 +v -0.05750388279557228 -0.2890915274620056 0.955572783946991 +v -0.06672482192516327 -0.33544832468032837 0.9396926164627075 +v -0.07577987015247345 -0.38097113370895386 0.9214761853218079 +v -0.08464651554822922 -0.425546795129776 0.9009688496589661 +v -0.09330271929502487 -0.46906447410583496 0.8782215714454651 +v -0.1017269641160965 -0.511415958404541 0.853290855884552 +v -0.10989829152822495 -0.5524960160255432 0.826238751411438 +v -0.1177963986992836 -0.5922024846076965 0.7971324920654297 +v -0.12540164589881897 -0.6304365992546082 0.7660444378852844 +v -0.13269512355327606 -0.6671034097671509 0.7330518960952759 +v -0.13965868949890137 -0.7021116614341736 0.6982368230819702 +v -0.14627505838871002 -0.735374391078949 0.6616858243942261 +v -0.15252774953842163 -0.766808807849884 0.6234897971153259 +v -0.1584012508392334 -0.796336829662323 0.583743691444397 +v -0.16388092935085297 -0.8238850831985474 0.542546272277832 +v -0.16895318031311035 -0.8493849635124207 0.5 +v -0.17360538244247437 -0.8727731704711914 0.4562106430530548 +v -0.17782597243785858 -0.8939915299415588 0.4112870991230011 +v -0.18160445988178253 -0.9129872918128967 0.36534103751182556 +v -0.1849314570426941 -0.9297131896018982 0.31848666071891785 +v -0.18779867887496948 -0.9441276788711548 0.27084046602249146 +v -0.19019900262355804 -0.9561949372291565 0.22252093255519867 +v -0.19212646782398224 -0.9658849239349365 0.1736481785774231 +v -0.19357626140117645 -0.9731736183166504 0.12434370815753937 +v -0.19454480707645416 -0.9780427813529968 0.07473009079694748 +v -0.19502969086170197 -0.980480432510376 0.024930691346526146 +v -0.19502969086170197 -0.980480432510376 -0.024930691346526146 +v -0.19454480707645416 -0.9780427813529968 -0.07473009079694748 +v -0.19357626140117645 -0.9731736183166504 -0.12434370815753937 +v -0.19212646782398224 -0.9658849239349365 -0.1736481785774231 +v -0.19019900262355804 -0.9561949372291565 -0.22252093255519867 +v -0.18779867887496948 -0.9441276788711548 -0.27084046602249146 +v -0.1849314570426941 -0.9297131896018982 -0.31848666071891785 +v -0.18160445988178253 -0.9129872918128967 -0.36534103751182556 +v -0.17782597243785858 -0.8939915299415588 -0.4112870991230011 +v -0.17360538244247437 -0.8727731704711914 -0.4562106430530548 +v -0.16895318031311035 -0.8493849635124207 -0.5 +v -0.16388092935085297 -0.8238850831985474 -0.542546272277832 +v -0.1584012508392334 -0.796336829662323 -0.583743691444397 +v -0.15252774953842163 -0.766808807849884 -0.6234897971153259 +v -0.14627505838871002 -0.735374391078949 -0.6616858243942261 +v -0.13965868949890137 -0.7021116614341736 -0.6982368230819702 +v -0.13269512355327606 -0.6671034097671509 -0.7330518960952759 +v -0.12540164589881897 -0.6304365992546082 -0.7660444378852844 +v -0.1177963986992836 -0.5922024846076965 -0.7971324920654297 +v -0.10989829152822495 -0.5524960160255432 -0.826238751411438 +v -0.1017269641160965 -0.511415958404541 -0.853290855884552 +v -0.09330271929502487 -0.46906447410583496 -0.8782215714454651 +v -0.08464651554822922 -0.425546795129776 -0.9009688496589661 +v -0.07577987015247345 -0.38097113370895386 -0.9214761853218079 +v -0.06672482192516327 -0.33544832468032837 -0.9396926164627075 +v -0.05750388279557228 -0.2890915274620056 -0.955572783946991 +v -0.04813998192548752 -0.2420160174369812 -0.9690772891044617 +v -0.03865639492869377 -0.1943388134241104 -0.9801725149154663 +v -0.029076702892780304 -0.1461784541606903 -0.9888308048248291 +v -0.019424723461270332 -0.09765467792749405 -0.9950307607650757 +v -0.009724450297653675 -0.048888109624385834 -0.9987569451332092 +v -0.004885751288384199 -0.04960586503148079 0.9987569451332092 +v -0.009759355336427689 -0.0990884006023407 0.9950307607650757 +v -0.014608697034418583 -0.14832459390163422 0.9888308048248291 +v -0.019421719014644623 -0.1971920132637024 0.9801725149154663 +v -0.024186454713344574 -0.24556919932365417 0.9690772891044617 +v -0.028891058638691902 -0.29333585500717163 0.955572783946991 +v -0.033523835241794586 -0.3403732180595398 0.9396926164627075 +v -0.03807326778769493 -0.38656437397003174 0.9214761853218079 +v -0.04252804443240166 -0.4317944645881653 0.9009688496589661 +v -0.04687708616256714 -0.47595104575157166 0.8782215714454651 +v -0.05110958591103554 -0.518924355506897 0.853290855884552 +v -0.0552150197327137 -0.5606074929237366 0.826238751411438 +v -0.05918318033218384 -0.600896954536438 0.7971324920654297 +v -0.06300420314073563 -0.6396924257278442 0.7660444378852844 +v -0.06666858494281769 -0.6768975257873535 0.7330518960952759 +v -0.07016722112894058 -0.7124197483062744 0.6982368230819702 +v -0.0734914094209671 -0.7461708188056946 0.6616858243942261 +v -0.07663288712501526 -0.7780667543411255 0.6234897971153259 +v -0.07958383858203888 -0.8080282807350159 0.583743691444397 +v -0.08233693987131119 -0.8359809517860413 0.542546272277832 +v -0.08488533645868301 -0.8618552684783936 0.5 +v -0.08722268790006638 -0.8855868577957153 0.4562106430530548 +v -0.08934319764375687 -0.907116711139679 0.4112870991230011 +v -0.09124158322811127 -0.9263913631439209 0.36534103751182556 +v -0.09291312843561172 -0.943362832069397 0.31848666071891785 +v -0.09435367584228516 -0.957988977432251 0.27084046602249146 +v -0.09555964916944504 -0.9702333807945251 0.22252093255519867 +v -0.09652803838253021 -0.980065643787384 0.1736481785774231 +v -0.09725645184516907 -0.9874613285064697 0.12434370815753937 +v -0.09774306416511536 -0.9924020171165466 0.07473009079694748 +v -0.09798667579889297 -0.994875431060791 0.024930691346526146 +v -0.09798667579889297 -0.994875431060791 -0.024930691346526146 +v -0.09774306416511536 -0.9924020171165466 -0.07473009079694748 +v -0.09725645184516907 -0.9874613285064697 -0.12434370815753937 +v -0.09652803838253021 -0.980065643787384 -0.1736481785774231 +v -0.09555964916944504 -0.9702333807945251 -0.22252093255519867 +v -0.09435367584228516 -0.957988977432251 -0.27084046602249146 +v -0.09291312843561172 -0.943362832069397 -0.31848666071891785 +v -0.09124158322811127 -0.9263913631439209 -0.36534103751182556 +v -0.08934319764375687 -0.907116711139679 -0.4112870991230011 +v -0.08722268790006638 -0.8855868577957153 -0.4562106430530548 +v -0.08488533645868301 -0.8618552684783936 -0.5 +v -0.08233693987131119 -0.8359809517860413 -0.542546272277832 +v -0.07958383858203888 -0.8080282807350159 -0.583743691444397 +v -0.07663288712501526 -0.7780667543411255 -0.6234897971153259 +v -0.0734914094209671 -0.7461708188056946 -0.6616858243942261 +v -0.07016722112894058 -0.7124197483062744 -0.6982368230819702 +v -0.06666858494281769 -0.6768975257873535 -0.7330518960952759 +v -0.06300420314073563 -0.6396924257278442 -0.7660444378852844 +v -0.05918318033218384 -0.600896954536438 -0.7971324920654297 +v -0.0552150197327137 -0.5606074929237366 -0.826238751411438 +v -0.05110958591103554 -0.518924355506897 -0.853290855884552 +v -0.04687708616256714 -0.47595104575157166 -0.8782215714454651 +v -0.04252804443240166 -0.4317944645881653 -0.9009688496589661 +v -0.03807326778769493 -0.38656437397003174 -0.9214761853218079 +v -0.033523835241794586 -0.3403732180595398 -0.9396926164627075 +v -0.028891058638691902 -0.29333585500717163 -0.955572783946991 +v -0.024186454713344574 -0.24556919932365417 -0.9690772891044617 +v -0.019421719014644623 -0.1971920132637024 -0.9801725149154663 +v -0.014608697034418583 -0.14832459390163422 -0.9888308048248291 +v -0.009759355336427689 -0.0990884006023407 -0.9950307607650757 +v -0.004885751288384199 -0.04960586503148079 -0.9987569451332092 +v -9.156540310743519e-18 -0.04984588548541069 0.9987569451332092 +v -1.829031661102958e-17 -0.09956784546375275 0.9950307607650757 +v -2.7378619484321148e-17 -0.1490422636270523 0.9888308048248291 +v -3.6398855319366954e-17 -0.19814614951610565 0.9801725149154663 +v -4.532859759539946e-17 -0.2467574030160904 0.9690772891044617 +v -5.414564809350019e-17 -0.29475516080856323 0.955572783946991 +v -6.282807990999159e-17 -0.3420201539993286 0.9396926164627075 +v -7.135431355705338e-17 -0.3884347975254059 0.9214761853218079 +v -7.970314674122459e-17 -0.4338837265968323 0.9009688496589661 +v -8.78538337727424e-17 -0.4782539904117584 0.8782215714454651 +v -9.578609218298701e-17 -0.5214352011680603 0.853290855884552 +v -1.0348021522104496e-16 -0.5633200407028198 0.826238751411438 +v -1.1091707185370911e-16 -0.6038044095039368 0.7971324920654297 +v -1.1807816632248278e-16 -0.6427876353263855 0.7660444378852844 +v -1.2494571093547364e-16 -0.6801727414131165 0.7330518960952759 +v -1.3150260621505898e-16 -0.7158668637275696 0.6982368230819702 +v -1.3773257324678376e-16 -0.7497811913490295 0.6616858243942261 +v -1.4362011397469119e-16 -0.7818315029144287 0.6234897971153259 +v -1.4915059061066153e-16 -0.8119379878044128 0.583743691444397 +v -1.543102521041917e-16 -0.8400259017944336 0.542546272277832 +v -1.5908628708195447e-16 -0.8660253882408142 0.5 +v -1.634667973780189e-16 -0.8898718357086182 0.4562106430530548 +v -1.6744090391296865e-16 -0.9115058779716492 0.4112870991230011 +v -1.7099873345901233e-16 -0.9308737516403198 0.36534103751182556 +v -1.7413143187487316e-16 -0.9479273557662964 0.31848666071891785 +v -1.7683120381045843e-16 -0.9626242518424988 0.27084046602249146 +v -1.7909135241152892e-16 -0.9749279022216797 0.22252093255519867 +v -1.8090625284991927e-16 -0.9848077297210693 0.1736481785774231 +v -1.8227137879331758e-16 -0.9922391772270203 0.12434370815753937 +v -1.8318336857971445e-16 -0.9972038269042969 0.07473009079694748 +v -1.8363991933828452e-16 -0.9996891617774963 0.024930691346526146 +v -1.8363991933828452e-16 -0.9996891617774963 -0.024930691346526146 +v -1.8318336857971445e-16 -0.9972038269042969 -0.07473009079694748 +v -1.8227137879331758e-16 -0.9922391772270203 -0.12434370815753937 +v -1.8090625284991927e-16 -0.9848077297210693 -0.1736481785774231 +v -1.7909135241152892e-16 -0.9749279022216797 -0.22252093255519867 +v -1.7683120381045843e-16 -0.9626242518424988 -0.27084046602249146 +v -1.7413143187487316e-16 -0.9479273557662964 -0.31848666071891785 +v -1.7099873345901233e-16 -0.9308737516403198 -0.36534103751182556 +v -1.6744090391296865e-16 -0.9115058779716492 -0.4112870991230011 +v -1.634667973780189e-16 -0.8898718357086182 -0.4562106430530548 +v -1.5908628708195447e-16 -0.8660253882408142 -0.5 +v -1.543102521041917e-16 -0.8400259017944336 -0.542546272277832 +v -1.4915059061066153e-16 -0.8119379878044128 -0.583743691444397 +v -1.4362011397469119e-16 -0.7818315029144287 -0.6234897971153259 +v -1.3773257324678376e-16 -0.7497811913490295 -0.6616858243942261 +v -1.3150260621505898e-16 -0.7158668637275696 -0.6982368230819702 +v -1.2494571093547364e-16 -0.6801727414131165 -0.7330518960952759 +v -1.1807816632248278e-16 -0.6427876353263855 -0.7660444378852844 +v -1.1091707185370911e-16 -0.6038044095039368 -0.7971324920654297 +v -1.0348021522104496e-16 -0.5633200407028198 -0.826238751411438 +v -9.578609218298701e-17 -0.5214352011680603 -0.853290855884552 +v -8.78538337727424e-17 -0.4782539904117584 -0.8782215714454651 +v -7.970314674122459e-17 -0.4338837265968323 -0.9009688496589661 +v -7.135431355705338e-17 -0.3884347975254059 -0.9214761853218079 +v -6.282807990999159e-17 -0.3420201539993286 -0.9396926164627075 +v -5.414564809350019e-17 -0.29475516080856323 -0.955572783946991 +v -4.532859759539946e-17 -0.2467574030160904 -0.9690772891044617 +v -3.6398855319366954e-17 -0.19814614951610565 -0.9801725149154663 +v -2.7378619484321148e-17 -0.1490422636270523 -0.9888308048248291 +v -1.829031661102958e-17 -0.09956784546375275 -0.9950307607650757 +v -9.156540310743519e-18 -0.04984588548541069 -0.9987569451332092 +v 0.004885751288384199 -0.04960586503148079 0.9987569451332092 +v 0.009759355336427689 -0.0990884006023407 0.9950307607650757 +v 0.014608697034418583 -0.14832459390163422 0.9888308048248291 +v 0.019421719014644623 -0.1971920132637024 0.9801725149154663 +v 0.024186454713344574 -0.24556919932365417 0.9690772891044617 +v 0.028891058638691902 -0.29333585500717163 0.955572783946991 +v 0.033523835241794586 -0.3403732180595398 0.9396926164627075 +v 0.03807326778769493 -0.38656437397003174 0.9214761853218079 +v 0.04252804443240166 -0.4317944645881653 0.9009688496589661 +v 0.04687708616256714 -0.47595104575157166 0.8782215714454651 +v 0.05110958591103554 -0.518924355506897 0.853290855884552 +v 0.0552150197327137 -0.5606074929237366 0.826238751411438 +v 0.05918318033218384 -0.600896954536438 0.7971324920654297 +v 0.06300420314073563 -0.6396924257278442 0.7660444378852844 +v 0.06666858494281769 -0.6768975257873535 0.7330518960952759 +v 0.07016722112894058 -0.7124197483062744 0.6982368230819702 +v 0.0734914094209671 -0.7461708188056946 0.6616858243942261 +v 0.07663288712501526 -0.7780667543411255 0.6234897971153259 +v 0.07958383858203888 -0.8080282807350159 0.583743691444397 +v 0.08233693987131119 -0.8359809517860413 0.542546272277832 +v 0.08488533645868301 -0.8618552684783936 0.5 +v 0.08722268790006638 -0.8855868577957153 0.4562106430530548 +v 0.08934319764375687 -0.907116711139679 0.4112870991230011 +v 0.09124158322811127 -0.9263913631439209 0.36534103751182556 +v 0.09291312843561172 -0.943362832069397 0.31848666071891785 +v 0.09435367584228516 -0.957988977432251 0.27084046602249146 +v 0.09555964916944504 -0.9702333807945251 0.22252093255519867 +v 0.09652803838253021 -0.980065643787384 0.1736481785774231 +v 0.09725645184516907 -0.9874613285064697 0.12434370815753937 +v 0.09774306416511536 -0.9924020171165466 0.07473009079694748 +v 0.09798667579889297 -0.994875431060791 0.024930691346526146 +v 0.09798667579889297 -0.994875431060791 -0.024930691346526146 +v 0.09774306416511536 -0.9924020171165466 -0.07473009079694748 +v 0.09725645184516907 -0.9874613285064697 -0.12434370815753937 +v 0.09652803838253021 -0.980065643787384 -0.1736481785774231 +v 0.09555964916944504 -0.9702333807945251 -0.22252093255519867 +v 0.09435367584228516 -0.957988977432251 -0.27084046602249146 +v 0.09291312843561172 -0.943362832069397 -0.31848666071891785 +v 0.09124158322811127 -0.9263913631439209 -0.36534103751182556 +v 0.08934319764375687 -0.907116711139679 -0.4112870991230011 +v 0.08722268790006638 -0.8855868577957153 -0.4562106430530548 +v 0.08488533645868301 -0.8618552684783936 -0.5 +v 0.08233693987131119 -0.8359809517860413 -0.542546272277832 +v 0.07958383858203888 -0.8080282807350159 -0.583743691444397 +v 0.07663288712501526 -0.7780667543411255 -0.6234897971153259 +v 0.0734914094209671 -0.7461708188056946 -0.6616858243942261 +v 0.07016722112894058 -0.7124197483062744 -0.6982368230819702 +v 0.06666858494281769 -0.6768975257873535 -0.7330518960952759 +v 0.06300420314073563 -0.6396924257278442 -0.7660444378852844 +v 0.05918318033218384 -0.600896954536438 -0.7971324920654297 +v 0.0552150197327137 -0.5606074929237366 -0.826238751411438 +v 0.05110958591103554 -0.518924355506897 -0.853290855884552 +v 0.04687708616256714 -0.47595104575157166 -0.8782215714454651 +v 0.04252804443240166 -0.4317944645881653 -0.9009688496589661 +v 0.03807326778769493 -0.38656437397003174 -0.9214761853218079 +v 0.033523835241794586 -0.3403732180595398 -0.9396926164627075 +v 0.028891058638691902 -0.29333585500717163 -0.955572783946991 +v 0.024186454713344574 -0.24556919932365417 -0.9690772891044617 +v 0.019421719014644623 -0.1971920132637024 -0.9801725149154663 +v 0.014608697034418583 -0.14832459390163422 -0.9888308048248291 +v 0.009759355336427689 -0.0990884006023407 -0.9950307607650757 +v 0.004885751288384199 -0.04960586503148079 -0.9987569451332092 +v 0.009724450297653675 -0.048888109624385834 0.9987569451332092 +v 0.019424723461270332 -0.09765467792749405 0.9950307607650757 +v 0.029076702892780304 -0.1461784541606903 0.9888308048248291 +v 0.03865639492869377 -0.1943388134241104 0.9801725149154663 +v 0.04813998192548752 -0.2420160174369812 0.9690772891044617 +v 0.05750388279557228 -0.2890915274620056 0.955572783946991 +v 0.06672482192516327 -0.33544832468032837 0.9396926164627075 +v 0.07577987015247345 -0.38097113370895386 0.9214761853218079 +v 0.08464651554822922 -0.425546795129776 0.9009688496589661 +v 0.09330271929502487 -0.46906447410583496 0.8782215714454651 +v 0.1017269641160965 -0.511415958404541 0.853290855884552 +v 0.10989829152822495 -0.5524960160255432 0.826238751411438 +v 0.1177963986992836 -0.5922024846076965 0.7971324920654297 +v 0.12540164589881897 -0.6304365992546082 0.7660444378852844 +v 0.13269512355327606 -0.6671034097671509 0.7330518960952759 +v 0.13965868949890137 -0.7021116614341736 0.6982368230819702 +v 0.14627505838871002 -0.735374391078949 0.6616858243942261 +v 0.15252774953842163 -0.766808807849884 0.6234897971153259 +v 0.1584012508392334 -0.796336829662323 0.583743691444397 +v 0.16388092935085297 -0.8238850831985474 0.542546272277832 +v 0.16895318031311035 -0.8493849635124207 0.5 +v 0.17360538244247437 -0.8727731704711914 0.4562106430530548 +v 0.17782597243785858 -0.8939915299415588 0.4112870991230011 +v 0.18160445988178253 -0.9129872918128967 0.36534103751182556 +v 0.1849314570426941 -0.9297131896018982 0.31848666071891785 +v 0.18779867887496948 -0.9441276788711548 0.27084046602249146 +v 0.19019900262355804 -0.9561949372291565 0.22252093255519867 +v 0.19212646782398224 -0.9658849239349365 0.1736481785774231 +v 0.19357626140117645 -0.9731736183166504 0.12434370815753937 +v 0.19454480707645416 -0.9780427813529968 0.07473009079694748 +v 0.19502969086170197 -0.980480432510376 0.024930691346526146 +v 0.19502969086170197 -0.980480432510376 -0.024930691346526146 +v 0.19454480707645416 -0.9780427813529968 -0.07473009079694748 +v 0.19357626140117645 -0.9731736183166504 -0.12434370815753937 +v 0.19212646782398224 -0.9658849239349365 -0.1736481785774231 +v 0.19019900262355804 -0.9561949372291565 -0.22252093255519867 +v 0.18779867887496948 -0.9441276788711548 -0.27084046602249146 +v 0.1849314570426941 -0.9297131896018982 -0.31848666071891785 +v 0.18160445988178253 -0.9129872918128967 -0.36534103751182556 +v 0.17782597243785858 -0.8939915299415588 -0.4112870991230011 +v 0.17360538244247437 -0.8727731704711914 -0.4562106430530548 +v 0.16895318031311035 -0.8493849635124207 -0.5 +v 0.16388092935085297 -0.8238850831985474 -0.542546272277832 +v 0.1584012508392334 -0.796336829662323 -0.583743691444397 +v 0.15252774953842163 -0.766808807849884 -0.6234897971153259 +v 0.14627505838871002 -0.735374391078949 -0.6616858243942261 +v 0.13965868949890137 -0.7021116614341736 -0.6982368230819702 +v 0.13269512355327606 -0.6671034097671509 -0.7330518960952759 +v 0.12540164589881897 -0.6304365992546082 -0.7660444378852844 +v 0.1177963986992836 -0.5922024846076965 -0.7971324920654297 +v 0.10989829152822495 -0.5524960160255432 -0.826238751411438 +v 0.1017269641160965 -0.511415958404541 -0.853290855884552 +v 0.09330271929502487 -0.46906447410583496 -0.8782215714454651 +v 0.08464651554822922 -0.425546795129776 -0.9009688496589661 +v 0.07577987015247345 -0.38097113370895386 -0.9214761853218079 +v 0.06672482192516327 -0.33544832468032837 -0.9396926164627075 +v 0.05750388279557228 -0.2890915274620056 -0.955572783946991 +v 0.04813998192548752 -0.2420160174369812 -0.9690772891044617 +v 0.03865639492869377 -0.1943388134241104 -0.9801725149154663 +v 0.029076702892780304 -0.1461784541606903 -0.9888308048248291 +v 0.019424723461270332 -0.09765467792749405 -0.9950307607650757 +v 0.009724450297653675 -0.048888109624385834 -0.9987569451332092 +v 0.01446949690580368 -0.04769953712821007 0.9987569451332092 +v 0.028903020545840263 -0.0952804908156395 0.9950307607650757 +v 0.043264687061309814 -0.14262455701828003 0.9888308048248291 +v 0.05751878768205643 -0.18961404263973236 0.9801725149154663 +v 0.07162988930940628 -0.23613210022449493 0.9690772891044617 +v 0.08556290715932846 -0.2820631265640259 0.955572783946991 +v 0.09928320348262787 -0.32729285955429077 0.9396926164627075 +v 0.11275666952133179 -0.3717089295387268 0.9214761853218079 +v 0.12594980001449585 -0.4152008593082428 0.9009688496589661 +v 0.1388297975063324 -0.4576605260372162 0.8782215714454651 +v 0.15136465430259705 -0.49898236989974976 0.853290855884552 +v 0.16352318227291107 -0.5390636920928955 0.826238751411438 +v 0.17527516186237335 -0.5778048038482666 0.7971324920654297 +v 0.18659138679504395 -0.615109384059906 0.7660444378852844 +v 0.19744372367858887 -0.650884747505188 0.7330518960952759 +v 0.20780517160892487 -0.6850418448448181 0.6982368230819702 +v 0.21764999628067017 -0.717495858669281 0.6616858243942261 +v 0.22695370018482208 -0.7481660842895508 0.6234897971153259 +v 0.23569315671920776 -0.7769762277603149 0.583743691444397 +v 0.24384665489196777 -0.8038547039031982 0.542546272277832 +v 0.2513939142227173 -0.8287346363067627 0.5 +v 0.25831615924835205 -0.8515542149543762 0.4562106430530548 +v 0.26459619402885437 -0.8722566962242126 0.4112870991230011 +v 0.2702183723449707 -0.8907906413078308 0.36534103751182556 +v 0.275168776512146 -0.9071099162101746 0.31848666071891785 +v 0.27943506836891174 -0.9211739897727966 0.27084046602249146 +v 0.2830066382884979 -0.9329478144645691 0.22252093255519867 +v 0.285874605178833 -0.9424022436141968 0.1736481785774231 +v 0.28803184628486633 -0.9495137333869934 0.12434370815753937 +v 0.2894729971885681 -0.9542645215988159 0.07473009079694748 +v 0.29019445180892944 -0.9566429257392883 0.024930691346526146 +v 0.29019445180892944 -0.9566429257392883 -0.024930691346526146 +v 0.2894729971885681 -0.9542645215988159 -0.07473009079694748 +v 0.28803184628486633 -0.9495137333869934 -0.12434370815753937 +v 0.285874605178833 -0.9424022436141968 -0.1736481785774231 +v 0.2830066382884979 -0.9329478144645691 -0.22252093255519867 +v 0.27943506836891174 -0.9211739897727966 -0.27084046602249146 +v 0.275168776512146 -0.9071099162101746 -0.31848666071891785 +v 0.2702183723449707 -0.8907906413078308 -0.36534103751182556 +v 0.26459619402885437 -0.8722566962242126 -0.4112870991230011 +v 0.25831615924835205 -0.8515542149543762 -0.4562106430530548 +v 0.2513939142227173 -0.8287346363067627 -0.5 +v 0.24384665489196777 -0.8038547039031982 -0.542546272277832 +v 0.23569315671920776 -0.7769762277603149 -0.583743691444397 +v 0.22695370018482208 -0.7481660842895508 -0.6234897971153259 +v 0.21764999628067017 -0.717495858669281 -0.6616858243942261 +v 0.20780517160892487 -0.6850418448448181 -0.6982368230819702 +v 0.19744372367858887 -0.650884747505188 -0.7330518960952759 +v 0.18659138679504395 -0.615109384059906 -0.7660444378852844 +v 0.17527516186237335 -0.5778048038482666 -0.7971324920654297 +v 0.16352318227291107 -0.5390636920928955 -0.826238751411438 +v 0.15136465430259705 -0.49898236989974976 -0.853290855884552 +v 0.1388297975063324 -0.4576605260372162 -0.8782215714454651 +v 0.12594980001449585 -0.4152008593082428 -0.9009688496589661 +v 0.11275666952133179 -0.3717089295387268 -0.9214761853218079 +v 0.09928320348262787 -0.32729285955429077 -0.9396926164627075 +v 0.08556290715932846 -0.2820631265640259 -0.955572783946991 +v 0.07162988930940628 -0.23613210022449493 -0.9690772891044617 +v 0.05751878768205643 -0.18961404263973236 -0.9801725149154663 +v 0.043264687061309814 -0.14262455701828003 -0.9888308048248291 +v 0.028903020545840263 -0.0952804908156395 -0.9950307607650757 +v 0.01446949690580368 -0.04769953712821007 -0.9987569451332092 +v 0.019075194373726845 -0.046051595360040665 0.9987569451332092 +v 0.03810296580195427 -0.0919886976480484 0.9950307607650757 +v 0.05703600496053696 -0.13769710063934326 0.9888308048248291 +v 0.07582724839448929 -0.18306316435337067 0.9801725149154663 +v 0.09442996978759766 -0.22797410190105438 0.9690772891044617 +v 0.11279792338609695 -0.2723182737827301 0.955572783946991 +v 0.13088543713092804 -0.31598541140556335 0.9396926164627075 +v 0.14864756166934967 -0.35886695981025696 0.9214761853218079 +v 0.16604012250900269 -0.4008563160896301 0.9009688496589661 +v 0.18301987648010254 -0.4418490529060364 0.8782215714454651 +v 0.19954460859298706 -0.48174330592155457 0.853290855884552 +v 0.215573251247406 -0.520439863204956 0.826238751411438 +v 0.23106594383716583 -0.5578425526618958 0.7971324920654297 +v 0.24598416686058044 -0.5938583016395569 0.7660444378852844 +v 0.26029083132743835 -0.6283976435661316 0.7330518960952759 +v 0.27395036816596985 -0.6613747477531433 0.6982368230819702 +v 0.28692883253097534 -0.6927074790000916 0.6616858243942261 +v 0.29919394850730896 -0.7223181128501892 0.6234897971153259 +v 0.3107152283191681 -0.7501329183578491 0.583743691444397 +v 0.32146400213241577 -0.7760827541351318 0.542546272277832 +v 0.3314135670661926 -0.8001031279563904 0.5 +v 0.34053918719291687 -0.8221343755722046 0.4562106430530548 +v 0.34881818294525146 -0.8421216011047363 0.4112870991230011 +v 0.3562299609184265 -0.8600152134895325 0.36534103751182556 +v 0.3627561032772064 -0.8757706880569458 0.31848666071891785 +v 0.3683803379535675 -0.8893488645553589 0.27084046602249146 +v 0.3730887472629547 -0.9007159471511841 0.22252093255519867 +v 0.3768696188926697 -0.9098437428474426 0.1736481785774231 +v 0.3797135055065155 -0.9167094826698303 0.12434370815753937 +v 0.3816133737564087 -0.9212961792945862 0.07473009079694748 +v 0.3825644850730896 -0.9235923886299133 0.024930691346526146 +v 0.3825644850730896 -0.9235923886299133 -0.024930691346526146 +v 0.3816133737564087 -0.9212961792945862 -0.07473009079694748 +v 0.3797135055065155 -0.9167094826698303 -0.12434370815753937 +v 0.3768696188926697 -0.9098437428474426 -0.1736481785774231 +v 0.3730887472629547 -0.9007159471511841 -0.22252093255519867 +v 0.3683803379535675 -0.8893488645553589 -0.27084046602249146 +v 0.3627561032772064 -0.8757706880569458 -0.31848666071891785 +v 0.3562299609184265 -0.8600152134895325 -0.36534103751182556 +v 0.34881818294525146 -0.8421216011047363 -0.4112870991230011 +v 0.34053918719291687 -0.8221343755722046 -0.4562106430530548 +v 0.3314135670661926 -0.8001031279563904 -0.5 +v 0.32146400213241577 -0.7760827541351318 -0.542546272277832 +v 0.3107152283191681 -0.7501329183578491 -0.583743691444397 +v 0.29919394850730896 -0.7223181128501892 -0.6234897971153259 +v 0.28692883253097534 -0.6927074790000916 -0.6616858243942261 +v 0.27395036816596985 -0.6613747477531433 -0.6982368230819702 +v 0.26029083132743835 -0.6283976435661316 -0.7330518960952759 +v 0.24598416686058044 -0.5938583016395569 -0.7660444378852844 +v 0.23106594383716583 -0.5578425526618958 -0.7971324920654297 +v 0.215573251247406 -0.520439863204956 -0.826238751411438 +v 0.19954460859298706 -0.48174330592155457 -0.853290855884552 +v 0.18301987648010254 -0.4418490529060364 -0.8782215714454651 +v 0.16604012250900269 -0.4008563160896301 -0.9009688496589661 +v 0.14864756166934967 -0.35886695981025696 -0.9214761853218079 +v 0.13088543713092804 -0.31598541140556335 -0.9396926164627075 +v 0.11279792338609695 -0.2723182737827301 -0.955572783946991 +v 0.09442996978759766 -0.22797410190105438 -0.9690772891044617 +v 0.07582724839448929 -0.18306316435337067 -0.9801725149154663 +v 0.05703600496053696 -0.13769710063934326 -0.9888308048248291 +v 0.03810296580195427 -0.0919886976480484 -0.9950307607650757 +v 0.019075194373726845 -0.046051595360040665 -0.9987569451332092 +v 0.023497188463807106 -0.043960146605968475 0.9987569451332092 +v 0.04693595692515373 -0.08781100064516068 0.9950307607650757 +v 0.07025803625583649 -0.1314435452222824 0.9888308048248291 +v 0.09340544790029526 -0.17474929988384247 0.9801725149154663 +v 0.11632063239812851 -0.2176205962896347 0.9690772891044617 +v 0.13894662261009216 -0.2599508464336395 0.955572783946991 +v 0.16122718155384064 -0.30163484811782837 0.9396926164627075 +v 0.1831068992614746 -0.34256890416145325 0.9214761853218079 +v 0.20453137159347534 -0.38265129923820496 0.9009688496589661 +v 0.2254473716020584 -0.42178234457969666 0.8782215714454651 +v 0.2458028495311737 -0.4598647952079773 0.853290855884552 +v 0.2655472457408905 -0.4968039393424988 0.826238751411438 +v 0.2846314311027527 -0.5325079560279846 0.7971324920654297 +v 0.30300799012184143 -0.5668880343437195 0.7660444378852844 +v 0.320631206035614 -0.599858820438385 0.7330518960952759 +v 0.3374572992324829 -0.6313381791114807 0.6982368230819702 +v 0.3534444272518158 -0.661247968673706 0.6616858243942261 +v 0.3685528039932251 -0.6895138025283813 0.6234897971153259 +v 0.3827449381351471 -0.7160654067993164 0.583743691444397 +v 0.39598548412323 -0.7408367395401001 0.542546272277832 +v 0.40824154019355774 -0.7637662291526794 0.5 +v 0.41948267817497253 -0.7847968935966492 0.4562106430530548 +v 0.42968088388442993 -0.8038763999938965 0.4112870991230011 +v 0.4388108551502228 -0.820957362651825 0.36534103751182556 +v 0.44684985280036926 -0.8359972834587097 0.31848666071891785 +v 0.453777939081192 -0.8489587903022766 0.27084046602249146 +v 0.4595778286457062 -0.8598096370697021 0.22252093255519867 +v 0.4642351567745209 -0.8685228824615479 0.1736481785774231 +v 0.4677383303642273 -0.8750768303871155 0.12434370815753937 +v 0.47007861733436584 -0.8794552087783813 0.07473009079694748 +v 0.4712502062320709 -0.8816471695899963 0.024930691346526146 +v 0.4712502062320709 -0.8816471695899963 -0.024930691346526146 +v 0.47007861733436584 -0.8794552087783813 -0.07473009079694748 +v 0.4677383303642273 -0.8750768303871155 -0.12434370815753937 +v 0.4642351567745209 -0.8685228824615479 -0.1736481785774231 +v 0.4595778286457062 -0.8598096370697021 -0.22252093255519867 +v 0.453777939081192 -0.8489587903022766 -0.27084046602249146 +v 0.44684985280036926 -0.8359972834587097 -0.31848666071891785 +v 0.4388108551502228 -0.820957362651825 -0.36534103751182556 +v 0.42968088388442993 -0.8038763999938965 -0.4112870991230011 +v 0.41948267817497253 -0.7847968935966492 -0.4562106430530548 +v 0.40824154019355774 -0.7637662291526794 -0.5 +v 0.39598548412323 -0.7408367395401001 -0.542546272277832 +v 0.3827449381351471 -0.7160654067993164 -0.583743691444397 +v 0.3685528039932251 -0.6895138025283813 -0.6234897971153259 +v 0.3534444272518158 -0.661247968673706 -0.6616858243942261 +v 0.3374572992324829 -0.6313381791114807 -0.6982368230819702 +v 0.320631206035614 -0.599858820438385 -0.7330518960952759 +v 0.30300799012184143 -0.5668880343437195 -0.7660444378852844 +v 0.2846314311027527 -0.5325079560279846 -0.7971324920654297 +v 0.2655472457408905 -0.4968039393424988 -0.826238751411438 +v 0.2458028495311737 -0.4598647952079773 -0.853290855884552 +v 0.2254473716020584 -0.42178234457969666 -0.8782215714454651 +v 0.20453137159347534 -0.38265129923820496 -0.9009688496589661 +v 0.1831068992614746 -0.34256890416145325 -0.9214761853218079 +v 0.16122718155384064 -0.30163484811782837 -0.9396926164627075 +v 0.13894662261009216 -0.2599508464336395 -0.955572783946991 +v 0.11632063239812851 -0.2176205962896347 -0.9690772891044617 +v 0.09340544790029526 -0.17474929988384247 -0.9801725149154663 +v 0.07025803625583649 -0.1314435452222824 -0.9888308048248291 +v 0.04693595692515373 -0.08781100064516068 -0.9950307607650757 +v 0.023497188463807106 -0.043960146605968475 -0.9987569451332092 +v 0.0276928897947073 -0.04144534096121788 0.9987569451332092 +v 0.05531693249940872 -0.0827876403927803 0.9950307607650757 +v 0.08280344307422638 -0.12392411381006241 0.9888308048248291 +v 0.11008410155773163 -0.16475249826908112 0.9801725149154663 +v 0.13709107041358948 -0.20517127215862274 0.9690772891044617 +v 0.16375720500946045 -0.24507996439933777 0.955572783946991 +v 0.19001621007919312 -0.28437936305999756 0.9396926164627075 +v 0.21580280363559723 -0.32297173142433167 0.9214761853218079 +v 0.24105289578437805 -0.3607611358165741 0.9009688496589661 +v 0.2657036781311035 -0.39765363931655884 0.8782215714454651 +v 0.2896938920021057 -0.43355754017829895 0.853290855884552 +v 0.3129638433456421 -0.4683835208415985 0.826238751411438 +v 0.3354557454586029 -0.5020450353622437 0.7971324920654297 +v 0.35711365938186646 -0.534458339214325 0.7660444378852844 +v 0.3778837323188782 -0.5655429363250732 0.7330518960952759 +v 0.3977143168449402 -0.5952215194702148 0.6982368230819702 +v 0.41655611991882324 -0.6234202980995178 0.6616858243942261 +v 0.4343622922897339 -0.6500691175460815 0.6234897971153259 +v 0.4510885775089264 -0.6751017570495605 0.583743691444397 +v 0.4666934013366699 -0.6984560489654541 0.542546272277832 +v 0.4811379313468933 -0.7200738191604614 0.5 +v 0.4943862855434418 -0.7399013638496399 0.4562106430530548 +v 0.5064055323600769 -0.7578893899917603 0.4112870991230011 +v 0.5171657204627991 -0.7739932537078857 0.36534103751182556 +v 0.5266402363777161 -0.7881727814674377 0.31848666071891785 +v 0.5348053574562073 -0.8003928065299988 0.27084046602249146 +v 0.5416409373283386 -0.8106229305267334 0.22252093255519867 +v 0.5471298694610596 -0.8188377022743225 0.1736481785774231 +v 0.5512585639953613 -0.8250167369842529 0.12434370815753937 +v 0.5540167689323425 -0.8291446566581726 0.07473009079694748 +v 0.5553975701332092 -0.8312111496925354 0.024930691346526146 +v 0.5553975701332092 -0.8312111496925354 -0.024930691346526146 +v 0.5540167689323425 -0.8291446566581726 -0.07473009079694748 +v 0.5512585639953613 -0.8250167369842529 -0.12434370815753937 +v 0.5471298694610596 -0.8188377022743225 -0.1736481785774231 +v 0.5416409373283386 -0.8106229305267334 -0.22252093255519867 +v 0.5348053574562073 -0.8003928065299988 -0.27084046602249146 +v 0.5266402363777161 -0.7881727814674377 -0.31848666071891785 +v 0.5171657204627991 -0.7739932537078857 -0.36534103751182556 +v 0.5064055323600769 -0.7578893899917603 -0.4112870991230011 +v 0.4943862855434418 -0.7399013638496399 -0.4562106430530548 +v 0.4811379313468933 -0.7200738191604614 -0.5 +v 0.4666934013366699 -0.6984560489654541 -0.542546272277832 +v 0.4510885775089264 -0.6751017570495605 -0.583743691444397 +v 0.4343622922897339 -0.6500691175460815 -0.6234897971153259 +v 0.41655611991882324 -0.6234202980995178 -0.6616858243942261 +v 0.3977143168449402 -0.5952215194702148 -0.6982368230819702 +v 0.3778837323188782 -0.5655429363250732 -0.7330518960952759 +v 0.35711365938186646 -0.534458339214325 -0.7660444378852844 +v 0.3354557454586029 -0.5020450353622437 -0.7971324920654297 +v 0.3129638433456421 -0.4683835208415985 -0.826238751411438 +v 0.2896938920021057 -0.43355754017829895 -0.853290855884552 +v 0.2657036781311035 -0.39765363931655884 -0.8782215714454651 +v 0.24105289578437805 -0.3607611358165741 -0.9009688496589661 +v 0.21580280363559723 -0.32297173142433167 -0.9214761853218079 +v 0.19001621007919312 -0.28437936305999756 -0.9396926164627075 +v 0.16375720500946045 -0.24507996439933777 -0.955572783946991 +v 0.13709107041358948 -0.20517127215862274 -0.9690772891044617 +v 0.11008410155773163 -0.16475249826908112 -0.9801725149154663 +v 0.08280344307422638 -0.12392411381006241 -0.9888308048248291 +v 0.05531693249940872 -0.0827876403927803 -0.9950307607650757 +v 0.0276928897947073 -0.04144534096121788 -0.9987569451332092 +v 0.03162189573049545 -0.03853138908743858 0.9987569451332092 +v 0.06316517293453217 -0.07696698606014252 0.9950307607650757 +v 0.09455141425132751 -0.11521122604608536 0.9888308048248291 +v 0.12570258975028992 -0.15316903591156006 0.9801725149154663 +v 0.15654124319553375 -0.19074605405330658 0.9690772891044617 +v 0.18699070811271667 -0.2278488278388977 0.955572783946991 +v 0.21697528660297394 -0.2643851339817047 0.9396926164627075 +v 0.24642042815685272 -0.3002641499042511 0.9214761853218079 +v 0.27525293827056885 -0.3353966772556305 0.9009688496589661 +v 0.3034011125564575 -0.3696953356266022 0.8782215714454651 +v 0.3307949900627136 -0.40307486057281494 0.853290855884552 +v 0.3573664724826813 -0.43545228242874146 0.826238751411438 +v 0.38304945826530457 -0.46674713492393494 0.7971324920654297 +v 0.40778014063835144 -0.49688154458999634 0.7660444378852844 +v 0.4314970076084137 -0.5257806181907654 0.7330518960952759 +v 0.45414111018180847 -0.5533725619316101 0.6982368230819702 +v 0.4756561517715454 -0.5795887112617493 0.6616858243942261 +v 0.4959886372089386 -0.6043639183044434 0.6234897971153259 +v 0.5150880217552185 -0.6276365518569946 0.583743691444397 +v 0.5329068303108215 -0.649348795413971 0.542546272277832 +v 0.5494006872177124 -0.6694467067718506 0.5 +v 0.5645287036895752 -0.6878802180290222 0.4562106430530548 +v 0.5782532095909119 -0.7046035528182983 0.4112870991230011 +v 0.5905400514602661 -0.7195751667022705 0.36534103751182556 +v 0.6013587713241577 -0.7327577471733093 0.31848666071891785 +v 0.6106823682785034 -0.7441186308860779 0.27084046602249146 +v 0.6184877157211304 -0.7536294460296631 0.22252093255519867 +v 0.6247554421424866 -0.7612667083740234 0.1736481785774231 +v 0.6294698715209961 -0.767011284828186 0.12434370815753937 +v 0.6326193809509277 -0.7708489298820496 0.07473009079694748 +v 0.6341961026191711 -0.7727701663970947 0.024930691346526146 +v 0.6341961026191711 -0.7727701663970947 -0.024930691346526146 +v 0.6326193809509277 -0.7708489298820496 -0.07473009079694748 +v 0.6294698715209961 -0.767011284828186 -0.12434370815753937 +v 0.6247554421424866 -0.7612667083740234 -0.1736481785774231 +v 0.6184877157211304 -0.7536294460296631 -0.22252093255519867 +v 0.6106823682785034 -0.7441186308860779 -0.27084046602249146 +v 0.6013587713241577 -0.7327577471733093 -0.31848666071891785 +v 0.5905400514602661 -0.7195751667022705 -0.36534103751182556 +v 0.5782532095909119 -0.7046035528182983 -0.4112870991230011 +v 0.5645287036895752 -0.6878802180290222 -0.4562106430530548 +v 0.5494006872177124 -0.6694467067718506 -0.5 +v 0.5329068303108215 -0.649348795413971 -0.542546272277832 +v 0.5150880217552185 -0.6276365518569946 -0.583743691444397 +v 0.4959886372089386 -0.6043639183044434 -0.6234897971153259 +v 0.4756561517715454 -0.5795887112617493 -0.6616858243942261 +v 0.45414111018180847 -0.5533725619316101 -0.6982368230819702 +v 0.4314970076084137 -0.5257806181907654 -0.7330518960952759 +v 0.40778014063835144 -0.49688154458999634 -0.7660444378852844 +v 0.38304945826530457 -0.46674713492393494 -0.7971324920654297 +v 0.3573664724826813 -0.43545228242874146 -0.826238751411438 +v 0.3307949900627136 -0.40307486057281494 -0.853290855884552 +v 0.3034011125564575 -0.3696953356266022 -0.8782215714454651 +v 0.27525293827056885 -0.3353966772556305 -0.9009688496589661 +v 0.24642042815685272 -0.3002641499042511 -0.9214761853218079 +v 0.21697528660297394 -0.2643851339817047 -0.9396926164627075 +v 0.18699070811271667 -0.2278488278388977 -0.955572783946991 +v 0.15654124319553375 -0.19074605405330658 -0.9690772891044617 +v 0.12570258975028992 -0.15316903591156006 -0.9801725149154663 +v 0.09455141425132751 -0.11521122604608536 -0.9888308048248291 +v 0.06316517293453217 -0.07696698606014252 -0.9950307607650757 +v 0.03162189573049545 -0.03853138908743858 -0.9987569451332092 +v 0.035246364772319794 -0.035246364772319794 0.9987569451332092 +v 0.07040509581565857 -0.07040509581565857 0.9950307607650757 +v 0.10538879781961441 -0.10538879781961441 0.9888308048248291 +v 0.14011047780513763 -0.14011047780513763 0.9801725149154663 +v 0.17448383569717407 -0.17448383569717407 0.9690772891044617 +v 0.20842337608337402 -0.20842337608337402 0.955572783946991 +v 0.2418447583913803 -0.2418447583913803 0.9396926164627075 +v 0.27466487884521484 -0.27466487884521484 0.9214761853218079 +v 0.3068021237850189 -0.3068021237850189 0.9009688496589661 +v 0.3381766378879547 -0.3381766378879547 0.8782215714454651 +v 0.36871036887168884 -0.36871036887168884 0.853290855884552 +v 0.39832744002342224 -0.39832744002342224 0.826238751411438 +v 0.4269541800022125 -0.4269541800022125 0.7971324920654297 +v 0.45451948046684265 -0.45451948046684265 0.7660444378852844 +v 0.48095476627349854 -0.48095476627349854 0.7330518960952759 +v 0.5061942934989929 -0.5061942934989929 0.6982368230819702 +v 0.5301753878593445 -0.5301753878593445 0.6616858243942261 +v 0.5528383255004883 -0.5528383255004883 0.6234897971153259 +v 0.5741268992424011 -0.5741268992424011 0.583743691444397 +v 0.5939880013465881 -0.5939880013465881 0.542546272277832 +v 0.6123724579811096 -0.6123724579811096 0.5 +v 0.6292343735694885 -0.6292343735694885 0.4562106430530548 +v 0.6445319652557373 -0.6445319652557373 0.4112870991230011 +v 0.6582271456718445 -0.6582271456718445 0.36534103751182556 +v 0.6702858805656433 -0.6702858805656433 0.31848666071891785 +v 0.680678129196167 -0.680678129196167 0.27084046602249146 +v 0.6893781423568726 -0.6893781423568726 0.22252093255519867 +v 0.6963642239570618 -0.6963642239570618 0.1736481785774231 +v 0.7016190886497498 -0.7016190886497498 0.12434370815753937 +v 0.7051295638084412 -0.7051295638084412 0.07473009079694748 +v 0.7068870067596436 -0.7068870067596436 0.024930691346526146 +v 0.7068870067596436 -0.7068870067596436 -0.024930691346526146 +v 0.7051295638084412 -0.7051295638084412 -0.07473009079694748 +v 0.7016190886497498 -0.7016190886497498 -0.12434370815753937 +v 0.6963642239570618 -0.6963642239570618 -0.1736481785774231 +v 0.6893781423568726 -0.6893781423568726 -0.22252093255519867 +v 0.680678129196167 -0.680678129196167 -0.27084046602249146 +v 0.6702858805656433 -0.6702858805656433 -0.31848666071891785 +v 0.6582271456718445 -0.6582271456718445 -0.36534103751182556 +v 0.6445319652557373 -0.6445319652557373 -0.4112870991230011 +v 0.6292343735694885 -0.6292343735694885 -0.4562106430530548 +v 0.6123724579811096 -0.6123724579811096 -0.5 +v 0.5939880013465881 -0.5939880013465881 -0.542546272277832 +v 0.5741268992424011 -0.5741268992424011 -0.583743691444397 +v 0.5528383255004883 -0.5528383255004883 -0.6234897971153259 +v 0.5301753878593445 -0.5301753878593445 -0.6616858243942261 +v 0.5061942934989929 -0.5061942934989929 -0.6982368230819702 +v 0.48095476627349854 -0.48095476627349854 -0.7330518960952759 +v 0.45451948046684265 -0.45451948046684265 -0.7660444378852844 +v 0.4269541800022125 -0.4269541800022125 -0.7971324920654297 +v 0.39832744002342224 -0.39832744002342224 -0.826238751411438 +v 0.36871036887168884 -0.36871036887168884 -0.853290855884552 +v 0.3381766378879547 -0.3381766378879547 -0.8782215714454651 +v 0.3068021237850189 -0.3068021237850189 -0.9009688496589661 +v 0.27466487884521484 -0.27466487884521484 -0.9214761853218079 +v 0.2418447583913803 -0.2418447583913803 -0.9396926164627075 +v 0.20842337608337402 -0.20842337608337402 -0.955572783946991 +v 0.17448383569717407 -0.17448383569717407 -0.9690772891044617 +v 0.14011047780513763 -0.14011047780513763 -0.9801725149154663 +v 0.10538879781961441 -0.10538879781961441 -0.9888308048248291 +v 0.07040509581565857 -0.07040509581565857 -0.9950307607650757 +v 0.035246364772319794 -0.035246364772319794 -0.9987569451332092 +v 0.03853138908743858 -0.03162189573049545 0.9987569451332092 +v 0.07696698606014252 -0.06316517293453217 0.9950307607650757 +v 0.11521122604608536 -0.09455141425132751 0.9888308048248291 +v 0.15316903591156006 -0.12570258975028992 0.9801725149154663 +v 0.19074605405330658 -0.15654124319553375 0.9690772891044617 +v 0.2278488278388977 -0.18699070811271667 0.955572783946991 +v 0.2643851339817047 -0.21697528660297394 0.9396926164627075 +v 0.3002641499042511 -0.24642042815685272 0.9214761853218079 +v 0.3353966772556305 -0.27525293827056885 0.9009688496589661 +v 0.3696953356266022 -0.3034011125564575 0.8782215714454651 +v 0.40307486057281494 -0.3307949900627136 0.853290855884552 +v 0.43545228242874146 -0.3573664724826813 0.826238751411438 +v 0.46674713492393494 -0.38304945826530457 0.7971324920654297 +v 0.49688154458999634 -0.40778014063835144 0.7660444378852844 +v 0.5257806181907654 -0.4314970076084137 0.7330518960952759 +v 0.5533725619316101 -0.45414111018180847 0.6982368230819702 +v 0.5795887112617493 -0.4756561517715454 0.6616858243942261 +v 0.6043639183044434 -0.4959886372089386 0.6234897971153259 +v 0.6276365518569946 -0.5150880217552185 0.583743691444397 +v 0.649348795413971 -0.5329068303108215 0.542546272277832 +v 0.6694467067718506 -0.5494006872177124 0.5 +v 0.6878802180290222 -0.5645287036895752 0.4562106430530548 +v 0.7046035528182983 -0.5782532095909119 0.4112870991230011 +v 0.7195751667022705 -0.5905400514602661 0.36534103751182556 +v 0.7327577471733093 -0.6013587713241577 0.31848666071891785 +v 0.7441186308860779 -0.6106823682785034 0.27084046602249146 +v 0.7536294460296631 -0.6184877157211304 0.22252093255519867 +v 0.7612667083740234 -0.6247554421424866 0.1736481785774231 +v 0.767011284828186 -0.6294698715209961 0.12434370815753937 +v 0.7708489298820496 -0.6326193809509277 0.07473009079694748 +v 0.7727701663970947 -0.6341961026191711 0.024930691346526146 +v 0.7727701663970947 -0.6341961026191711 -0.024930691346526146 +v 0.7708489298820496 -0.6326193809509277 -0.07473009079694748 +v 0.767011284828186 -0.6294698715209961 -0.12434370815753937 +v 0.7612667083740234 -0.6247554421424866 -0.1736481785774231 +v 0.7536294460296631 -0.6184877157211304 -0.22252093255519867 +v 0.7441186308860779 -0.6106823682785034 -0.27084046602249146 +v 0.7327577471733093 -0.6013587713241577 -0.31848666071891785 +v 0.7195751667022705 -0.5905400514602661 -0.36534103751182556 +v 0.7046035528182983 -0.5782532095909119 -0.4112870991230011 +v 0.6878802180290222 -0.5645287036895752 -0.4562106430530548 +v 0.6694467067718506 -0.5494006872177124 -0.5 +v 0.649348795413971 -0.5329068303108215 -0.542546272277832 +v 0.6276365518569946 -0.5150880217552185 -0.583743691444397 +v 0.6043639183044434 -0.4959886372089386 -0.6234897971153259 +v 0.5795887112617493 -0.4756561517715454 -0.6616858243942261 +v 0.5533725619316101 -0.45414111018180847 -0.6982368230819702 +v 0.5257806181907654 -0.4314970076084137 -0.7330518960952759 +v 0.49688154458999634 -0.40778014063835144 -0.7660444378852844 +v 0.46674713492393494 -0.38304945826530457 -0.7971324920654297 +v 0.43545228242874146 -0.3573664724826813 -0.826238751411438 +v 0.40307486057281494 -0.3307949900627136 -0.853290855884552 +v 0.3696953356266022 -0.3034011125564575 -0.8782215714454651 +v 0.3353966772556305 -0.27525293827056885 -0.9009688496589661 +v 0.3002641499042511 -0.24642042815685272 -0.9214761853218079 +v 0.2643851339817047 -0.21697528660297394 -0.9396926164627075 +v 0.2278488278388977 -0.18699070811271667 -0.955572783946991 +v 0.19074605405330658 -0.15654124319553375 -0.9690772891044617 +v 0.15316903591156006 -0.12570258975028992 -0.9801725149154663 +v 0.11521122604608536 -0.09455141425132751 -0.9888308048248291 +v 0.07696698606014252 -0.06316517293453217 -0.9950307607650757 +v 0.03853138908743858 -0.03162189573049545 -0.9987569451332092 +v 0.04144534096121788 -0.0276928897947073 0.9987569451332092 +v 0.0827876403927803 -0.05531693249940872 0.9950307607650757 +v 0.12392411381006241 -0.08280344307422638 0.9888308048248291 +v 0.16475249826908112 -0.11008410155773163 0.9801725149154663 +v 0.20517127215862274 -0.13709107041358948 0.9690772891044617 +v 0.24507996439933777 -0.16375720500946045 0.955572783946991 +v 0.28437936305999756 -0.19001621007919312 0.9396926164627075 +v 0.32297173142433167 -0.21580280363559723 0.9214761853218079 +v 0.3607611358165741 -0.24105289578437805 0.9009688496589661 +v 0.39765363931655884 -0.2657036781311035 0.8782215714454651 +v 0.43355754017829895 -0.2896938920021057 0.853290855884552 +v 0.4683835208415985 -0.3129638433456421 0.826238751411438 +v 0.5020450353622437 -0.3354557454586029 0.7971324920654297 +v 0.534458339214325 -0.35711365938186646 0.7660444378852844 +v 0.5655429363250732 -0.3778837323188782 0.7330518960952759 +v 0.5952215194702148 -0.3977143168449402 0.6982368230819702 +v 0.6234202980995178 -0.41655611991882324 0.6616858243942261 +v 0.6500691175460815 -0.4343622922897339 0.6234897971153259 +v 0.6751017570495605 -0.4510885775089264 0.583743691444397 +v 0.6984560489654541 -0.4666934013366699 0.542546272277832 +v 0.7200738191604614 -0.4811379313468933 0.5 +v 0.7399013638496399 -0.4943862855434418 0.4562106430530548 +v 0.7578893899917603 -0.5064055323600769 0.4112870991230011 +v 0.7739932537078857 -0.5171657204627991 0.36534103751182556 +v 0.7881727814674377 -0.5266402363777161 0.31848666071891785 +v 0.8003928065299988 -0.5348053574562073 0.27084046602249146 +v 0.8106229305267334 -0.5416409373283386 0.22252093255519867 +v 0.8188377022743225 -0.5471298694610596 0.1736481785774231 +v 0.8250167369842529 -0.5512585639953613 0.12434370815753937 +v 0.8291446566581726 -0.5540167689323425 0.07473009079694748 +v 0.8312111496925354 -0.5553975701332092 0.024930691346526146 +v 0.8312111496925354 -0.5553975701332092 -0.024930691346526146 +v 0.8291446566581726 -0.5540167689323425 -0.07473009079694748 +v 0.8250167369842529 -0.5512585639953613 -0.12434370815753937 +v 0.8188377022743225 -0.5471298694610596 -0.1736481785774231 +v 0.8106229305267334 -0.5416409373283386 -0.22252093255519867 +v 0.8003928065299988 -0.5348053574562073 -0.27084046602249146 +v 0.7881727814674377 -0.5266402363777161 -0.31848666071891785 +v 0.7739932537078857 -0.5171657204627991 -0.36534103751182556 +v 0.7578893899917603 -0.5064055323600769 -0.4112870991230011 +v 0.7399013638496399 -0.4943862855434418 -0.4562106430530548 +v 0.7200738191604614 -0.4811379313468933 -0.5 +v 0.6984560489654541 -0.4666934013366699 -0.542546272277832 +v 0.6751017570495605 -0.4510885775089264 -0.583743691444397 +v 0.6500691175460815 -0.4343622922897339 -0.6234897971153259 +v 0.6234202980995178 -0.41655611991882324 -0.6616858243942261 +v 0.5952215194702148 -0.3977143168449402 -0.6982368230819702 +v 0.5655429363250732 -0.3778837323188782 -0.7330518960952759 +v 0.534458339214325 -0.35711365938186646 -0.7660444378852844 +v 0.5020450353622437 -0.3354557454586029 -0.7971324920654297 +v 0.4683835208415985 -0.3129638433456421 -0.826238751411438 +v 0.43355754017829895 -0.2896938920021057 -0.853290855884552 +v 0.39765363931655884 -0.2657036781311035 -0.8782215714454651 +v 0.3607611358165741 -0.24105289578437805 -0.9009688496589661 +v 0.32297173142433167 -0.21580280363559723 -0.9214761853218079 +v 0.28437936305999756 -0.19001621007919312 -0.9396926164627075 +v 0.24507996439933777 -0.16375720500946045 -0.955572783946991 +v 0.20517127215862274 -0.13709107041358948 -0.9690772891044617 +v 0.16475249826908112 -0.11008410155773163 -0.9801725149154663 +v 0.12392411381006241 -0.08280344307422638 -0.9888308048248291 +v 0.0827876403927803 -0.05531693249940872 -0.9950307607650757 +v 0.04144534096121788 -0.0276928897947073 -0.9987569451332092 +v 0.043960146605968475 -0.023497188463807106 0.9987569451332092 +v 0.08781100064516068 -0.04693595692515373 0.9950307607650757 +v 0.1314435452222824 -0.07025803625583649 0.9888308048248291 +v 0.17474929988384247 -0.09340544790029526 0.9801725149154663 +v 0.2176205962896347 -0.11632063239812851 0.9690772891044617 +v 0.2599508464336395 -0.13894662261009216 0.955572783946991 +v 0.30163484811782837 -0.16122718155384064 0.9396926164627075 +v 0.34256890416145325 -0.1831068992614746 0.9214761853218079 +v 0.38265129923820496 -0.20453137159347534 0.9009688496589661 +v 0.42178234457969666 -0.2254473716020584 0.8782215714454651 +v 0.4598647952079773 -0.2458028495311737 0.853290855884552 +v 0.4968039393424988 -0.2655472457408905 0.826238751411438 +v 0.5325079560279846 -0.2846314311027527 0.7971324920654297 +v 0.5668880343437195 -0.30300799012184143 0.7660444378852844 +v 0.599858820438385 -0.320631206035614 0.7330518960952759 +v 0.6313381791114807 -0.3374572992324829 0.6982368230819702 +v 0.661247968673706 -0.3534444272518158 0.6616858243942261 +v 0.6895138025283813 -0.3685528039932251 0.6234897971153259 +v 0.7160654067993164 -0.3827449381351471 0.583743691444397 +v 0.7408367395401001 -0.39598548412323 0.542546272277832 +v 0.7637662291526794 -0.40824154019355774 0.5 +v 0.7847968935966492 -0.41948267817497253 0.4562106430530548 +v 0.8038763999938965 -0.42968088388442993 0.4112870991230011 +v 0.820957362651825 -0.4388108551502228 0.36534103751182556 +v 0.8359972834587097 -0.44684985280036926 0.31848666071891785 +v 0.8489587903022766 -0.453777939081192 0.27084046602249146 +v 0.8598096370697021 -0.4595778286457062 0.22252093255519867 +v 0.8685228824615479 -0.4642351567745209 0.1736481785774231 +v 0.8750768303871155 -0.4677383303642273 0.12434370815753937 +v 0.8794552087783813 -0.47007861733436584 0.07473009079694748 +v 0.8816471695899963 -0.4712502062320709 0.024930691346526146 +v 0.8816471695899963 -0.4712502062320709 -0.024930691346526146 +v 0.8794552087783813 -0.47007861733436584 -0.07473009079694748 +v 0.8750768303871155 -0.4677383303642273 -0.12434370815753937 +v 0.8685228824615479 -0.4642351567745209 -0.1736481785774231 +v 0.8598096370697021 -0.4595778286457062 -0.22252093255519867 +v 0.8489587903022766 -0.453777939081192 -0.27084046602249146 +v 0.8359972834587097 -0.44684985280036926 -0.31848666071891785 +v 0.820957362651825 -0.4388108551502228 -0.36534103751182556 +v 0.8038763999938965 -0.42968088388442993 -0.4112870991230011 +v 0.7847968935966492 -0.41948267817497253 -0.4562106430530548 +v 0.7637662291526794 -0.40824154019355774 -0.5 +v 0.7408367395401001 -0.39598548412323 -0.542546272277832 +v 0.7160654067993164 -0.3827449381351471 -0.583743691444397 +v 0.6895138025283813 -0.3685528039932251 -0.6234897971153259 +v 0.661247968673706 -0.3534444272518158 -0.6616858243942261 +v 0.6313381791114807 -0.3374572992324829 -0.6982368230819702 +v 0.599858820438385 -0.320631206035614 -0.7330518960952759 +v 0.5668880343437195 -0.30300799012184143 -0.7660444378852844 +v 0.5325079560279846 -0.2846314311027527 -0.7971324920654297 +v 0.4968039393424988 -0.2655472457408905 -0.826238751411438 +v 0.4598647952079773 -0.2458028495311737 -0.853290855884552 +v 0.42178234457969666 -0.2254473716020584 -0.8782215714454651 +v 0.38265129923820496 -0.20453137159347534 -0.9009688496589661 +v 0.34256890416145325 -0.1831068992614746 -0.9214761853218079 +v 0.30163484811782837 -0.16122718155384064 -0.9396926164627075 +v 0.2599508464336395 -0.13894662261009216 -0.955572783946991 +v 0.2176205962896347 -0.11632063239812851 -0.9690772891044617 +v 0.17474929988384247 -0.09340544790029526 -0.9801725149154663 +v 0.1314435452222824 -0.07025803625583649 -0.9888308048248291 +v 0.08781100064516068 -0.04693595692515373 -0.9950307607650757 +v 0.043960146605968475 -0.023497188463807106 -0.9987569451332092 +v 0.046051595360040665 -0.019075194373726845 0.9987569451332092 +v 0.0919886976480484 -0.03810296580195427 0.9950307607650757 +v 0.13769710063934326 -0.05703600496053696 0.9888308048248291 +v 0.18306316435337067 -0.07582724839448929 0.9801725149154663 +v 0.22797410190105438 -0.09442996978759766 0.9690772891044617 +v 0.2723182737827301 -0.11279792338609695 0.955572783946991 +v 0.31598541140556335 -0.13088543713092804 0.9396926164627075 +v 0.35886695981025696 -0.14864756166934967 0.9214761853218079 +v 0.4008563160896301 -0.16604012250900269 0.9009688496589661 +v 0.4418490529060364 -0.18301987648010254 0.8782215714454651 +v 0.48174330592155457 -0.19954460859298706 0.853290855884552 +v 0.520439863204956 -0.215573251247406 0.826238751411438 +v 0.5578425526618958 -0.23106594383716583 0.7971324920654297 +v 0.5938583016395569 -0.24598416686058044 0.7660444378852844 +v 0.6283976435661316 -0.26029083132743835 0.7330518960952759 +v 0.6613747477531433 -0.27395036816596985 0.6982368230819702 +v 0.6927074790000916 -0.28692883253097534 0.6616858243942261 +v 0.7223181128501892 -0.29919394850730896 0.6234897971153259 +v 0.7501329183578491 -0.3107152283191681 0.583743691444397 +v 0.7760827541351318 -0.32146400213241577 0.542546272277832 +v 0.8001031279563904 -0.3314135670661926 0.5 +v 0.8221343755722046 -0.34053918719291687 0.4562106430530548 +v 0.8421216011047363 -0.34881818294525146 0.4112870991230011 +v 0.8600152134895325 -0.3562299609184265 0.36534103751182556 +v 0.8757706880569458 -0.3627561032772064 0.31848666071891785 +v 0.8893488645553589 -0.3683803379535675 0.27084046602249146 +v 0.9007159471511841 -0.3730887472629547 0.22252093255519867 +v 0.9098437428474426 -0.3768696188926697 0.1736481785774231 +v 0.9167094826698303 -0.3797135055065155 0.12434370815753937 +v 0.9212961792945862 -0.3816133737564087 0.07473009079694748 +v 0.9235923886299133 -0.3825644850730896 0.024930691346526146 +v 0.9235923886299133 -0.3825644850730896 -0.024930691346526146 +v 0.9212961792945862 -0.3816133737564087 -0.07473009079694748 +v 0.9167094826698303 -0.3797135055065155 -0.12434370815753937 +v 0.9098437428474426 -0.3768696188926697 -0.1736481785774231 +v 0.9007159471511841 -0.3730887472629547 -0.22252093255519867 +v 0.8893488645553589 -0.3683803379535675 -0.27084046602249146 +v 0.8757706880569458 -0.3627561032772064 -0.31848666071891785 +v 0.8600152134895325 -0.3562299609184265 -0.36534103751182556 +v 0.8421216011047363 -0.34881818294525146 -0.4112870991230011 +v 0.8221343755722046 -0.34053918719291687 -0.4562106430530548 +v 0.8001031279563904 -0.3314135670661926 -0.5 +v 0.7760827541351318 -0.32146400213241577 -0.542546272277832 +v 0.7501329183578491 -0.3107152283191681 -0.583743691444397 +v 0.7223181128501892 -0.29919394850730896 -0.6234897971153259 +v 0.6927074790000916 -0.28692883253097534 -0.6616858243942261 +v 0.6613747477531433 -0.27395036816596985 -0.6982368230819702 +v 0.6283976435661316 -0.26029083132743835 -0.7330518960952759 +v 0.5938583016395569 -0.24598416686058044 -0.7660444378852844 +v 0.5578425526618958 -0.23106594383716583 -0.7971324920654297 +v 0.520439863204956 -0.215573251247406 -0.826238751411438 +v 0.48174330592155457 -0.19954460859298706 -0.853290855884552 +v 0.4418490529060364 -0.18301987648010254 -0.8782215714454651 +v 0.4008563160896301 -0.16604012250900269 -0.9009688496589661 +v 0.35886695981025696 -0.14864756166934967 -0.9214761853218079 +v 0.31598541140556335 -0.13088543713092804 -0.9396926164627075 +v 0.2723182737827301 -0.11279792338609695 -0.955572783946991 +v 0.22797410190105438 -0.09442996978759766 -0.9690772891044617 +v 0.18306316435337067 -0.07582724839448929 -0.9801725149154663 +v 0.13769710063934326 -0.05703600496053696 -0.9888308048248291 +v 0.0919886976480484 -0.03810296580195427 -0.9950307607650757 +v 0.046051595360040665 -0.019075194373726845 -0.9987569451332092 +v 0.04769953712821007 -0.01446949690580368 0.9987569451332092 +v 0.0952804908156395 -0.028903020545840263 0.9950307607650757 +v 0.14262455701828003 -0.043264687061309814 0.9888308048248291 +v 0.18961404263973236 -0.05751878768205643 0.9801725149154663 +v 0.23613210022449493 -0.07162988930940628 0.9690772891044617 +v 0.2820631265640259 -0.08556290715932846 0.955572783946991 +v 0.32729285955429077 -0.09928320348262787 0.9396926164627075 +v 0.3717089295387268 -0.11275666952133179 0.9214761853218079 +v 0.4152008593082428 -0.12594980001449585 0.9009688496589661 +v 0.4576605260372162 -0.1388297975063324 0.8782215714454651 +v 0.49898236989974976 -0.15136465430259705 0.853290855884552 +v 0.5390636920928955 -0.16352318227291107 0.826238751411438 +v 0.5778048038482666 -0.17527516186237335 0.7971324920654297 +v 0.615109384059906 -0.18659138679504395 0.7660444378852844 +v 0.650884747505188 -0.19744372367858887 0.7330518960952759 +v 0.6850418448448181 -0.20780517160892487 0.6982368230819702 +v 0.717495858669281 -0.21764999628067017 0.6616858243942261 +v 0.7481660842895508 -0.22695370018482208 0.6234897971153259 +v 0.7769762277603149 -0.23569315671920776 0.583743691444397 +v 0.8038547039031982 -0.24384665489196777 0.542546272277832 +v 0.8287346363067627 -0.2513939142227173 0.5 +v 0.8515542149543762 -0.25831615924835205 0.4562106430530548 +v 0.8722566962242126 -0.26459619402885437 0.4112870991230011 +v 0.8907906413078308 -0.2702183723449707 0.36534103751182556 +v 0.9071099162101746 -0.275168776512146 0.31848666071891785 +v 0.9211739897727966 -0.27943506836891174 0.27084046602249146 +v 0.9329478144645691 -0.2830066382884979 0.22252093255519867 +v 0.9424022436141968 -0.285874605178833 0.1736481785774231 +v 0.9495137333869934 -0.28803184628486633 0.12434370815753937 +v 0.9542645215988159 -0.2894729971885681 0.07473009079694748 +v 0.9566429257392883 -0.29019445180892944 0.024930691346526146 +v 0.9566429257392883 -0.29019445180892944 -0.024930691346526146 +v 0.9542645215988159 -0.2894729971885681 -0.07473009079694748 +v 0.9495137333869934 -0.28803184628486633 -0.12434370815753937 +v 0.9424022436141968 -0.285874605178833 -0.1736481785774231 +v 0.9329478144645691 -0.2830066382884979 -0.22252093255519867 +v 0.9211739897727966 -0.27943506836891174 -0.27084046602249146 +v 0.9071099162101746 -0.275168776512146 -0.31848666071891785 +v 0.8907906413078308 -0.2702183723449707 -0.36534103751182556 +v 0.8722566962242126 -0.26459619402885437 -0.4112870991230011 +v 0.8515542149543762 -0.25831615924835205 -0.4562106430530548 +v 0.8287346363067627 -0.2513939142227173 -0.5 +v 0.8038547039031982 -0.24384665489196777 -0.542546272277832 +v 0.7769762277603149 -0.23569315671920776 -0.583743691444397 +v 0.7481660842895508 -0.22695370018482208 -0.6234897971153259 +v 0.717495858669281 -0.21764999628067017 -0.6616858243942261 +v 0.6850418448448181 -0.20780517160892487 -0.6982368230819702 +v 0.650884747505188 -0.19744372367858887 -0.7330518960952759 +v 0.615109384059906 -0.18659138679504395 -0.7660444378852844 +v 0.5778048038482666 -0.17527516186237335 -0.7971324920654297 +v 0.5390636920928955 -0.16352318227291107 -0.826238751411438 +v 0.49898236989974976 -0.15136465430259705 -0.853290855884552 +v 0.4576605260372162 -0.1388297975063324 -0.8782215714454651 +v 0.4152008593082428 -0.12594980001449585 -0.9009688496589661 +v 0.3717089295387268 -0.11275666952133179 -0.9214761853218079 +v 0.32729285955429077 -0.09928320348262787 -0.9396926164627075 +v 0.2820631265640259 -0.08556290715932846 -0.955572783946991 +v 0.23613210022449493 -0.07162988930940628 -0.9690772891044617 +v 0.18961404263973236 -0.05751878768205643 -0.9801725149154663 +v 0.14262455701828003 -0.043264687061309814 -0.9888308048248291 +v 0.0952804908156395 -0.028903020545840263 -0.9950307607650757 +v 0.04769953712821007 -0.01446949690580368 -0.9987569451332092 +v 0.048888109624385834 -0.009724450297653675 0.9987569451332092 +v 0.09765467792749405 -0.019424723461270332 0.9950307607650757 +v 0.1461784541606903 -0.029076702892780304 0.9888308048248291 +v 0.1943388134241104 -0.03865639492869377 0.9801725149154663 +v 0.2420160174369812 -0.04813998192548752 0.9690772891044617 +v 0.2890915274620056 -0.05750388279557228 0.955572783946991 +v 0.33544832468032837 -0.06672482192516327 0.9396926164627075 +v 0.38097113370895386 -0.07577987015247345 0.9214761853218079 +v 0.425546795129776 -0.08464651554822922 0.9009688496589661 +v 0.46906447410583496 -0.09330271929502487 0.8782215714454651 +v 0.511415958404541 -0.1017269641160965 0.853290855884552 +v 0.5524960160255432 -0.10989829152822495 0.826238751411438 +v 0.5922024846076965 -0.1177963986992836 0.7971324920654297 +v 0.6304365992546082 -0.12540164589881897 0.7660444378852844 +v 0.6671034097671509 -0.13269512355327606 0.7330518960952759 +v 0.7021116614341736 -0.13965868949890137 0.6982368230819702 +v 0.735374391078949 -0.14627505838871002 0.6616858243942261 +v 0.766808807849884 -0.15252774953842163 0.6234897971153259 +v 0.796336829662323 -0.1584012508392334 0.583743691444397 +v 0.8238850831985474 -0.16388092935085297 0.542546272277832 +v 0.8493849635124207 -0.16895318031311035 0.5 +v 0.8727731704711914 -0.17360538244247437 0.4562106430530548 +v 0.8939915299415588 -0.17782597243785858 0.4112870991230011 +v 0.9129872918128967 -0.18160445988178253 0.36534103751182556 +v 0.9297131896018982 -0.1849314570426941 0.31848666071891785 +v 0.9441276788711548 -0.18779867887496948 0.27084046602249146 +v 0.9561949372291565 -0.19019900262355804 0.22252093255519867 +v 0.9658849239349365 -0.19212646782398224 0.1736481785774231 +v 0.9731736183166504 -0.19357626140117645 0.12434370815753937 +v 0.9780427813529968 -0.19454480707645416 0.07473009079694748 +v 0.980480432510376 -0.19502969086170197 0.024930691346526146 +v 0.980480432510376 -0.19502969086170197 -0.024930691346526146 +v 0.9780427813529968 -0.19454480707645416 -0.07473009079694748 +v 0.9731736183166504 -0.19357626140117645 -0.12434370815753937 +v 0.9658849239349365 -0.19212646782398224 -0.1736481785774231 +v 0.9561949372291565 -0.19019900262355804 -0.22252093255519867 +v 0.9441276788711548 -0.18779867887496948 -0.27084046602249146 +v 0.9297131896018982 -0.1849314570426941 -0.31848666071891785 +v 0.9129872918128967 -0.18160445988178253 -0.36534103751182556 +v 0.8939915299415588 -0.17782597243785858 -0.4112870991230011 +v 0.8727731704711914 -0.17360538244247437 -0.4562106430530548 +v 0.8493849635124207 -0.16895318031311035 -0.5 +v 0.8238850831985474 -0.16388092935085297 -0.542546272277832 +v 0.796336829662323 -0.1584012508392334 -0.583743691444397 +v 0.766808807849884 -0.15252774953842163 -0.6234897971153259 +v 0.735374391078949 -0.14627505838871002 -0.6616858243942261 +v 0.7021116614341736 -0.13965868949890137 -0.6982368230819702 +v 0.6671034097671509 -0.13269512355327606 -0.7330518960952759 +v 0.6304365992546082 -0.12540164589881897 -0.7660444378852844 +v 0.5922024846076965 -0.1177963986992836 -0.7971324920654297 +v 0.5524960160255432 -0.10989829152822495 -0.826238751411438 +v 0.511415958404541 -0.1017269641160965 -0.853290855884552 +v 0.46906447410583496 -0.09330271929502487 -0.8782215714454651 +v 0.425546795129776 -0.08464651554822922 -0.9009688496589661 +v 0.38097113370895386 -0.07577987015247345 -0.9214761853218079 +v 0.33544832468032837 -0.06672482192516327 -0.9396926164627075 +v 0.2890915274620056 -0.05750388279557228 -0.955572783946991 +v 0.2420160174369812 -0.04813998192548752 -0.9690772891044617 +v 0.1943388134241104 -0.03865639492869377 -0.9801725149154663 +v 0.1461784541606903 -0.029076702892780304 -0.9888308048248291 +v 0.09765467792749405 -0.019424723461270332 -0.9950307607650757 +v 0.048888109624385834 -0.009724450297653675 -0.9987569451332092 +v 0.04960586503148079 -0.004885751288384199 0.9987569451332092 +v 0.0990884006023407 -0.009759355336427689 0.9950307607650757 +v 0.14832459390163422 -0.014608697034418583 0.9888308048248291 +v 0.1971920132637024 -0.019421719014644623 0.9801725149154663 +v 0.24556919932365417 -0.024186454713344574 0.9690772891044617 +v 0.29333585500717163 -0.028891058638691902 0.955572783946991 +v 0.3403732180595398 -0.033523835241794586 0.9396926164627075 +v 0.38656437397003174 -0.03807326778769493 0.9214761853218079 +v 0.4317944645881653 -0.04252804443240166 0.9009688496589661 +v 0.47595104575157166 -0.04687708616256714 0.8782215714454651 +v 0.518924355506897 -0.05110958591103554 0.853290855884552 +v 0.5606074929237366 -0.0552150197327137 0.826238751411438 +v 0.600896954536438 -0.05918318033218384 0.7971324920654297 +v 0.6396924257278442 -0.06300420314073563 0.7660444378852844 +v 0.6768975257873535 -0.06666858494281769 0.7330518960952759 +v 0.7124197483062744 -0.07016722112894058 0.6982368230819702 +v 0.7461708188056946 -0.0734914094209671 0.6616858243942261 +v 0.7780667543411255 -0.07663288712501526 0.6234897971153259 +v 0.8080282807350159 -0.07958383858203888 0.583743691444397 +v 0.8359809517860413 -0.08233693987131119 0.542546272277832 +v 0.8618552684783936 -0.08488533645868301 0.5 +v 0.8855868577957153 -0.08722268790006638 0.4562106430530548 +v 0.907116711139679 -0.08934319764375687 0.4112870991230011 +v 0.9263913631439209 -0.09124158322811127 0.36534103751182556 +v 0.943362832069397 -0.09291312843561172 0.31848666071891785 +v 0.957988977432251 -0.09435367584228516 0.27084046602249146 +v 0.9702333807945251 -0.09555964916944504 0.22252093255519867 +v 0.980065643787384 -0.09652803838253021 0.1736481785774231 +v 0.9874613285064697 -0.09725645184516907 0.12434370815753937 +v 0.9924020171165466 -0.09774306416511536 0.07473009079694748 +v 0.994875431060791 -0.09798667579889297 0.024930691346526146 +v 0.994875431060791 -0.09798667579889297 -0.024930691346526146 +v 0.9924020171165466 -0.09774306416511536 -0.07473009079694748 +v 0.9874613285064697 -0.09725645184516907 -0.12434370815753937 +v 0.980065643787384 -0.09652803838253021 -0.1736481785774231 +v 0.9702333807945251 -0.09555964916944504 -0.22252093255519867 +v 0.957988977432251 -0.09435367584228516 -0.27084046602249146 +v 0.943362832069397 -0.09291312843561172 -0.31848666071891785 +v 0.9263913631439209 -0.09124158322811127 -0.36534103751182556 +v 0.907116711139679 -0.08934319764375687 -0.4112870991230011 +v 0.8855868577957153 -0.08722268790006638 -0.4562106430530548 +v 0.8618552684783936 -0.08488533645868301 -0.5 +v 0.8359809517860413 -0.08233693987131119 -0.542546272277832 +v 0.8080282807350159 -0.07958383858203888 -0.583743691444397 +v 0.7780667543411255 -0.07663288712501526 -0.6234897971153259 +v 0.7461708188056946 -0.0734914094209671 -0.6616858243942261 +v 0.7124197483062744 -0.07016722112894058 -0.6982368230819702 +v 0.6768975257873535 -0.06666858494281769 -0.7330518960952759 +v 0.6396924257278442 -0.06300420314073563 -0.7660444378852844 +v 0.600896954536438 -0.05918318033218384 -0.7971324920654297 +v 0.5606074929237366 -0.0552150197327137 -0.826238751411438 +v 0.518924355506897 -0.05110958591103554 -0.853290855884552 +v 0.47595104575157166 -0.04687708616256714 -0.8782215714454651 +v 0.4317944645881653 -0.04252804443240166 -0.9009688496589661 +v 0.38656437397003174 -0.03807326778769493 -0.9214761853218079 +v 0.3403732180595398 -0.033523835241794586 -0.9396926164627075 +v 0.29333585500717163 -0.028891058638691902 -0.955572783946991 +v 0.24556919932365417 -0.024186454713344574 -0.9690772891044617 +v 0.1971920132637024 -0.019421719014644623 -0.9801725149154663 +v 0.14832459390163422 -0.014608697034418583 -0.9888308048248291 +v 0.0990884006023407 -0.009759355336427689 -0.9950307607650757 +v 0.04960586503148079 -0.004885751288384199 -0.9987569451332092 +v 0.07434713281691074 0.007322553312405944 0.9968938529491425 +v 0.0701339736681371 -0.0139505152432224 0.9971335787729753 +v 0.06588557362556458 -0.03521657269448042 0.9968938529491425 +v 0.0505637764449488 -0.0505637764449488 0.9971335788102194 +v 0.03521657269448042 -0.06588557362556458 0.9968938529491425 +v 0.013950515001912146 -0.07013397245498848 0.9971335788656702 +v -0.007322553312405944 -0.07434713281691074 0.9968938529491425 +v -0.02736491825848915 -0.06606475851317518 0.9971335790090124 +v -0.04739353433251381 -0.05774918757379055 0.9968938529491425 +v -0.059456711975812496 -0.039727703748416304 0.9971335789913638 +v -0.07149001397192478 -0.021686258725821972 0.9968938529491425 +v -0.07150797912864008 8.757201972099599e-18 0.9971335788228384 +v -0.07149001397192478 0.021686258725821972 0.9968938529491425 +v -0.05945671180862766 0.039727703636706954 0.9971335790064322 +v -0.04739353433251381 0.05774918757379055 0.9968938529491425 +v -0.027364917947386384 0.06606475776210669 0.9971335790699353 +v -0.007322553312405944 0.07434713281691074 0.9968938529491425 +v 0.0139505152432224 0.0701339736681371 0.9971335787729753 +v 0.03521657269448042 0.06588557362556458 0.9968938529491425 +v 0.0505637764449488 0.0505637764449488 0.9971335788102194 +v 0.06606475851317518 0.02736491825848915 0.9971335790090124 +v 0.058320647928770036 0.03896860998289589 0.9972359725241738 +v 0.05396059819212855 0.04428431958133136 0.9972611422237504 +v 0.06156320281398565 0.03290621749441673 0.9972611422734732 +v 0.07292054383949498 0.014504798490789662 0.9969206608955334 +v 0.06810120585879835 0.020658275084853454 0.9971592390509152 +v 0.08169084833721893 0.02478064781534686 0.9960949999079954 +v 0.0833254986457543 0.0165744726013087 0.9961256330455268 +v 0.08414399384747487 0.008287459840049492 0.9961561197265474 +v 0.07849090365951931 0.03251199628043784 0.9961256332419902 +v 0.07456744034168325 0.03985712686908108 0.9961561196312256 +v 0.06996506477614946 0.046749160960613906 0.9961864604111729 +v 0.06473447950632275 0.05312621489775816 0.9962166555681746 +v 0.058931939606729815 0.058931939606729815 0.9962467061092398 +v 0.060904326731940406 0.012114624394221668 0.9978388030143713 +v 0.059139072209495366 0.017939641727015638 0.9978610863179652 +v 0.056822512417338426 0.023536654449151775 0.9978832630598878 +v 0.05398216959280605 0.028854071047960006 0.9979053333183685 +v 0.05065033776386076 0.03384347232807069 0.9979272981211579 +v 0.04686371971011271 0.03846006230751906 0.9979491574946205 +v 0.04266304382033815 0.04266304382033815 0.9979709123978946 +v 0.06150258179219232 0.006057475540073005 0.9978610864324683 +v 0.041306168737954924 0.06181905165713066 0.9969206611490139 +v 0.03449952926633111 0.051632196309174275 0.9978388031411116 +v 0.029132397310620176 0.054502881435636544 0.9978610863277096 +v 0.045146960754023205 0.055011729570494064 0.9971592390501454 +v 0.03809262121868865 0.046415991418909994 0.9979925630428949 +v 0.054156066844277685 0.06598935781531828 0.9960949999076105 +v 0.047200102209612384 0.07063994601425892 0.9961256333175093 +v 0.03985712607675826 0.07456743885935144 0.9961561197571853 +v 0.02845218388732218 0.06868964985130867 0.9969206611586248 +v 0.023536654640910322 0.05682251288028449 0.997883263022336 +v 0.03220138150753434 0.07774101328749881 0.9961864605393786 +v 0.02065827529196143 0.06810120654154189 0.9971592389974478 +v 0.017768249499051883 0.05857406771963484 0.9979053332431503 +v 0.0118842480553443 0.0597461466192997 0.9979272979153121 +v 0.024309408844385306 0.0801373815461411 0.9962166558065164 +v 0.016259285629758192 0.08174094667313367 0.9962467064160001 +v 4.552572033822715e-18 0.07434914181656936 0.9969206609089086 +v 3.802376137633662e-18 0.062097513650990026 0.997838803021059 +v -0.006057475602119168 0.06150258242215763 0.99786108638503 +v 0.006975445744358535 0.07082289030284852 0.997159239053834 +v 0.005942284261148708 0.06033302503283419 0.997949157588566 +v 0.008367400540393113 0.08495564545259461 0.9960949999094548 +v 5.202173669578637e-18 0.08495794111481028 0.9961256330755933 +v -0.008287459792348616 0.08414399336315971 0.9961561197630178 +v -0.014504798490789662 0.07292054383949498 0.9969206608955334 +v -0.011998883402115034 0.060322457448278584 0.997883262821664 +v -0.01641612274193005 0.0825294201124053 0.9961864602779934 +v -0.020658274849996335 0.06810120508457815 0.9971592391115464 +v -0.017768249540953764 0.05857406785776685 0.9979053332323329 +v -0.023311789358073935 0.05627964008313953 0.9979272981057403 +v -0.024309409185687803 0.08013738267126472 0.9962166557184051 +v -0.031893735204408226 0.07699828943523167 0.996246706525092 +v -0.04130616873639176 0.06181905165479122 0.9969206611492247 +v -0.03449952926554953 0.05163219630800455 0.997838803141217 +v -0.03920561191954729 0.047772174599746225 0.9978610862618175 +v -0.033547217867416566 0.06276243023251822 0.9971592390709317 +v -0.02857840431616753 0.053466433382551325 0.9979491575094097 +v -0.04024158739628515 0.07528671543883944 0.9960949999180038 +v -0.04720010185705785 0.0706399454866238 0.9961256333650651 +v -0.053638668605943025 0.06535890619691533 0.9961561197847657 +v -0.052572781530418486 0.052572781530418486 0.9969206609055387 +v -0.04349006841776013 0.04349006841776013 0.9978832628714319 +v -0.059500395166053405 0.059500395166053405 0.996186460185787 +v -0.05501173004075209 0.04514696113995398 0.9971592390045556 +v -0.04731576707543766 0.03883104796864788 0.9979053332286015 +v -0.05065033822699523 0.03384347263752726 0.9979272980794156 +v -0.06473447784422708 0.05312621353371158 0.9962166557293084 +v -0.06929662866348109 0.04630252620696934 0.9962467066052252 +v -0.06868965036700582 0.028452184100930943 0.996920661116794 +v -0.05737062286352324 0.023763689237328894 0.9978388031250016 +v -0.059139073209987396 0.017939642030511557 0.9978610862396141 +v -0.06276243046102868 0.03354721798955794 0.9971592390515143 +v -0.053466433240690565 0.028578404240341377 0.9979491575214642 +v -0.07528671555309469 0.040241587457355836 0.996094999908295 +v -0.0784909022331271 0.032511995689606825 0.9961256333576919 +v -0.08091038584161316 0.02454389711979567 0.996156119892478 +v -0.07292054354228006 0.014504798431669939 0.9969206609182432 +v -0.06032245798264624 0.011998883508407365 0.9978832627808336 +v -0.0825294189393381 0.016416122508592482 0.9961864603676258 +v -0.07082289003070058 0.006975445717554304 0.9971592390743276 +v -0.060914997424813905 0.005999603534962804 0.9979053333098651 +v -0.06091664346216004 7.460137378762202e-18 0.9979272979243236 +v -0.0833400943576934 0.00820828265637408 0.9962166557859432 +v -0.08334234636458965 1.0206494035883636e-17 0.9962467064183604 +v -0.07292054354228006 -0.014504798431669939 0.9969206609182432 +v -0.060904326583332946 -0.012114624364661807 0.9978388030257261 +v -0.059139072065195156 -0.01793964168324265 0.9978610863292657 +v -0.07082289003070058 -0.006975445717554304 0.9971592390743276 +v -0.06033302567368508 -0.005942284324267006 0.9979491575403079 +v -0.08495564531652064 -0.008367400526990997 0.9960949999197016 +v -0.08332549791301228 -0.016574472455557255 0.9961256331015146 +v -0.08091038690827618 -0.02454389744336434 0.9961561198089449 +v -0.06868965036700582 -0.028452184100930943 0.996920661116794 +v -0.05682251227869065 -0.023536654391721985 0.9978832630711342 +v -0.07774101400577388 -0.03220138180505363 0.9961864604811158 +v -0.0627624306375088 -0.033547218083888525 0.997159239036518 +v -0.05398216946108876 -0.02885407097755565 0.9979053333295611 +v -0.05065033764027319 -0.03384347224549211 0.9979272981322969 +v -0.07385503482059032 -0.0394763382065716 0.9962166556087939 +v -0.06929662964455384 -0.046302526862501234 0.996246706516801 +v -0.052572781530418486 -0.052572781530418486 0.9969206609055387 +v -0.04390957315136914 -0.04390957315136914 0.9978388030193739 +v -0.03920561181837744 -0.0477721744764704 0.9978610862737686 +v -0.05501173019543821 -0.045146961266901584 0.9971592389895594 +v -0.04686371959576453 -0.03846006221367595 0.9979491575057061 +v -0.06598935812779036 -0.054156067100716875 0.9960949998773175 +v -0.06007433588702435 -0.06007433588702435 0.9961256329352222 +v -0.053638668885797776 -0.06535890653791936 0.9961561197517067 +v -0.04130616873639176 -0.06181905165479122 0.9969206611492247 +v -0.03416992643393253 -0.05113891083549543 0.9978832629956993 +v -0.04674916032635253 -0.06996506382691028 0.9961864604967279 +v -0.033547218248803835 -0.06276243094604367 0.9971592390103006 +v -0.028854071207237913 -0.05398216989079408 0.9979053332930473 +v -0.02331178931160955 -0.056279639970964586 0.9979272981148394 +v -0.03947633779194219 -0.07385503404487322 0.9962166556747097 +v -0.03189373489437626 -0.07699828868674831 0.9962467065858052 +v -0.014504798490789662 -0.07292054383949498 0.9969206608955334 +v -0.012114624394221668 -0.060904326731940406 0.9978388030143713 +v -0.006057475540073005 -0.06150258179219232 0.9978610864324683 +v -0.020658275084853454 -0.06810120585879835 0.9971592390509152 +v -0.017598494586653787 -0.05801446073616597 0.9979491574760103 +v -0.02478064781534686 -0.08169084833721893 0.9960949999079954 +v -0.0165744726013087 -0.0833254986457543 0.9961256330455268 +v -0.008287459840049492 -0.08414399384747487 0.9961561197265474 +v -1.3657715687877839e-17 -0.07434914181656936 0.9969206609089086 +v -1.1298146073526448e-17 -0.061504243707206044 0.9978832629174824 +v -1.5457418578174008e-17 -0.08414626757729224 0.9961864602282005 +v 0.006975445623700151 -0.07082288907778331 0.9971592391460851 +v 0.005999603429900588 -0.06091499635809925 0.9979053333901919 +v 0.011884247954507595 -0.05974614611235933 0.9979272979540467 +v 0.008208282832112092 -0.08334009614199149 0.9962166556515802 +v 0.01625928594232576 -0.08174094824451703 0.9962467062959329 +v 0.02845218388732218 -0.06868964985130867 0.9969206611586248 +v 0.023763689130524512 -0.057370622605674666 0.997838803145917 +v 0.029132397525038232 -0.054502881836784534 0.9978610862936225 +v 0.02065827493462311 -0.06810120536355524 0.997159239089699 +v 0.017598494473125087 -0.05801446036191199 0.9979491575053191 +v 0.024780647740231684 -0.08169084808959737 0.9960949999273874 +v 0.032511996181887375 -0.07849090342159744 0.9961256332612892 +v 0.03985712674826603 -0.0745674401156542 0.9961561196504323 +v 0.041306168737954924 -0.06181905165713066 0.9969206611490139 +v 0.034169926624927856 -0.05113891112134012 0.9978832629699361 +v 0.0467491608189077 -0.06996506456407114 0.9961864604302875 +v 0.045146960754023205 -0.055011729570494064 0.9971592390501454 +v 0.03883104820898993 -0.04731576736829532 0.9979053332002101 +v 0.043074571719841054 -0.043074571719841054 0.9979272979447936 +v 0.053126214736721813 -0.06473447931009937 0.9962166555871977 +v 0.05893193942809513 -0.05893193942809513 0.9962467061281718 +v 0.06181905165713066 -0.041306168737954924 0.9969206611490139 +v 0.051632196309174275 -0.03449952926633111 0.9978388031411116 +v 0.054502881435636544 -0.029132397310620176 0.9978610863277096 +v 0.055011729570494064 -0.045146960754023205 0.9971592390501454 +v 0.04686372028587348 -0.03846006278003368 0.9979491574388026 +v 0.06598935781531828 -0.054156066844277685 0.9960949999076105 +v 0.07063994601425892 -0.047200102209612384 0.9961256333175093 +v 0.07456743885935144 -0.03985712607675826 0.9961561197571853 +v 0.06868964985130867 -0.02845218388732218 0.9969206611586248 +v 0.05682251288028449 -0.023536654640910322 0.997883263022336 +v 0.07774101328749881 -0.03220138150753434 0.9961864605393786 +v 0.06810120654154189 -0.02065827529196143 0.9971592389974478 +v 0.05857406771963484 -0.017768249499051883 0.9979053332431503 +v 0.0597461466192997 -0.0118842480553443 0.9979272979153121 +v 0.0801373815461411 -0.024309408844385306 0.9962166558065164 +v 0.08174094667313367 -0.016259285629758192 0.9962467064160001 +v 0.07434914181656938 0 0.9969206609089086 +v 0.08414626757729224 0 0.9961864602282005 +v 0.061504243707206044 0 0.9978832629174824 +v 0.07082289030284852 -0.006975445744358535 0.997159239053834 +v 0.06033302503283419 -0.005942284261148708 0.997949157588566 +v 0.08254387703498076 -0.008129862100064111 0.9962766133549583 +v 0.07434713281691074 0.007322553312405944 -0.9968938529491425 +v 0.0701339736681371 -0.013950515243222399 -0.9971335787729753 +v 0.06588557362556458 -0.03521657269448042 -0.9968938529491425 +v 0.050563776444948805 -0.050563776444948805 -0.9971335788102194 +v 0.03521657269448042 -0.06588557362556458 -0.9968938529491425 +v 0.013950515001912144 -0.07013397245498848 -0.9971335788656702 +v -0.007322553312405944 -0.07434713281691074 -0.9968938529491425 +v -0.027364918258489152 -0.06606475851317518 -0.9971335790090124 +v -0.04739353433251381 -0.05774918757379055 -0.9968938529491425 +v -0.059456711975812496 -0.0397277037484163 -0.9971335789913638 +v -0.07149001397192478 -0.021686258725821972 -0.9968938529491425 +v -0.07150797912864008 8.757201972099599e-18 -0.9971335788228384 +v -0.07149001397192478 0.021686258725821972 -0.9968938529491425 +v -0.059456711808627646 0.03972770363670695 -0.9971335790064322 +v -0.04739353433251381 0.05774918757379055 -0.9968938529491425 +v -0.027364917947386387 0.0660647577621067 -0.9971335790699353 +v -0.007322553312405944 0.07434713281691074 -0.9968938529491425 +v 0.013950515243222399 0.0701339736681371 -0.9971335787729753 +v 0.03521657269448042 0.06588557362556458 -0.9968938529491425 +v 0.050563776444948805 0.050563776444948805 -0.9971335788102194 +v 0.06606475851317518 0.027364918258489152 -0.9971335790090124 +v 0.05832064792877003 0.038968609982895884 -0.9972359725241738 +v 0.05396059819212854 0.044284319581331354 -0.9972611422237504 +v 0.06156320281398564 0.032906217494416724 -0.9972611422734732 +v 0.07292054383949498 0.014504798490789662 -0.9969206608955334 +v 0.06810120585879835 0.020658275084853454 -0.9971592390509152 +v 0.08169084833721893 0.02478064781534686 -0.9960949999079954 +v 0.08332549864575431 0.0165744726013087 -0.9961256330455268 +v 0.08414399384747488 0.008287459840049494 -0.9961561197265474 +v 0.07849090365951931 0.03251199628043785 -0.9961256332419902 +v 0.07456744034168325 0.03985712686908108 -0.9961561196312256 +v 0.06996506477614946 0.046749160960613906 -0.9961864604111729 +v 0.06473447950632275 0.05312621489775816 -0.9962166555681746 +v 0.058931939606729815 0.058931939606729815 -0.9962467061092398 +v 0.060904326731940406 0.012114624394221668 -0.9978388030143713 +v 0.059139072209495366 0.017939641727015638 -0.9978610863179652 +v 0.056822512417338426 0.023536654449151775 -0.9978832630598878 +v 0.05398216959280606 0.028854071047960006 -0.9979053333183685 +v 0.05065033776386077 0.033843472328070695 -0.9979272981211579 +v 0.046863719710112724 0.03846006230751907 -0.9979491574946205 +v 0.042663043820338156 0.042663043820338156 -0.9979709123978946 +v 0.061502581792192326 0.006057475540073006 -0.9978610864324683 +v 0.04130616873795492 0.061819051657130655 -0.9969206611490139 +v 0.03449952926633111 0.05163219630917427 -0.9978388031411116 +v 0.029132397310620173 0.05450288143563654 -0.9978610863277096 +v 0.04514696075402321 0.055011729570494064 -0.9971592390501454 +v 0.05415606684427769 0.06598935781531828 -0.9960949999076105 +v 0.047200102209612384 0.07063994601425894 -0.9961256333175093 +v 0.039857126076758266 0.07456743885935144 -0.9961561197571853 +v 0.03809262121868866 0.04641599141891 -0.9979925630428949 +v 0.028452183887322187 0.06868964985130868 -0.9969206611586248 +v 0.03220138150753434 0.07774101328749881 -0.9961864605393786 +v 0.023536654640910315 0.056822512880284476 -0.997883263022336 +v 0.020658275291961434 0.0681012065415419 -0.9971592389974478 +v 0.024309408844385306 0.0801373815461411 -0.9962166558065164 +v 0.016259285629758192 0.08174094667313368 -0.9962467064160001 +v 0.01776824949905188 0.058574067719634824 -0.9979053332431503 +v 0.011884248055344299 0.05974614661929969 -0.9979272979153121 +v 4.552572033822715e-18 0.07434914181656938 -0.9969206609089086 +v 3.802376137633662e-18 0.06209751365099003 -0.997838803021059 +v -0.006057475602119169 0.06150258242215764 -0.99786108638503 +v 0.006975445744358536 0.07082289030284854 -0.997159239053834 +v 0.008367400540393113 0.08495564545259462 -0.9960949999094548 +v 5.2021736695786375e-18 0.08495794111481031 -0.9961256330755933 +v -0.00828745979234862 0.08414399336315975 -0.9961561197630178 +v 0.005942284261148706 0.06033302503283418 -0.997949157588566 +v -0.014504798490789662 0.07292054383949498 -0.9969206608955334 +v -0.016416122741930057 0.08252942011240534 -0.9961864602779934 +v -0.011998883402115038 0.06032245744827859 -0.997883262821664 +v -0.02065827484999634 0.06810120508457816 -0.9971592391115464 +v -0.02430940918568782 0.08013738267126477 -0.9962166557184051 +v -0.03189373520440825 0.07699828943523171 -0.996246706525092 +v -0.017768249540953768 0.05857406785776686 -0.9979053332323329 +v -0.023311789358073938 0.05627964008313953 -0.9979272981057403 +v -0.04130616873639175 0.06181905165479122 -0.9969206611492247 +v -0.03449952926554953 0.05163219630800455 -0.997838803141217 +v -0.03920561191954728 0.047772174599746225 -0.9978610862618175 +v -0.03354721786741657 0.06276243023251823 -0.9971592390709317 +v -0.04024158739628515 0.07528671543883945 -0.9960949999180038 +v -0.047200101857057856 0.07063994548662382 -0.9961256333650651 +v -0.05363866860594303 0.06535890619691534 -0.9961561197847657 +v -0.02857840431616753 0.053466433382551325 -0.9979491575094097 +v -0.0525727815304185 0.0525727815304185 -0.9969206609055387 +v -0.05950039516605342 0.05950039516605342 -0.996186460185787 +v -0.04349006841776013 0.04349006841776013 -0.9978832628714319 +v -0.05501173004075208 0.045146961139953964 -0.9971592390045556 +v -0.06473447784422709 0.053126213533711594 -0.9962166557293084 +v -0.0692966286634811 0.046302526206969355 -0.9962467066052252 +v -0.04731576707543766 0.03883104796864788 -0.9979053332286015 +v -0.050650338226995224 0.03384347263752726 -0.9979272980794156 +v -0.06868965036700583 0.028452184100930946 -0.996920661116794 +v -0.05737062286352325 0.023763689237328897 -0.9978388031250016 +v -0.05913907320998741 0.01793964203051156 -0.9978610862396141 +v -0.06276243046102867 0.03354721798955793 -0.9971592390515143 +v -0.07528671555309467 0.04024158745735583 -0.996094999908295 +v -0.07849090223312709 0.03251199568960682 -0.9961256333576919 +v -0.08091038584161314 0.024543897119795665 -0.996156119892478 +v -0.05346643324069056 0.028578404240341373 -0.9979491575214642 +v -0.07292054354228004 0.014504798431669937 -0.9969206609182432 +v -0.0825294189393381 0.016416122508592482 -0.9961864603676258 +v -0.06032245798264625 0.011998883508407368 -0.9978832627808336 +v -0.07082289003070061 0.006975445717554306 -0.9971592390743276 +v -0.0833400943576934 0.00820828265637408 -0.9962166557859433 +v -0.08334234636458963 1.0206494035883634e-17 -0.9962467064183604 +v -0.060914997424813926 0.005999603534962806 -0.9979053333098651 +v -0.06091664346216006 7.460137378762203e-18 -0.9979272979243236 +v -0.07292054354228004 -0.014504798431669937 -0.9969206609182432 +v -0.06090432658333294 -0.012114624364661805 -0.9978388030257261 +v -0.059139072065195156 -0.01793964168324265 -0.9978610863292657 +v -0.07082289003070061 -0.006975445717554306 -0.9971592390743276 +v -0.08495564531652065 -0.008367400526990997 -0.9960949999197016 +v -0.08332549791301229 -0.01657447245555726 -0.9961256331015146 +v -0.0809103869082762 -0.024543897443364347 -0.9961561198089449 +v -0.060333025673685106 -0.005942284324267009 -0.9979491575403079 +v -0.06868965036700583 -0.028452184100930946 -0.996920661116794 +v -0.0777410140057739 -0.032201381805053636 -0.9961864604811158 +v -0.05682251227869066 -0.023536654391721985 -0.9978832630711342 +v -0.06276243063750879 -0.033547218083888525 -0.997159239036518 +v -0.07385503482059035 -0.039476338206571614 -0.9962166556087939 +v -0.06929662964455387 -0.04630252686250125 -0.996246706516801 +v -0.05398216946108877 -0.028854070977555657 -0.9979053333295611 +v -0.0506503376402732 -0.03384347224549211 -0.9979272981322969 +v -0.0525727815304185 -0.0525727815304185 -0.9969206609055387 +v -0.04390957315136915 -0.04390957315136915 -0.9978388030193739 +v -0.03920561181837744 -0.047772174476470405 -0.9978610862737686 +v -0.0550117301954382 -0.04514696126690158 -0.9971592389895594 +v -0.06598935812779036 -0.05415606710071687 -0.9960949998773175 +v -0.06007433588702435 -0.06007433588702435 -0.9961256329352222 +v -0.053638668885797776 -0.06535890653791936 -0.9961561197517067 +v -0.04686371959576454 -0.03846006221367595 -0.9979491575057061 +v -0.04130616873639175 -0.06181905165479122 -0.9969206611492247 +v -0.04674916032635253 -0.06996506382691028 -0.9961864604967279 +v -0.03416992643393254 -0.051138910835495446 -0.9978832629956993 +v -0.033547218248803835 -0.06276243094604365 -0.9971592390103006 +v -0.03947633779194219 -0.07385503404487322 -0.9962166556747097 +v -0.03189373489437626 -0.07699828868674832 -0.9962467065858052 +v -0.028854071207237923 -0.053982169890794095 -0.9979053332930473 +v -0.02331178931160956 -0.05627963997096461 -0.9979272981148394 +v -0.014504798490789662 -0.07292054383949498 -0.9969206608955334 +v -0.012114624394221668 -0.060904326731940406 -0.9978388030143713 +v -0.006057475540073006 -0.061502581792192326 -0.9978610864324683 +v -0.020658275084853454 -0.06810120585879835 -0.9971592390509152 +v -0.02478064781534686 -0.08169084833721893 -0.9960949999079954 +v -0.0165744726013087 -0.08332549864575431 -0.9961256330455268 +v -0.008287459840049494 -0.08414399384747488 -0.9961561197265474 +v -0.017598494586653794 -0.05801446073616599 -0.9979491574760103 +v -1.3657715687877842e-17 -0.07434914181656938 -0.9969206609089086 +v -1.545741857817401e-17 -0.08414626757729225 -0.9961864602282005 +v -1.129814607352645e-17 -0.06150424370720606 -0.9978832629174824 +v 0.006975445623700151 -0.07082288907778331 -0.9971592391460851 +v 0.008208282832112094 -0.0833400961419915 -0.9962166556515802 +v 0.016259285942325764 -0.08174094824451704 -0.9962467062959329 +v 0.005999603429900589 -0.060914996358099265 -0.9979053333901919 +v 0.011884247954507599 -0.059746146112359344 -0.9979272979540467 +v 0.028452183887322187 -0.06868964985130868 -0.9969206611586248 +v 0.023763689130524516 -0.05737062260567467 -0.997838803145917 +v 0.029132397525038236 -0.05450288183678454 -0.9978610862936225 +v 0.02065827493462311 -0.06810120536355524 -0.997159239089699 +v 0.024780647740231684 -0.08169084808959737 -0.9960949999273874 +v 0.032511996181887375 -0.07849090342159742 -0.9961256332612892 +v 0.039857126748266025 -0.07456744011565418 -0.9961561196504323 +v 0.017598494473125094 -0.058014460361912 -0.9979491575053191 +v 0.04130616873795492 -0.061819051657130655 -0.9969206611490139 +v 0.04674916081890769 -0.06996506456407112 -0.9961864604302875 +v 0.03416992662492786 -0.05113891112134013 -0.9978832629699361 +v 0.04514696075402321 -0.055011729570494064 -0.9971592390501454 +v 0.0531262147367218 -0.06473447931009936 -0.9962166555871977 +v 0.05893193942809512 -0.05893193942809512 -0.9962467061281718 +v 0.038831048208989936 -0.04731576736829532 -0.9979053332002101 +v 0.043074571719841054 -0.043074571719841054 -0.9979272979447936 +v 0.061819051657130655 -0.04130616873795492 -0.9969206611490139 +v 0.05163219630917427 -0.03449952926633111 -0.9978388031411116 +v 0.05450288143563654 -0.029132397310620173 -0.9978610863277096 +v 0.055011729570494064 -0.04514696075402321 -0.9971592390501454 +v 0.06598935781531828 -0.05415606684427769 -0.9960949999076105 +v 0.07063994601425894 -0.047200102209612384 -0.9961256333175093 +v 0.07456743885935144 -0.039857126076758266 -0.9961561197571853 +v 0.046863720285873475 -0.03846006278003368 -0.9979491574388026 +v 0.06868964985130868 -0.028452183887322187 -0.9969206611586248 +v 0.07774101328749881 -0.03220138150753434 -0.9961864605393786 +v 0.056822512880284476 -0.023536654640910315 -0.997883263022336 +v 0.0681012065415419 -0.020658275291961434 -0.9971592389974478 +v 0.0801373815461411 -0.024309408844385306 -0.9962166558065164 +v 0.08174094667313368 -0.016259285629758192 -0.9962467064160001 +v 0.058574067719634824 -0.01776824949905188 -0.9979053332431503 +v 0.05974614661929969 -0.011884248055344299 -0.9979272979153121 +v 0.07434914181656936 0 -0.9969206609089086 +v 0.06150424370720605 0 -0.9978832629174824 +v 0.08414626757729225 0 -0.9961864602282005 +v 0.07082289030284854 -0.006975445744358536 -0.997159239053834 +v 0.08254387703498077 -0.008129862100064113 -0.9962766133549583 +v 0.06033302503283418 -0.005942284261148706 -0.997949157588566 +v 0.12370649725198746 0.012184026185423136 0.9919307827949524 +v 0.11895252391695976 0.03608385380357504 0.9919307827949524 +v 0.10962727293372154 0.05859699659049511 0.9919307827949524 +v 0.09608910605311394 0.07885829359292984 0.9919307827949524 +v 0.07885829359292984 0.09608910605311394 0.9919307827949524 +v 0.05859699659049511 0.10962727293372154 0.9919307827949524 +v 0.03608385380357504 0.11895252391695976 0.9919307827949524 +v 0.012184026185423136 0.12370649725198746 0.9919307827949524 +v -0.012184026185423136 0.12370649725198746 0.9919307827949524 +v -0.03608385380357504 0.11895252391695976 0.9919307827949524 +v -0.05859699659049511 0.10962727293372154 0.9919307827949524 +v -0.07885829359292984 0.09608910605311394 0.9919307827949524 +v -0.09608910605311394 0.07885829359292984 0.9919307827949524 +v -0.10962727293372154 0.05859699659049511 0.9919307827949524 +v -0.11895252391695976 0.03608385380357504 0.9919307827949524 +v -0.12370649725198746 0.012184026185423136 0.9919307827949524 +v -0.12370649725198746 -0.012184026185423136 0.9919307827949524 +v -0.11895252391695976 -0.03608385380357504 0.9919307827949524 +v -0.10962727293372154 -0.05859699659049511 0.9919307827949524 +v -0.09608910605311394 -0.07885829359292984 0.9919307827949524 +v -0.07885829359292984 -0.09608910605311394 0.9919307827949524 +v -0.05859699659049511 -0.10962727293372154 0.9919307827949524 +v -0.03608385380357504 -0.11895252391695976 0.9919307827949524 +v -0.012184026185423136 -0.12370649725198746 0.9919307827949524 +v 0.012184026185423136 -0.12370649725198746 0.9919307827949524 +v 0.03608385380357504 -0.11895252391695976 0.9919307827949524 +v 0.05859699659049511 -0.10962727293372154 0.9919307827949524 +v 0.07885829359292984 -0.09608910605311394 0.9919307827949524 +v 0.09608910605311394 -0.07885829359292984 0.9919307827949524 +v 0.10962727293372154 -0.05859699659049511 0.9919307827949524 +v 0.11895252391695976 -0.03608385380357504 0.9919307827949524 +v 0.12370649725198746 -0.012184026185423136 0.9919307827949524 +v 0.12371575178042572 0 0.9920046320955397 +v 0.12133858625467199 -0.024135745846113103 0.992004632142776 +v 0.11429844972061998 -0.04734396706136631 0.9920046327265591 +v 0.10286588584774592 -0.06873278612457248 0.9920046326885268 +v 0.0874802438083597 -0.0874802438083597 0.9920046326207196 +v 0.06873278612457248 -0.10286588584774592 0.9920046326885268 +v 0.04734396706136631 -0.11429844972061998 0.9920046327265591 +v 0.024135745846113103 -0.12133858625467199 0.992004632142776 +v -2.272621488955838e-17 -0.12371575178042574 0.9920046320955397 +v -0.024135745846113103 -0.12133858625467199 0.992004632142776 +v -0.04734396706136631 -0.11429844972061998 0.9920046327265591 +v -0.06873278612457248 -0.10286588584774592 0.9920046326885268 +v -0.0874802438083597 -0.0874802438083597 0.9920046326207196 +v -0.10286588584774592 -0.06873278612457248 0.9920046326885268 +v -0.11429844972061998 -0.04734396706136631 0.9920046327265591 +v -0.12133858625467199 -0.024135745846113103 0.992004632142776 +v -0.12371575178042572 1.5150810336678306e-17 0.9920046320955397 +v -0.12133858625467199 0.024135745846113103 0.992004632142776 +v -0.11429844972061998 0.04734396706136631 0.9920046327265591 +v -0.10286588584774592 0.06873278612457248 0.9920046326885268 +v -0.0874802438083597 0.0874802438083597 0.9920046326207196 +v -0.06873278612457248 0.10286588584774592 0.9920046326885268 +v -0.04734396706136631 0.11429844972061998 0.9920046327265591 +v -0.024135745846113103 0.12133858625467199 0.992004632142776 +v 7.575405168339153e-18 0.12371575178042572 0.9920046320955397 +v 0.024135745846113103 0.12133858625467199 0.992004632142776 +v 0.04734396706136631 0.11429844972061998 0.9920046327265591 +v 0.06873278612457248 0.10286588584774592 0.9920046326885268 +v 0.0874802438083597 0.0874802438083597 0.9920046326207196 +v 0.10286588584774592 0.06873278612457248 0.9920046326885268 +v 0.11429844972061998 0.04734396706136631 0.9920046327265591 +v 0.12133858625467199 0.024135745846113103 0.992004632142776 +v 0.12370649725198746 0.012184026185423136 -0.9919307827949524 +v 0.12370649725198746 -0.012184026185423136 -0.9919307827949524 +v 0.11895252391695976 -0.03608385380357504 -0.9919307827949524 +v 0.10962727293372154 -0.05859699659049511 -0.9919307827949524 +v 0.09608910605311394 -0.07885829359292984 -0.9919307827949524 +v 0.07885829359292984 -0.09608910605311394 -0.9919307827949524 +v 0.05859699659049511 -0.10962727293372154 -0.9919307827949524 +v 0.03608385380357504 -0.11895252391695976 -0.9919307827949524 +v 0.012184026185423136 -0.12370649725198746 -0.9919307827949524 +v -0.012184026185423136 -0.12370649725198746 -0.9919307827949524 +v -0.03608385380357504 -0.11895252391695976 -0.9919307827949524 +v -0.05859699659049511 -0.10962727293372154 -0.9919307827949524 +v -0.07885829359292984 -0.09608910605311394 -0.9919307827949524 +v -0.09608910605311394 -0.07885829359292984 -0.9919307827949524 +v -0.10962727293372154 -0.05859699659049511 -0.9919307827949524 +v -0.11895252391695976 -0.03608385380357504 -0.9919307827949524 +v -0.12370649725198746 -0.012184026185423136 -0.9919307827949524 +v -0.12370649725198746 0.012184026185423136 -0.9919307827949524 +v -0.11895252391695976 0.03608385380357504 -0.9919307827949524 +v -0.10962727293372154 0.05859699659049511 -0.9919307827949524 +v -0.09608910605311394 0.07885829359292984 -0.9919307827949524 +v -0.07885829359292984 0.09608910605311394 -0.9919307827949524 +v -0.05859699659049511 0.10962727293372154 -0.9919307827949524 +v -0.03608385380357504 0.11895252391695976 -0.9919307827949524 +v -0.012184026185423136 0.12370649725198746 -0.9919307827949524 +v 0.012184026185423136 0.12370649725198746 -0.9919307827949524 +v 0.03608385380357504 0.11895252391695976 -0.9919307827949524 +v 0.05859699659049511 0.10962727293372154 -0.9919307827949524 +v 0.07885829359292984 0.09608910605311394 -0.9919307827949524 +v 0.09608910605311394 0.07885829359292984 -0.9919307827949524 +v 0.10962727293372154 0.05859699659049511 -0.9919307827949524 +v 0.11895252391695976 0.03608385380357504 -0.9919307827949524 +v 0.11429844972061998 0.04734396706136631 -0.9920046327265591 +v 0.12133858625467199 0.024135745846113103 -0.992004632142776 +v 0.10286588584774592 0.06873278612457248 -0.9920046326885268 +v 0.08748024380835968 0.08748024380835968 -0.9920046326207196 +v 0.06873278612457248 0.10286588584774592 -0.9920046326885268 +v 0.04734396706136631 0.11429844972061998 -0.9920046327265591 +v 0.024135745846113103 0.12133858625467199 -0.992004632142776 +v 7.575405168339153e-18 0.12371575178042572 -0.9920046320955397 +v -0.024135745846113103 0.12133858625467199 -0.992004632142776 +v -0.04734396706136631 0.11429844972061998 -0.9920046327265591 +v -0.06873278612457248 0.10286588584774592 -0.9920046326885268 +v -0.08748024380835968 0.08748024380835968 -0.9920046326207196 +v -0.10286588584774592 0.06873278612457248 -0.9920046326885268 +v -0.11429844972061998 0.04734396706136631 -0.9920046327265591 +v -0.12133858625467199 0.024135745846113103 -0.992004632142776 +v -0.12371575178042572 1.5150810336678306e-17 -0.9920046320955397 +v -0.12133858625467199 -0.024135745846113103 -0.992004632142776 +v -0.11429844972061998 -0.04734396706136631 -0.9920046327265591 +v -0.10286588584774592 -0.06873278612457248 -0.9920046326885268 +v -0.08748024380835968 -0.08748024380835968 -0.9920046326207196 +v -0.06873278612457248 -0.10286588584774592 -0.9920046326885268 +v -0.04734396706136631 -0.11429844972061998 -0.9920046327265591 +v -0.024135745846113103 -0.12133858625467199 -0.992004632142776 +v -2.272621488955838e-17 -0.12371575178042574 -0.9920046320955397 +v 0.024135745846113103 -0.12133858625467199 -0.992004632142776 +v 0.04734396706136631 -0.11429844972061998 -0.9920046327265591 +v 0.06873278612457248 -0.10286588584774592 -0.9920046326885268 +v 0.08748024380835968 -0.08748024380835968 -0.9920046326207196 +v 0.10286588584774592 -0.06873278612457248 -0.9920046326885268 +v 0.11429844972061998 -0.04734396706136631 -0.9920046327265591 +v 0.12133858625467199 -0.024135745846113103 -0.992004632142776 +v 0.12371575178042572 0 -0.9920046320955397 +v 0.17359420657157898 0 0.9845016598701477 +v 0.17195150808306037 0.01693574558260502 0.9846446072454003 +v 0.16867211288337483 0.033550970249168664 0.9847868852751841 +v 0.1638027817947483 0.0496890295060109 0.9849285018424657 +v 0.15740510508672725 0.06519932967618113 0.9850694554356446 +v 0.1495547949550304 0.07993870239907394 0.985209751409815 +v 0.14034080610437746 0.09377272784618537 0.9853493915110191 +v 0.12986436497259407 0.10657693690208339 0.9854883800285218 +v 0.11823789921489847 0.11823789921489847 0.9856267189337792 +v 0.1055838175702064 0.12865424579829937 0.9857644118299628 +v 0.09203325765195786 0.13773750538537663 0.9859014616613574 +v 0.08183174207806587 0.15309642255306244 0.9845016598701477 +v 0.06612138712354987 0.15963114813983065 0.9846446064510382 +v 0.049922169701291604 0.1645713422353825 0.9847868867764595 +v 0.03339428491079294 0.16788440257293935 0.9849285001704352 +v 0.016699578339319045 0.16955366245412823 0.985069455598029 +v 1.0383682230545944e-17 0.16957840014083955 0.9852097508016024 +v -0.01654396584870333 0.16797370273897713 0.9853493915847759 +v -0.03277482561982391 0.16477017072808578 0.9854883780984679 +v -0.04853955867390518 0.16001348322203474 0.9856267185852494 +v -0.06369105389198297 0.15376380611868623 0.985764411522439 +v -0.078089456373514 0.14609509950735672 0.9859014619323118 +v -0.096443772315979 0.14433830603957176 0.9845016598701477 +v -0.10961269938879861 0.13356345155349422 0.9846446073205922 +v -0.12160581503256483 0.12160581503256483 0.9847868865422634 +v -0.13231886308527996 0.108591291833022 0.984928501671026 +v -0.1416608523258455 0.09465475466716962 0.9850694553090197 +v -0.14955479671663954 0.07993870334067378 0.9852097510576097 +v -0.15593835254915323 0.06459178073464933 0.9853493908687838 +v -0.16076426344676642 0.04876730530117421 0.9854883797045834 +v -0.1640006818267627 0.0326217647454353 0.9856267171410247 +v -0.16563132624504118 0.01631326183980636 0.9857644119608491 +v -0.1656554916226183 2.0286946813852908e-17 0.9859014614192796 +v -0.1727583035826683 -0.017015208024531603 0.9845016598701477 +v -0.1694635214864379 -0.03370839122376 0.9846446054993087 +v -0.16457134354442854 -0.04992217009838627 0.9847868865352546 +v -0.15814364892334615 -0.06550524462414826 0.9849285014686324 +v -0.15025650518862665 -0.08031377439029361 0.9850694557220404 +v -0.14099928435235712 -0.09421270901222885 0.9852097510220417 +v -0.13047368782197588 -0.1070769953375909 0.9853493916649401 +v -0.11879267078285652 -0.11879267078285652 0.9854883796290319 +v -0.10607921629829331 -0.12925789077885388 0.9856267186052352 +v -0.0924650772366095 -0.13838376896547866 0.9857644114814758 +v -0.0780894568179751 -0.14609510033888481 0.9859014617660611 +v -0.06643162667751312 -0.16038013249635696 0.9845016598701477 +v -0.0501564029125153 -0.16534350586770896 0.9846446077857933 +v -0.03355096986796054 -0.16867211096691231 0.9847868856197266 +v -0.016777932260541615 -0.17034920295711717 0.9849285023739907 +v -3.1297205580130173e-17 -0.17037405672135786 0.9850694558897499 +v 0.016621589640778187 -0.1687618301182786 0.9852097518120074 +v 0.03292860409307028 -0.16554326724212412 0.9853493904777599 +v 0.048767304718803646 -0.16076426152694745 0.9854883800583295 +v 0.0639898903955922 -0.15448526108309368 0.9856267190332302 +v 0.07845584972304684 -0.14678057314683787 0.9857644124906789 +v 0.09203325578550839 -0.13773750259203804 0.985901462253728 +v 0.11012700200080872 -0.1341901309788227 0.9845016598701477 +v 0.12217638928191271 -0.12217638928191271 0.9846446066391098 +v 0.1329397029075517 -0.10910080200573413 0.9847868862514969 +v 0.1423255247394928 -0.09509887464302416 0.9849285012514842 +v 0.15025650750303582 -0.08031377562737126 0.9850694552593116 +v 0.15667001496003968 -0.06489484530205611 0.9852097502486681 +v 0.16151856906063403 -0.04899612134324542 0.9853493912351684 +v 0.16477017269474656 -0.0327748260110171 0.9854883777449007 +v 0.16640846808692727 -0.01638980365059931 0.9856267185967308 +v 0.17359420657157898 0 -0.9845016598701477 +v 0.17195150808306037 0.016935745582605015 -0.9846446072454003 +v 0.16867211288337483 0.033550970249168664 -0.9847868852751841 +v 0.1638027817947483 0.0496890295060109 -0.9849285018424657 +v 0.15740510508672725 0.06519932967618113 -0.9850694554356446 +v 0.1495547949550304 0.07993870239907394 -0.985209751409815 +v 0.14034080610437746 0.09377272784618537 -0.9853493915110191 +v 0.1298643649725941 0.10657693690208339 -0.9854883800285218 +v 0.1182378992148985 0.1182378992148985 -0.9856267189337791 +v 0.10558381757020643 0.12865424579829943 -0.9857644118299628 +v 0.09203325765195788 0.13773750538537669 -0.9859014616613574 +v 0.08183174207806587 0.15309642255306244 -0.9845016598701477 +v 0.06612138712354987 0.15963114813983065 -0.9846446064510382 +v 0.049922169701291604 0.1645713422353825 -0.9847868867764595 +v 0.03339428491079294 0.16788440257293935 -0.9849285001704352 +v 0.01669957833931904 0.16955366245412823 -0.985069455598029 +v 1.0383682230545944e-17 0.16957840014083952 -0.9852097508016024 +v -0.016543965848703327 0.1679737027389771 -0.9853493915847759 +v -0.0327748256198239 0.16477017072808575 -0.9854883780984679 +v -0.04853955867390517 0.1600134832220347 -0.9856267185852494 +v -0.06369105389198296 0.15376380611868623 -0.985764411522439 +v -0.078089456373514 0.1460950995073567 -0.9859014619323118 +v -0.096443772315979 0.14433830603957176 -0.9845016598701477 +v -0.10961269938879861 0.13356345155349422 -0.9846446073205922 +v -0.12160581503256485 0.12160581503256485 -0.9847868865422634 +v -0.13231886308527996 0.10859129183302202 -0.984928501671026 +v -0.1416608523258455 0.09465475466716963 -0.9850694553090197 +v -0.14955479671663954 0.0799387033406738 -0.9852097510576097 +v -0.15593835254915323 0.06459178073464933 -0.9853493908687838 +v -0.16076426344676642 0.0487673053011742 -0.9854883797045834 +v -0.1640006818267627 0.0326217647454353 -0.9856267171410247 +v -0.16563132624504118 0.01631326183980636 -0.9857644119608491 +v -0.1656554916226183 2.0286946813852908e-17 -0.9859014614192796 +v -0.1727583035826683 -0.017015208024531603 -0.9845016598701477 +v -0.1694635214864379 -0.03370839122376 -0.9846446054993087 +v -0.16457134354442854 -0.04992217009838627 -0.9847868865352546 +v -0.15814364892334615 -0.06550524462414826 -0.9849285014686324 +v -0.15025650518862665 -0.08031377439029361 -0.9850694557220404 +v -0.14099928435235712 -0.09421270901222885 -0.9852097510220417 +v -0.13047368782197588 -0.1070769953375909 -0.9853493916649401 +v -0.11879267078285653 -0.11879267078285653 -0.9854883796290319 +v -0.10607921629829332 -0.12925789077885388 -0.9856267186052352 +v -0.0924650772366095 -0.1383837689654787 -0.9857644114814758 +v -0.07808945681797512 -0.14609510033888481 -0.9859014617660611 +v -0.06643162667751312 -0.16038013249635696 -0.9845016598701477 +v -0.0501564029125153 -0.16534350586770896 -0.9846446077857933 +v -0.03355096986796054 -0.16867211096691231 -0.9847868856197266 +v -0.016777932260541615 -0.17034920295711717 -0.9849285023739907 +v -3.1297205580130173e-17 -0.17037405672135786 -0.9850694558897499 +v 0.016621589640778187 -0.1687618301182786 -0.9852097518120074 +v 0.03292860409307028 -0.16554326724212412 -0.9853493904777599 +v 0.048767304718803646 -0.16076426152694742 -0.9854883800583295 +v 0.06398989039559219 -0.15448526108309368 -0.9856267190332302 +v 0.07845584972304684 -0.14678057314683787 -0.9857644124906789 +v 0.09203325578550839 -0.13773750259203804 -0.985901462253728 +v 0.11012700200080872 -0.1341901309788227 -0.9845016598701477 +v 0.12217638928191271 -0.12217638928191271 -0.9846446066391098 +v 0.1329397029075517 -0.10910080200573413 -0.9847868862514969 +v 0.1423255247394928 -0.09509887464302416 -0.9849285012514842 +v 0.15025650750303582 -0.08031377562737127 -0.9850694552593116 +v 0.15667001496003968 -0.06489484530205611 -0.9852097502486681 +v 0.16151856906063403 -0.048996121343245415 -0.9853493912351684 +v 0.16477017269474653 -0.0327748260110171 -0.9854883777449007 +v 0.16640846808692725 -0.016389803650599305 -0.9856267185967308 +v -0.20929744094610214 0.1717659756541252 -0.9623250365257263 +v 0.9632141590118408 -0.14287932589650154 -0.22252093255519867 +v -0.5903642177581787 0.7960140109062195 0.12434370815753937 +v -0.23660282045602798 0.9445713758468628 0.22252093255519867 +v -0.14112191647291183 0.17195754498243332 -0.974624902009964 +v -0.12302194895076114 0.18411534980518865 -0.9748572816843648 +v -0.10390535604534372 0.19439324708237052 -0.9750885998426841 +v -0.08396509416507296 0.202709659909164 -0.9753188575106306 +v -0.06457433849573135 0.21287307143211365 -0.974624902009964 +v -0.043199562917102634 0.21717885771551027 -0.9748572818980683 +v -0.021604955695132655 0.21935879307768008 -0.9750886014519153 +v 1.3435069804085699e-17 0.21941134850843638 -0.975318861332229 +v 0.0218040868639946 0.22138060629367828 -0.974624902009964 +v 0.043199563195630335 0.2171788591157634 -0.9748572815722077 +v 0.06398459836075449 0.21092895918040055 -0.9750886007444908 +v 0.08396509109968811 0.2027096525086711 -0.9753188593389189 +v 0.10486304014921188 0.1961849480867386 -0.974624902009964 +v 0.12302194443592043 0.18411534304825392 -0.9748572835391889 +v 0.13983308588008472 0.1703870999351392 -0.9750886014540703 +v 0.15514725488932843 0.15514725488932843 -0.9753188594261524 +v 0.17195754498243332 0.14112191647291183 -0.974624902009964 +v 0.18411534980518865 0.12302194895076114 -0.9748572816843648 +v 0.19439324708237052 0.10390535604534372 -0.9750885998426841 +v 0.202709659909164 0.08396509416507296 -0.9753188575106306 +v 0.21287307143211365 0.06457433849573135 -0.974624902009964 +v 0.21717885771551027 0.043199562917102634 -0.9748572818980683 +v 0.21935879307768008 0.021604955695132655 -0.9750886014519153 +v 0.21941134850843638 0 -0.975318861332229 +v 0.22138060629367828 -0.0218040868639946 -0.974624902009964 +v 0.2171788591157634 -0.043199563195630335 -0.9748572815722077 +v 0.21092895918040055 -0.06398459836075449 -0.9750886007444908 +v 0.2027096525086711 -0.08396509109968811 -0.9753188593389189 +v 0.1961849480867386 -0.10486304014921188 -0.974624902009964 +v 0.18411534304825392 -0.12302194443592043 -0.9748572835391889 +v 0.1703870999351392 -0.13983308588008472 -0.9750886014540703 +v 0.15514725488932843 -0.15514725488932843 -0.9753188594261524 +v 0.14112191647291183 -0.17195754498243332 -0.974624902009964 +v 0.12302194895076114 -0.18411534980518865 -0.9748572816843648 +v 0.10390535604534372 -0.19439324708237052 -0.9750885998426841 +v 0.08396509416507296 -0.202709659909164 -0.9753188575106306 +v 0.06457433849573135 -0.21287307143211365 -0.974624902009964 +v 0.043199562917102634 -0.21717885771551027 -0.9748572818980683 +v 0.021604955695132655 -0.21935879307768008 -0.9750886014519153 +v -4.030520894693015e-17 -0.21941134850843638 -0.975318861332229 +v -0.0218040868639946 -0.22138060629367828 -0.974624902009964 +v -0.043199563195630335 -0.2171788591157634 -0.9748572815722077 +v -0.06398459836075449 -0.21092895918040055 -0.9750886007444908 +v -0.08396509109968811 -0.2027096525086711 -0.9753188593389189 +v -0.10486304014921188 -0.1961849480867386 -0.974624902009964 +v -0.12302194443592043 -0.18411534304825392 -0.9748572835391889 +v -0.13983308588008472 -0.1703870999351392 -0.9750886014540703 +v -0.15514725488932843 -0.15514725488932843 -0.9753188594261524 +v -0.17195754498243332 -0.14112191647291183 -0.974624902009964 +v -0.18411534980518865 -0.12302194895076114 -0.9748572816843648 +v -0.19439324708237052 -0.10390535604534372 -0.9750885998426841 +v -0.202709659909164 -0.08396509416507296 -0.9753188575106306 +v -0.21287307143211365 -0.06457433849573135 -0.974624902009964 +v -0.21717885771551027 -0.043199562917102634 -0.9748572818980683 +v -0.21935879307768008 -0.021604955695132655 -0.9750886014519153 +v -0.21941134850843638 2.6870139608171398e-17 -0.975318861332229 +v -0.22138060629367828 0.0218040868639946 -0.974624902009964 +v -0.2171788591157634 0.043199563195630335 -0.9748572815722077 +v -0.21092895918040055 0.06398459836075449 -0.9750886007444908 +v -0.2027096525086711 0.08396509109968811 -0.9753188593389189 +v -0.1961849480867386 0.10486304014921188 -0.974624902009964 +v -0.18411534304825392 0.12302194443592043 -0.9748572835391889 +v -0.1703870999351392 0.13983308588008472 -0.9750886014540703 +v -0.15514725488932843 0.15514725488932843 -0.9753188594261524 +v -0.5727438628673553 -0.7722557187080383 0.27084046602249146 +v 0.010971690760925412 -0.11139744892716408 0.993480771780014 +v 0.021734190316338428 -0.10926515158095648 0.9935472730671909 +v 0.03218607616398665 -0.10610327397698431 0.9936134600527968 +v 0.042229872578916375 -0.10195193263049855 0.9936793327152863 +v 0.05276647675782442 -0.09871913678944111 0.993480771780014 +v 0.06189373478739735 -0.09263052138540914 0.9935472736450117 +v 0.07034002088987162 -0.08570956228317751 0.9936134600837969 +v 0.07803062866703428 -0.07803062866703428 0.993679332385824 +v 0.08652804605662823 -0.071011733263731 0.993480771780014 +v 0.09263052065790267 -0.0618937343012931 0.9935472737546591 +v 0.09778533499609464 -0.052267349335515446 0.9936134601215211 +v 0.10195192939267261 -0.042229871237765046 0.9936793331544698 +v 0.10711650736629963 -0.03249343717470765 0.993480771780014 +v 0.10926515164632523 -0.021734190329341083 0.9935472730588386 +v 0.11034372150036974 -0.010867907669531468 0.9936134600156671 +v 0.11035197637904527 0 0.9936793323095054 +v 0.11139744892716408 0.010971690760925412 0.993480771780014 +v 0.10926515158095648 0.021734190316338428 0.9935472730671909 +v 0.10610327397698431 0.03218607616398665 0.9936134600527968 +v 0.10195193263049855 0.042229872578916375 0.9936793327152863 +v 0.09871913678944111 0.05276647675782442 0.993480771780014 +v 0.09263052138540914 0.06189373478739735 0.9935472736450117 +v 0.08570956228317751 0.07034002088987162 0.9936134600837969 +v 0.07803062866703428 0.07803062866703428 0.993679332385824 +v 0.071011733263731 0.08652804605662823 0.993480771780014 +v 0.0618937343012931 0.09263052065790267 0.9935472737546591 +v 0.052267349335515446 0.09778533499609464 0.9936134601215211 +v 0.042229871237765046 0.10195192939267261 0.9936793331544698 +v 0.03249343717470765 0.10711650736629963 0.993480771780014 +v 0.021734190329341083 0.10926515164632523 0.9935472730588386 +v 0.010867907669531468 0.11034372150036974 0.9936134600156671 +v 6.757109919909716e-18 0.11035197637904527 0.9936793323095054 +v -0.010971690760925412 0.11139744892716408 0.993480771780014 +v -0.021734190316338428 0.10926515158095648 0.9935472730671909 +v -0.03218607616398665 0.10610327397698431 0.9936134600527968 +v -0.042229872578916375 0.10195193263049855 0.9936793327152863 +v -0.05276647675782442 0.09871913678944111 0.993480771780014 +v -0.06189373478739735 0.09263052138540914 0.9935472736450117 +v -0.07034002088987162 0.08570956228317751 0.9936134600837969 +v -0.07803062866703428 0.07803062866703428 0.993679332385824 +v -0.08652804605662823 0.071011733263731 0.993480771780014 +v -0.09263052065790267 0.0618937343012931 0.9935472737546591 +v -0.09778533499609464 0.052267349335515446 0.9936134601215211 +v -0.10195192939267261 0.042229871237765046 0.9936793331544698 +v -0.10711650736629963 0.03249343717470765 0.993480771780014 +v -0.10926515164632523 0.021734190329341083 0.9935472730588386 +v -0.11034372150036974 0.010867907669531468 0.9936134600156671 +v -0.11035197637904527 1.3514219839819434e-17 0.9936793323095054 +v -0.11139744892716408 -0.010971690760925412 0.993480771780014 +v -0.10926515158095648 -0.021734190316338428 0.9935472730671909 +v -0.10610327397698431 -0.03218607616398665 0.9936134600527968 +v -0.10195193263049855 -0.042229872578916375 0.9936793327152863 +v -0.09871913678944111 -0.05276647675782442 0.993480771780014 +v -0.09263052138540914 -0.06189373478739735 0.9935472736450117 +v -0.08570956228317751 -0.07034002088987162 0.9936134600837969 +v -0.07803062866703428 -0.07803062866703428 0.993679332385824 +v -0.071011733263731 -0.08652804605662823 0.993480771780014 +v -0.0618937343012931 -0.09263052065790267 0.9935472737546591 +v -0.052267349335515446 -0.09778533499609464 0.9936134601215211 +v -0.042229871237765046 -0.10195192939267261 0.9936793331544698 +v -0.03249343717470765 -0.10711650736629963 0.993480771780014 +v -0.021734190329341083 -0.10926515164632523 0.9935472730588386 +v -0.010867907669531468 -0.11034372150036974 0.9936134600156671 +v -2.0271329255986957e-17 -0.11035197637904527 0.9936793323095054 +v -0.09871913678944111 0.05276647675782442 -0.993480771780014 +v -0.09263052138540914 0.06189373478739735 -0.9935472736450117 +v -0.08570956228317751 0.07034002088987162 -0.9936134600837969 +v -0.07803062866703428 0.07803062866703428 -0.993679332385824 +v -0.071011733263731 0.08652804605662823 -0.993480771780014 +v -0.0618937343012931 0.09263052065790267 -0.9935472737546591 +v -0.052267349335515446 0.09778533499609464 -0.9936134601215211 +v -0.042229871237765046 0.10195192939267261 -0.9936793331544698 +v -0.03249343717470765 0.10711650736629963 -0.993480771780014 +v -0.021734190329341083 0.10926515164632523 -0.9935472730588386 +v -0.010867907669531468 0.11034372150036972 -0.9936134600156671 +v 6.7571099199097154e-18 0.11035197637904526 -0.9936793323095054 +v 0.010971690760925412 0.11139744892716408 -0.993480771780014 +v 0.02173419031633843 0.10926515158095648 -0.9935472730671909 +v 0.03218607616398665 0.1061032739769843 -0.9936134600527968 +v 0.04222987257891637 0.10195193263049854 -0.9936793327152863 +v 0.05276647675782442 0.09871913678944111 -0.993480771780014 +v 0.06189373478739735 0.09263052138540914 -0.9935472736450117 +v 0.07034002088987162 0.08570956228317751 -0.9936134600837969 +v 0.07803062866703428 0.07803062866703428 -0.993679332385824 +v 0.08652804605662823 0.071011733263731 -0.993480771780014 +v 0.09263052065790267 0.0618937343012931 -0.9935472737546591 +v 0.09778533499609464 0.052267349335515446 -0.9936134601215211 +v 0.10195192939267261 0.042229871237765046 -0.9936793331544698 +v 0.10711650736629963 0.03249343717470765 -0.993480771780014 +v 0.10926515164632523 0.021734190329341083 -0.9935472730588386 +v 0.11034372150036972 0.010867907669531468 -0.9936134600156671 +v 0.11035197637904526 0 -0.9936793323095054 +v 0.11139744892716408 -0.010971690760925412 -0.993480771780014 +v 0.10926515158095648 -0.02173419031633843 -0.9935472730671909 +v 0.1061032739769843 -0.03218607616398665 -0.9936134600527968 +v 0.10195193263049854 -0.04222987257891637 -0.9936793327152863 +v 0.09871913678944111 -0.05276647675782442 -0.993480771780014 +v 0.09263052138540914 -0.06189373478739735 -0.9935472736450117 +v 0.08570956228317751 -0.07034002088987162 -0.9936134600837969 +v 0.07803062866703428 -0.07803062866703428 -0.993679332385824 +v 0.071011733263731 -0.08652804605662823 -0.993480771780014 +v 0.0618937343012931 -0.09263052065790267 -0.9935472737546591 +v 0.052267349335515446 -0.09778533499609464 -0.9936134601215211 +v 0.042229871237765046 -0.10195192939267261 -0.9936793331544698 +v 0.03249343717470765 -0.10711650736629963 -0.993480771780014 +v 0.021734190329341083 -0.10926515164632523 -0.9935472730588386 +v 0.010867907669531468 -0.11034372150036972 -0.9936134600156671 +v -2.0271329255986954e-17 -0.11035197637904526 -0.9936793323095054 +v -0.010971690760925412 -0.11139744892716408 -0.993480771780014 +v -0.02173419031633843 -0.10926515158095648 -0.9935472730671909 +v -0.03218607616398665 -0.1061032739769843 -0.9936134600527968 +v -0.04222987257891637 -0.10195193263049854 -0.9936793327152863 +v -0.05276647675782442 -0.09871913678944111 -0.993480771780014 +v -0.06189373478739735 -0.09263052138540914 -0.9935472736450117 +v -0.07034002088987162 -0.08570956228317751 -0.9936134600837969 +v -0.07803062866703428 -0.07803062866703428 -0.993679332385824 +v -0.08652804605662823 -0.071011733263731 -0.993480771780014 +v -0.09263052065790267 -0.0618937343012931 -0.9935472737546591 +v -0.09778533499609464 -0.052267349335515446 -0.9936134601215211 +v -0.10195192939267261 -0.042229871237765046 -0.9936793331544698 +v -0.10711650736629963 -0.03249343717470765 -0.993480771780014 +v -0.10926515164632523 -0.021734190329341083 -0.9935472730588386 +v -0.11034372150036972 -0.010867907669531468 -0.9936134600156671 +v -0.11035197637904526 1.3514219839819431e-17 -0.9936793323095054 +v -0.11139744892716408 0.010971690760925412 -0.993480771780014 +v -0.10926515158095648 0.02173419031633843 -0.9935472730671909 +v -0.1061032739769843 0.03218607616398665 -0.9936134600527968 +v -0.10195193263049854 0.04222987257891637 -0.9936793327152863 +v -0.3280476927757263 0.9168318808078766 -0.22252093255519867 +v -0.005471613445251685 0.055554223726819216 -0.9983090157591197 +v -0.0108383848755503 0.05448823714264389 -0.9983290473264907 +v -0.016049753995259278 0.05290894692049752 -0.9983489831267572 +v -0.02119349186590039 0.0511656177215901 -0.9983421216194748 +v 3.4018070863905905e-18 0.05555572490567775 -0.9983290473914143 +v -0.05526043122814735 0.005442677411673502 0.9983311392215372 +v -0.054605283803516036 0.010861666903030521 0.9983201039570214 +v -0.053022601094390095 0.016084230609924127 0.9983400825820369 +v -0.05171110911178195 0.02141944180552787 0.9982978741291054 +v -0.055381264473785365 6.782248930825709e-18 0.9983421215287664 +v -0.06457433849573135 -0.21287307143211365 0.974624902009964 +v -0.043199562917102634 -0.21717885771551027 0.9748572818980683 +v -0.02160495569513265 -0.21935879307768008 0.9750886014519153 +v -4.030520894693015e-17 -0.21941134850843638 0.975318861332229 +v 0.0218040868639946 -0.22138060629367828 0.974624902009964 +v 0.043199563195630335 -0.2171788591157634 0.9748572815722077 +v 0.06398459836075449 -0.21092895918040055 0.9750886007444908 +v 0.0839650910996881 -0.2027096525086711 0.9753188593389189 +v 0.10486304014921188 -0.1961849480867386 0.974624902009964 +v 0.12302194443592042 -0.18411534304825392 0.9748572835391889 +v 0.13983308588008472 -0.1703870999351392 0.9750886014540704 +v 0.1551472548893284 -0.1551472548893284 0.9753188594261524 +v 0.17195754498243332 -0.14112191647291183 0.974624902009964 +v 0.18411534980518865 -0.12302194895076114 0.9748572816843648 +v 0.19439324708237052 -0.10390535604534372 0.9750885998426841 +v 0.202709659909164 -0.08396509416507296 0.9753188575106306 +v 0.21287307143211365 -0.06457433849573135 0.974624902009964 +v 0.21717885771551027 -0.043199562917102634 0.9748572818980683 +v 0.21935879307768008 -0.02160495569513265 0.9750886014519153 +v 0.21941134850843638 0 0.975318861332229 +v 0.22138060629367828 0.0218040868639946 0.974624902009964 +v 0.2171788591157634 0.043199563195630335 0.9748572815722077 +v 0.21092895918040055 0.06398459836075449 0.9750886007444908 +v 0.2027096525086711 0.0839650910996881 0.9753188593389189 +v 0.1961849480867386 0.10486304014921188 0.974624902009964 +v 0.18411534304825392 0.12302194443592042 0.9748572835391889 +v 0.1703870999351392 0.13983308588008472 0.9750886014540704 +v 0.1551472548893284 0.1551472548893284 0.9753188594261524 +v 0.14112191647291183 0.17195754498243332 0.974624902009964 +v 0.12302194895076114 0.18411534980518865 0.9748572816843648 +v 0.10390535604534372 0.19439324708237052 0.9750885998426841 +v 0.08396509416507296 0.202709659909164 0.9753188575106306 +v 0.06457433849573135 0.21287307143211365 0.974624902009964 +v 0.043199562917102634 0.21717885771551027 0.9748572818980683 +v 0.02160495569513265 0.21935879307768008 0.9750886014519153 +v 1.3435069804085699e-17 0.21941134850843638 0.975318861332229 +v -0.0218040868639946 0.22138060629367828 0.974624902009964 +v -0.043199563195630335 0.2171788591157634 0.9748572815722077 +v -0.06398459836075449 0.21092895918040055 0.9750886007444908 +v -0.0839650910996881 0.2027096525086711 0.9753188593389189 +v -0.10486304014921188 0.1961849480867386 0.974624902009964 +v -0.12302194443592042 0.18411534304825392 0.9748572835391889 +v -0.13983308588008472 0.1703870999351392 0.9750886014540704 +v -0.1551472548893284 0.1551472548893284 0.9753188594261524 +v -0.17195754498243332 0.14112191647291183 0.974624902009964 +v -0.18411534980518865 0.12302194895076114 0.9748572816843648 +v -0.19439324708237052 0.10390535604534372 0.9750885998426841 +v -0.202709659909164 0.08396509416507296 0.9753188575106306 +v -0.21287307143211365 0.06457433849573135 0.974624902009964 +v -0.21717885771551027 0.043199562917102634 0.9748572818980683 +v -0.21935879307768008 0.02160495569513265 0.9750886014519153 +v -0.21941134850843638 2.6870139608171398e-17 0.975318861332229 +v -0.22138060629367828 -0.0218040868639946 0.974624902009964 +v -0.2171788591157634 -0.043199563195630335 0.9748572815722077 +v -0.21092895918040055 -0.06398459836075449 0.9750886007444908 +v -0.2027096525086711 -0.0839650910996881 0.9753188593389189 +v -0.1961849480867386 -0.10486304014921188 0.974624902009964 +v -0.18411534304825392 -0.12302194443592042 0.9748572835391889 +v -0.1703870999351392 -0.13983308588008472 0.9750886014540704 +v -0.1551472548893284 -0.1551472548893284 0.9753188594261524 +v -0.14112191647291183 -0.17195754498243332 0.974624902009964 +v -0.12302194895076114 -0.18411534980518865 0.9748572816843648 +v -0.10390535604534372 -0.19439324708237052 0.9750885998426841 +v -0.08396509416507296 -0.202709659909164 0.9753188575106306 +v 0.0489933378994464 -0.9972822964191437 0.024930691346526146 +v 0.04862822592258456 0.989850252866745 -0.12434370815753937 +v 0.20929744094610214 0.1717659756541252 0.9623250365257263 +v 0.9824366867542267 -0.048264019191265106 -0.1736481785774231 +v -0.502915769815445 -0.5548820495605469 0.6616858243942261 +v -0.0211934918426682 -0.051165617665502636 -0.9983421216240242 +v -0.026175629835522515 -0.04897115824838129 -0.9983311392131282 +v -0.03093140811431992 -0.04629212589835666 -0.9983201040644543 +v -0.03515076364648146 -0.04283132736307807 -0.9983400825358101 +v -0.03957796896184447 -0.03957796896184447 -0.9982978740762916 +v -0.048628225922584624 -0.989850252866745 0.12434370815753937 +v -0.136379007703739 1.6701611938839738e-17 0.9904177184601843 +v 0.9685616791248322 -0.2426120713353157 -0.024930691346526146 +v 0.005442677359142395 -0.05526043069479003 -0.9983311392617006 +v -1.0227343192134984e-17 -0.055675064596308374 -0.9983201040253458 +v -0.005430979886885472 -0.055141664258014214 -0.9983400827330702 +v -0.01091953734593767 -0.05489621817816312 -0.9982978740737902 +v 0.010804349126080637 -0.05431712786837259 -0.9983421215436279 +v -0.005471613414228602 -0.05555422341183655 0.9983090157828387 +v -0.010838384814098575 -0.05448823683370519 0.9983290473500963 +v -1.0205420787718523e-17 -0.055555724590686584 0.9983290474150198 +v 0.0054193385330122915 -0.05502346760078806 0.9983489832983189 +v 0.010804349126080635 -0.05431712786837258 0.9983421215436279 +v -0.09643452081398704 0.09643452081398704 -0.9904177187227743 +v 0.2426120713353157 0.9685616791248322 0.024930691346526146 +v 0.055675064596308374 0 -0.9983201040253458 +v 0.05514166425801421 0.005430979886885471 -0.9983400827330702 +v 0.05489621817816312 0.01091953734593767 -0.9982978740737902 +v 0.053305103010814606 0.016169926624254878 -0.9983179591451704 +v 0.05121706802236484 0.021214803278991216 -0.9983379482324536 +v 0.04897115809938726 0.026175629755883556 -0.9983311392257889 +v 0.046047839362539325 0.030768181061388997 -0.9983421216271835 +v 0.9613436758518219 -0.2408040538430214 0.12434370815753937 +v -0.9026197791099548 0.42690734565258026 0.024930691346526146 +v 0.713890790939331 0.5294574946165085 0.4562106430530548 +v -0.5446074604988098 -0.6008817255496979 0.583743691444397 +v -0.5538528859615326 -0.7467842102050781 0.36534103751182556 +v 0.07859639823436737 0.2590976133942604 -0.9623250365257263 +v 0.2426120713353157 -0.9685616791248322 -0.024930691346526146 +v -0.7398285865783691 0.6705415546894073 0.024930691346526146 +v 0.8877293467521667 0.04361134395003319 0.4562106430530548 +v 0.048993337899446515 0.9972822964191437 0.024930691346526146 +v 0.9803174734115601 -0.14541635662317276 -0.12434370815753937 +v -0.7398285865783691 -0.6705415546894073 -0.024930691346526146 +v 0.9401176571846008 -0.3363794684410095 0.024930691346526146 +v -0.04897115803352862 -0.026175629720681378 0.9983311392313852 +v -0.04604783930074553 -0.030768181020099705 0.998342121632753 +v -0.051437053819365655 -0.021305924382724415 0.9983201041021716 +v -0.05302260171730399 -0.01608423079888298 0.9983400825332549 +v -0.05489621810385939 -0.01091953733115774 0.9982978740794677 +v -0.9972822964191437 -0.048993337899446425 -0.024930691346526146 +v -0.05143705381936566 -0.021305924382724415 -0.9983201041021716 +v -0.053022601717303997 -0.016084230798882982 -0.9983400825332549 +v -0.05489621810385939 -0.01091953733115774 -0.9982978740794677 +v -0.048865909594217445 -0.02611937326105284 -0.9983400826168203 +v -0.04604783930074554 -0.030768181020099705 -0.998342121632753 +v 0.0644275164231658 0.12053540907800198 -0.9903807938098907 +v 0.02130592450731858 0.05143705412016257 -0.9983201040777726 +v 0.02611937341379538 0.04886590987997865 -0.998340082592538 +v 0.031096209530519205 0.04653876863519607 -0.9982978741371604 +v 0.01608423089294137 0.053022602027372974 -0.9983400825089727 +v 0.010804349176498987 0.054317128121842764 -0.9983421215242607 +v 0.12917418851001805 0.12917418851001805 0.9828996169246227 +v -0.2590976133942604 -0.07859639823436737 -0.9623250365257263 +v 0.03522647196974269 -0.04292357822786695 0.9983311391667097 +v 0.039160468246080424 -0.039160468246080424 0.9983421215390014 +v 0.030931408209817577 -0.046292126041279 0.9983201040515727 +v 0.026119373354397995 -0.04886590976885396 0.9983400826019807 +v 0.02141944175212568 -0.05171110898285766 0.9982978741395632 +v -0.0066900144262394745 -0.06792485730455153 0.9973774696908053 +v 0.2590976133942604 0.07859639823436737 0.9623250365257263 +v 0.2590976133942604 -0.07859639823436737 -0.9623250365257263 +v 0.026606224369446704 -0.13375852020768114 -0.9904177184838026 +v 0.059347538849225646 0.048705267239544756 0.9967388988959625 +v 0.9541435837745667 0.23900053650140762 -0.1736481785774231 +v -0.7196463942527771 0.43133944272994995 0.542546272277832 +v -0.6688744723796844 0.7379892468452454 0.07473009079694748 +v 0.03831644356250755 -0.7799491286277771 -0.6234897971153259 +v -0.28079284727573395 -0.1500869020819664 0.9476327002048492 +v 0.29415184259414673 0.1218416802585125 0.9476327002048492 +v 0.14650818333029747 -0.9876779317855835 0.024930691346526146 +v 0.11339499982890416 -0.07576811459939943 0.990417718756678 +v 0.24200890213251114 0.9661536514759064 -0.07473009079694748 +v -0.9168318808078766 -0.3280476927757263 -0.22252093255519867 +v -0.42690734565258026 -0.9026197791099548 -0.024930691346526146 +v -0.06531454301855996 -0.01981295020453231 -0.9973774696392042 +v 0.06466851762187684 -0.06466851762187684 -0.9956387334466237 +v 0.789968729019165 0.2826553285121918 -0.542546272277832 +v -0.13375852020768114 0.026606224369446704 -0.9904177184838026 +v -0.5006093829870224 -0.8352162837982178 -0.22252093255519867 +v 0.8958931565284729 -0.4237259179353714 -0.12434370815753937 +v 0.7604652643203735 0.5639995038509369 -0.31848666071891785 +v 0.8436802923679352 -0.5056825131177902 -0.1736481785774231 +v -0.8120850324630737 0.48674504458904266 -0.31848666071891785 +v -0.48674504458904266 -0.8120850324630737 0.31848666071891785 +v 0.03217448510975933 -0.060194227731174554 0.9973774696213824 +v 0.8019906580448151 -0.5947968363761902 0.024930691346526146 +v 0.20929744094610214 0.1717659756541252 -0.9623250365257263 +v 0.9005541205406189 0.13358458504080772 0.4112870991230011 +v 0.1805665769942229 0.14818716453780922 0.9720829452792661 +v -0.5933180749416351 0.7999967932701111 0.07473009079694748 +v 0.6739024221897125 0.4998001158237457 -0.542546272277832 +v -0.3067071884870529 0.8571891486644745 -0.4112870991230011 +v 0.713890790939331 0.5294574946165085 -0.4562106430530548 +v -0.6739024221897125 -0.4998001158237457 -0.542546272277832 +v 0.22591141611337662 0.9018889665603638 0.36534103751182556 +v 0.048264019191265134 0.9824366867542267 -0.1736481785774231 +v -0.03541375382502136 0.043151781843592404 -0.9983090156975134 +v -0.03928382933787462 0.03928382933787462 -0.9983290475127713 +v -0.04273951681466675 0.03507541668150913 -0.998348983217919 +v -0.04604783959410655 0.03076818121611728 -0.9983421216063124 +v -0.03086510553823066 0.04619289708586646 -0.9983290475643334 +v -0.5244134813547134 0.5786011219024658 0.6234897971153259 +v 0.031207446940243244 0.3168545365333557 -0.9476327002048492 +v 0.4205523878335953 0.8891833126544952 0.1736481785774231 +v 0.7974753081798553 -0.4779882878065109 0.36534103751182556 +v 0.25014618784189224 0.1036139465868473 0.9623250365257263 +v -0.20929744094610214 -0.1717659756541252 0.9623250365257263 +v 0.7398285865783691 0.6705415546894073 -0.024930691346526146 +v -0.026606224369446704 -0.13375852020768114 0.9904177184838026 +v 0.07859639823436737 -0.2590976133942604 -0.9623250365257263 +v -0.03674570471048362 -0.7479760050773621 0.6616858243942261 +v -0.2826553285121918 -0.789968729019165 -0.542546272277832 +v 0.854299932718277 -0.5120476931333542 -0.07473009079694748 +v -0.3221018761396408 -0.17216704040765762 -0.9305844008922577 +v 0.14650818333029747 0.9876779317855835 -0.024930691346526146 +v 0.02631479288721364 0.04923151402080251 0.9983090157304595 +v 0.03086510578819516 0.04619289745996476 0.9983290475306159 +v 0.021260254623774913 0.051326797271653526 0.9983290475531973 +v 0.016049753870163812 0.052908946508112994 0.9983489831590521 +v 0.010804349176498987 0.054317128121842764 0.9983421215242607 +v -0.9026197791099548 -0.42690734565258026 -0.024930691346526146 +v 0.7398285865783691 0.6705415546894073 0.024930691346526146 +v -0.15042413771152496 0.22512561827898026 -0.9623250365257263 +v -0.08670485392212868 -0.10565016604959965 0.9903807938098907 +v 0.5056825131177902 -0.8436802923679352 0.1736481785774231 +v -0.6705415546894073 -0.7398285865783691 0.024930691346526146 +v 0.3355431854724884 -0.937780350446701 -0.07473009079694748 +v 0.8368442952632904 0.29942767322063446 -0.4562106430530548 +v 0.5538528859615326 -0.7467842102050781 -0.36534103751182556 +v -0.33387337625026703 0.7059159576892853 -0.6234897971153259 +v -0.4779882878065109 0.7974753081798553 0.36534103751182556 +v 0.6539329290390015 -0.7215037941932678 0.22252093255519867 +v 0.2826553285121918 0.789968729019165 -0.542546272277832 +v 0.1364230215549469 -0.9196893274784088 -0.36534103751182556 +v -0.04717683792114267 -0.9603066146373749 0.27084046602249146 +v -0.45693448185920715 -0.7623491287231445 0.4562106430530548 +v 0.17216704040765762 -0.3221018761396408 -0.9305844008922577 +v 0.7379892468452454 -0.6688744723796844 -0.07473009079694748 +v -0.8034656345844269 -0.3800109326839447 0.4562106430530548 +v 0.23660282045602798 -0.9445713758468628 0.22252093255519867 +v 0.04269755439877565 0.03504097901900726 -0.9983530513139149 +v 0.14614393562078476 0.9852223992347717 -0.07473009079694748 +v 0.8034656345844269 0.3800109326839447 -0.4562106430530548 +v -0.33742618560791016 -0.13976649940013885 0.9305844008922577 +v 0.22512561827898026 -0.15042413771152496 0.9623250365257263 +v 0.42584599554538727 -0.9003756940364838 -0.07473009079694748 +v 0.2590976133942604 0.07859639823436737 -0.9623250365257263 +v 0.7215037941932678 0.6539329290390015 0.22252093255519867 +v -0.8436802923679352 -0.5056825131177902 0.1736481785774231 +v -0.5727438628673553 0.7722557187080383 -0.27084046602249146 +v -0.6955835819244385 -0.41691675782203674 -0.583743691444397 +v 0.4156055897474289 0.2776987850666046 0.8657562136650085 +v -0.01339636160992086 -0.13601554557681084 0.9903807938098907 +v 0.1307885404676199 -0.03967427043244243 -0.9903807938098907 +v -0.9184115529060364 0.23005011677742004 0.31848666071891785 +v -0.08670485392212868 0.10565016604959965 0.9903807938098907 +v -0.25014618784189224 -0.1036139465868473 0.9623250365257263 +v 0.2408040538430214 -0.9613436758518219 0.12434370815753937 +v -0.9445713758468628 0.23660282045602798 0.22252093255519867 +v 0.3239077031612396 -0.9052614271640778 0.27084046602249146 +v -0.2694525271654129 -0.026538756676018238 -0.9623250365257263 +v -0.9725806415081024 -0.04777982458472246 -0.22252093255519867 +v -0.22591141611337662 0.9018889665603638 -0.36534103751182556 +v 0.07125234603881836 0.3582097291946411 0.9305844008922577 +v -0.7999967932701111 -0.5933180749416351 0.07473009079694748 +v 0.12917418851001805 0.12917418851001805 -0.9828996169246227 +v -0.04329957312603054 0.05276068108676839 -0.99737746960548 +v -0.41633328795433044 0.8802627921104431 0.22252093255519867 +v -0.5133238881826401 0.8564291596412659 -0.024930691346526146 +v -0.6409095823764801 0.580886572599411 0.5 +v 0.14432725310325623 -0.9729752838611603 -0.1736481785774231 +v 0.9401176571846008 0.3363794684410095 -0.024930691346526146 +v 0.5446074604988098 0.6008817255496979 -0.583743691444397 +v 0.4757005125284195 0.43114981055259705 -0.7660444378852844 +v -0.7196463942527771 -0.43133944272994995 0.542546272277832 +v 0.6243835985660553 0.6889011561870575 -0.36534103751182556 +v -0.22512561827898026 -0.15042413771152496 -0.9623250365257263 +v -0.19704720377922058 0.786656528711319 0.583743691444397 +v 0.3046779930591583 -0.09242305532097816 -0.9476327002048492 +v -0.5120476931333542 0.854299932718277 0.07473009079694748 +v -0.22121108323335648 -0.8831241130828857 0.4112870991230011 +v 0.1389222927391529 0.9365380108356476 0.31848666071891785 +v -0.25014618784189224 -0.1036139465868473 -0.9623250365257263 +v -0.07576811459939943 0.11339499982890416 -0.990417718756678 +v 0.09643452081398704 -0.09643452081398704 -0.9904177187227743 +v -0.13375852020768114 -0.026606224369446704 0.9904177184838026 +v 0.41831620037555695 0.31024444103240967 0.853290855884552 +v -0.3189624398946762 -0.8914403021335602 -0.31848666071891785 +v 0.9286325573921204 -0.045620791614055634 0.36534103751182556 +v 4.177474085728189e-18 0.06822332773377969 0.9973797319649838 +v 0.29140374064445496 -0.8144188821315765 0.5 +v 0.580886572599411 0.6409095823764801 -0.5 +v 0.14541635662317276 -0.9803174734115601 -0.12434370815753937 +v 0.06792485730455153 0.006690014426239475 -0.9973774696908053 +v -0.04329957312603055 0.05276068108676839 0.99737746960548 +v 0.7604652643203735 0.5639995038509369 0.31848666071891785 +v 0.4446897357702255 -0.7419200241565704 0.5 +v 0.2426120713353157 -0.9685616791248322 0.024930691346526146 +v 0.24200890213251114 -0.9661536514759064 -0.07473009079694748 +v -0.7312464714050293 -0.5423293709754944 0.4112870991230011 +v -0.048264019191265196 -0.9824366867542267 0.1736481785774231 +v 0.11458031833171844 0.7724377810955048 -0.6234897971153259 +v -0.039791919291019516 -0.8099831342697144 0.583743691444397 +v -0.18141166975437178 0.01786749042722549 0.9829684634381577 +v 0.12691925838589668 -0.8556201159954071 0.5 +v 0.678535133600235 -0.033334292471408844 -0.7330518960952759 +v -0.10491295531392097 -0.707265704870224 0.6982368230819702 +v 0.7288154661655426 -0.6605598330497742 -0.1736481785774231 +v -0.4998001158237457 -0.6739024221897125 -0.542546272277832 +v 0.9541435837745667 -0.23900053650140762 0.1736481785774231 +v 0.1717659756541252 0.20929744094610214 0.9623250365257263 +v 0.3221018761396408 0.17216704040765762 0.9305844008922577 +v -0.37986163794994354 -0.15734384208917618 -0.911222517490387 +v -0.8138698935508728 0.20386379212141037 0.542546272277832 +v 0.5294574946165085 0.713890790939331 -0.4562106430530548 +v 0.12053540907800198 0.0644275164231658 0.9903807938098907 +v 0.06414285635245974 0.0428588854717549 -0.9967112164676734 +v -0.04329957307544562 -0.05276068102513048 -0.9973774696114555 +v 0.031207446940243244 -0.3168545365333557 0.9476327002048492 +v -0.04116846993565567 -0.8380034267902374 -0.542546272277832 +v 0.012917381649283348 0.06494006014371839 -0.9975304383441437 +v -0.14541635662317276 -0.9803174734115601 0.12434370815753937 +v 0.02141944175212568 -0.05171110898285767 -0.9982978741395632 +v 0.026258536534794128 -0.049126265782065325 -0.998317959098851 +v 0.030799121667080806 -0.0460941452490013 -0.9983379480743658 +v 0.0352264719697427 -0.04292357822786695 -0.9983311391667097 +v 0.039160468246080424 -0.039160468246080424 -0.9983421215390014 +v -0.8831241130828857 -0.22121108323335648 0.4112870991230011 +v 0.048993337899446515 0.9972822964191437 -0.024930691346526146 +v -0.7059159576892853 -0.33387337625026703 -0.6234897971153259 +v -0.8958931565284729 -0.4237259179353714 -0.12434370815753937 +v -0.7379892468452454 -0.6688744723796844 -0.07473009079694748 +v -0.046047839594106556 0.03076818121611728 0.9983421216063124 +v -0.042923578081438124 0.035226471849571665 0.9983311391809053 +v -0.03936821659503996 0.03936821659503996 0.9983201040023206 +v -0.035150763664223816 0.042831327384697186 0.9983400825337142 +v -0.031096209530128413 0.04653876863461122 0.998297874137213 +v 0.6216683983802795 0.5634474158287048 -0.542546272277832 +v -0.21017354726791382 -0.8390597999095917 -0.5 +v -0.7215037941932678 -0.6539329290390015 0.22252093255519867 +v -0.1307885404676199 0.03967427043244243 -0.9903807938098907 +v 0.026538756676018238 0.2694525271654129 -0.9623250365257263 +v -0.9603066146373749 -0.047176837921142516 -0.27084046602249146 +v -0.15042413771152496 -0.22512561827898026 0.9623250365257263 +v -0.13358458504080772 0.9005541205406189 -0.4112870991230011 +v 0.20929744094610214 -0.1717659756541252 0.9623250365257263 +v -0.7015218138694763 0.6358223259449005 -0.31848666071891785 +v -0.9803174734115601 -0.14541635662317276 -0.12434370815753937 +v -0.04244266822934148 0.8639403283596039 0.5 +v 0.04777982458472243 -0.9725806415081024 -0.22252093255519867 +v -0.8914403021335602 -0.3189624398946762 -0.31848666071891785 +v 0.9286325573921204 0.045620791614055634 0.36534103751182556 +v -0.3189624398946762 0.8914403021335602 -0.31848666071891785 +v 0.4680432081222534 0.7808828949928284 -0.4112870991230011 +v 0.8621636927127838 0.2159607708454132 0.4562106430530548 +v 0.4237259179353714 0.8958931565284729 -0.12434370815753937 +v -0.5933180749416351 -0.7999967932701111 0.07473009079694748 +v 0.9052614271640778 -0.3239077031612396 0.27084046602249146 +v -0.40325896441936493 0.08021319285035133 -0.911222517490387 +v 0.07125234603881836 -0.3582097291946411 0.9305844008922577 +v 0.580886572599411 -0.6409095823764801 0.5 +v 0.2336168736219406 -0.9326508343219757 -0.27084046602249146 +v 0.7724377810955048 -0.11458031833171844 0.6234897971153259 +v 0.6423341333866119 -0.3850002735853195 0.6616858243942261 +v -0.41691675782203674 -0.6955835819244385 -0.583743691444397 +v -0.9168318808078766 0.3280476927757263 0.22252093255519867 +v 0.6589940786361694 -0.16506942361593246 -0.7330518960952759 +v 0.7623491287231445 0.45693448185920715 -0.4562106430530548 +v -0.9401176571846008 0.3363794684410095 0.024930691346526146 +v -0.8404862880706787 -0.39752040803432465 0.36534103751182556 +v -0.2461169809103012 -0.2019829973578453 0.9476327002048492 +v -1.2477930880702144e-17 -0.0679266927618877 0.9974019619131955 +v -0.07125234603881836 -0.3582097291946411 0.9305844008922577 +v -0.9729752838611603 -0.14432725310325623 -0.1736481785774231 +v -0.49429164826869965 0.8246757984161377 -0.27084046602249146 +v 0.15742528352203256 0.03131383657097449 -0.9868088450500065 +v 0.29415184259414673 0.1218416802585125 -0.9476327002048492 +v 0.04116846993565552 -0.8380034267902374 0.542546272277832 +v -0.5786011219024658 -0.5244134813547134 0.6234897971153259 +v 0.6113925874233246 -0.6745677590370178 0.4112870991230011 +v 0.9365380108356476 0.1389222927391529 -0.31848666071891785 +v 0.7821261882781982 0.5800643265247345 -0.22252093255519867 +v 0.6113925874233246 0.6745677590370178 0.4112870991230011 +v 0.047176837921142495 -0.9603066146373749 -0.27084046602249146 +v 0.7819346785545349 -0.3698275536298752 -0.5 +v 0.3351651579141617 -0.45191790163517 -0.826238751411438 +v -0.789968729019165 -0.2826553285121918 0.542546272277832 +v -0.6243835985660553 -0.6889011561870575 -0.36534103751182556 +v 0.758459746837616 -0.3587247431278229 -0.542546272277832 +v 0.026606214695112783 0.08770893618089029 0.995623708285796 +v -0.7799491286277771 -0.03831644356250758 0.6234897971153259 +v 0.9852223992347717 -0.14614393562078476 -0.07473009079694748 +v -0.14614393562078476 -0.9852223992347717 -0.07473009079694748 +v 0.04269755439877565 0.035040979019007254 0.9983530513139149 +v 0.046047839362539325 0.030768181061388993 0.9983421216271835 +v 0.04897115809938726 0.026175629755883556 0.9983311392257889 +v 0.05143705388868955 0.02130592441143931 0.9983201040965486 +v 0.053022601788764785 0.016084230820560375 0.9983400825276586 +v 0.05489621817816312 0.01091953734593767 0.9982978740737902 +v 0.05543545640009235 0.00545991588534224 0.9983179592975052 +v 0.0554369543696277 0 0.9983379481048016 +v -0.02248384201784208 0.07411929387792146 -0.9966879474149757 +v 0.5133238881826401 0.8564291596412659 -0.024930691346526146 +v -0.1532066752974633 0.10236942811245063 -0.9826209851122429 +v -0.3800109326839447 0.8034656345844269 -0.4562106430530548 +v -0.7722557187080383 0.5727438628673553 0.27084046602249146 +v -0.6412400305271149 -0.03150210157036778 0.7660444378852844 +v -0.5947968363761902 -0.8019906580448151 -0.024930691346526146 +v 0.06691243528571769 0.013309711442505666 -0.9973797319549524 +v -0.2159607708454132 0.8621636927127838 0.4562106430530548 +v -0.4757005125284195 0.43114981055259705 -0.7660444378852844 +v -0.11899254471063614 0.8021825551986694 0.583743691444397 +v 0.5094984471797943 -0.8500467836856842 -0.12434370815753937 +v -0.03979191929101942 0.8099831342697144 -0.583743691444397 +v 0.8831241130828857 0.22121108323335648 -0.4112870991230011 +v -0.8404862880706787 -0.39752040803432465 -0.36534103751182556 +v 0.051033046659158206 -0.07637635247842571 -0.9956086105976816 +v -0.7574874460697174 -0.18974072486162186 -0.6234897971153259 +v -0.7312464714050293 -0.5423293709754944 -0.4112870991230011 +v -0.026606224369446704 0.13375852020768114 -0.9904177184838026 +v -0.9196893274784088 0.1364230215549469 0.36534103751182556 +v -0.7604652643203735 0.5639995038509369 -0.31848666071891785 +v -0.08486485582691114 -0.03515217380350395 0.9956086106230957 +v 0.0679266927618877 0 -0.9974019619131955 +v 0.9972822964191437 -0.04899333789944649 0.024930691346526146 +v 0.24200890213251114 0.9661536514759064 0.07473009079694748 +v 0.5133238881826401 0.8564291596412659 0.024930691346526146 +v -0.23005011677742004 0.9184115529060364 0.31848666071891785 +v -0.14650818333029747 -0.9876779317855835 -0.024930691346526146 +v 0.08021319285035133 0.40325896441936493 0.911222517490387 +v -0.048871532082557644 0.9948029220104218 0.07473009079694748 +v -0.4237259179353714 -0.8958931565284729 -0.12434370815753937 +v 0.2461169809103012 -0.2019829973578453 -0.9476327002048492 +v 0.7799491286277771 -0.03831644356250763 -0.6234897971153259 +v 0.5786011219024658 0.5244134813547134 -0.6234897971153259 +v -0.6513691544532776 0.48308829963207245 -0.583743691444397 +v 0.8831241130828857 -0.22121108323335648 -0.4112870991230011 +v -0.7623491287231445 -0.45693448185920715 -0.4562106430530548 +v 0.41633328795433044 0.8802627921104431 0.22252093255519867 +v 0.726435124874115 -0.1819625273346901 -0.6616858243942261 +v -0.6589940786361694 -0.16506942361593246 -0.7330518960952759 +v 0.3057038336992264 -0.646356463432312 0.6982368230819702 +v 0.8802627921104431 -0.41633328795433044 -0.22252093255519867 +v -0.36522747576236725 4.472746559192247e-17 -0.9305844008922577 +v -0.04899333789944658 -0.9972822964191437 0.024930691346526146 +v 0.9613436758518219 -0.2408040538430214 -0.12434370815753937 +v -0.9456450939178467 -0.0464565642178058 0.31848666071891785 +v -0.021193491865900388 0.0511656177215901 0.9983421216194748 +v -0.016118873223378724 0.05313680249298846 0.998331139182771 +v -0.010861666849884355 0.054605283536332205 0.9983201039774365 +v -0.005430979856213339 0.055141663946594796 0.998340082756521 +v 3.4272781895391353e-18 0.05597169956820036 0.9982978740771341 +v -0.41633328795433044 -0.8802627921104431 0.22252093255519867 +v -0.16250290253623456 -0.08685961114529443 0.9826209853187533 +v -0.18141166975437178 0.01786749042722549 -0.9829684634381577 +v 0.5446074604988098 -0.6008817255496979 0.583743691444397 +v -0.7051016688346863 0.25228941440582275 0.6616858243942261 +v -0.7722557187080383 -0.5727438628673553 0.27084046602249146 +v 0.01339636160992086 -0.13601554557681084 -0.9903807938098907 +v 0.04467159882187835 -0.9093112945556641 -0.4112870991230011 +v 0.9331116080284119 0.3338726758956909 -0.12434370815753937 +v 0.6955835819244385 -0.41691675782203674 0.583743691444397 +v 0.04467159882187846 0.9093112945556641 0.4112870991230011 +v 0.43133944272994995 0.7196463942527771 0.542546272277832 +v 0.9445713758468628 -0.23660282045602798 -0.22252093255519867 +v 0.04653876863519608 -0.031096209530519205 0.9982978741371604 +v 0.049126265390406536 -0.026258536325448157 0.9983179591321318 +v 0.05121706928357467 -0.021214803801401453 0.9983379481301506 +v 0.05313680242392245 -0.01611887320242778 0.9983311391881797 +v 0.054317128121842764 -0.010804349176498987 0.9983421215242607 +v 0.07576811459939943 -0.11339499982890416 0.990417718756678 +v -0.29415184259414673 -0.1218416802585125 0.9476327002048492 +v 0.048264019191265016 -0.9824366867542267 0.1736481785774231 +v -0.17216704040765762 -0.3221018761396408 -0.9305844008922577 +v -0.789968729019165 -0.2826553285121918 -0.542546272277832 +v -0.9326508343219757 0.2336168736219406 -0.27084046602249146 +v 0.16481554529325124 0.16481554529325124 0.972198074265307 +v 0.8299330174922943 -0.12310893461108208 0.542546272277832 +v 0.03790284900214301 0.056725623983152465 -0.9973797321450628 +v -0.22591141611337662 -0.9018889665603638 -0.36534103751182556 +v 0.8368442952632904 -0.29942767322063446 -0.4562106430530548 +v -0.3582097291946411 0.07125234603881836 0.9305844008922577 +v 0.5094984471797943 0.8500467836856842 -0.12434370815753937 +v 0.8556201159954071 0.12691925838589668 0.5 +v 0.3183876574039459 0 -0.9476327002048492 +v 0.14287932589650154 0.9632141590118408 0.22252093255519867 +v 0.17023413472004895 0.07051328903533521 0.9826209851755554 +v 0.10988323390483856 -0.7407726049423218 -0.6616858243942261 +v 0.4237259179353714 -0.8958931565284729 -0.12434370815753937 +v -0.3280476927757263 -0.9168318808078766 -0.22252093255519867 +v -6.709119673352248e-17 -0.36522747576236725 0.9305844008922577 +v 0.9331116080284119 0.3338726758956909 0.12434370815753937 +v 0.6272165179252625 -0.46517546474933624 -0.6234897971153259 +v 0.14107617735862732 -0.9510583281517029 -0.27084046602249146 +v -0.6456802487373352 -0.7123983800411224 0.27084046602249146 +v 0.026107936508923346 -0.06303013622849167 0.9973797321522708 +v 0.8436802923679352 0.5056825131177902 -0.1736481785774231 +v 0.9184115529060364 -0.23005011677742004 0.31848666071891785 +v 0.646356463432312 -0.3057038336992264 -0.6982368230819702 +v -0.07812302072764617 -0.015539635443613597 -0.9965231470098789 +v -0.4562258869409561 -0.503367692232132 0.7330518960952759 +v -0.41633328795433044 0.8802627921104431 -0.22252093255519867 +v -0.763554573059082 -0.2732041925191879 0.583743691444397 +v -0.17216704040765762 0.3221018761396408 0.9305844008922577 +v -0.9184115529060364 -0.23005011677742004 0.31848666071891785 +v -0.9401176571846008 -0.3363794684410095 0.024930691346526146 +v 0.7960140109062195 -0.5903642177581787 0.12434370815753937 +v -0.9541435837745667 0.23900053650140762 -0.1736481785774231 +v -0.42690734565258026 -0.9026197791099548 0.024930691346526146 +v 0.5965497195720673 -0.08848978951573372 0.7971324920654297 +v -0.9510583281517029 0.14107617735862732 -0.27084046602249146 +v -0.8556201159954071 0.12691925838589668 0.5 +v 0.5094984471797943 -0.8500467836856842 0.12434370815753937 +v 0.04161979098635593 0.05071386049470203 0.9975759010465202 +v -0.5120476931333542 -0.854299932718277 -0.07473009079694748 +v 0.04467159882187846 0.9093112945556641 -0.4112870991230011 +v 0.1036139465868473 -0.25014618784189224 -0.9623250365257263 +v -0.9972822964191437 0.04899333789944655 -0.024930691346526146 +v -0.08574745213690463 0.16042219969559957 -0.983036988423889 +v -0.9632141590118408 0.14287932589650154 -0.22252093255519867 +v 0.04562079161405554 -0.9286325573921204 0.36534103751182556 +v -0.18974072486162186 0.7574874460697174 -0.6234897971153259 +v -0.04645656421780595 -0.9456450939178467 0.31848666071891785 +v -0.8802627921104431 0.41633328795433044 0.22252093255519867 +v 0.6889011561870575 -0.6243835985660553 0.36534103751182556 +v -0.08848978951573372 0.5965497195720673 -0.7971324920654297 +v 0.0492315140208025 -0.02631479288721364 -0.9983090157304595 +v 0.04619289745996475 -0.03086510578819516 -0.9983290475306159 +v 0.05132679727165352 -0.021260254623774913 -0.9983290475531973 +v 0.05290894650811299 -0.01604975387016381 -0.9983489831590521 +v 0.054317128121842764 -0.010804349176498987 -0.9983421215242607 +v -0.13358458504080772 0.9005541205406189 0.4112870991230011 +v 0.048871532082557706 0.9948029220104218 0.07473009079694748 +v -0.2630738243460655 -0.73524209856987 0.6234897971153259 +v -0.8621636927127838 -0.2159607708454132 -0.4562106430530548 +v 0.9685616791248322 -0.2426120713353157 0.024930691346526146 +v 0.20386379212141037 0.8138698935508728 -0.542546272277832 +v -0.5120476931333542 0.854299932718277 -0.07473009079694748 +v 0.5446074604988098 -0.6008817255496979 -0.583743691444397 +v 0.23005011677742004 -0.9184115529060364 0.31848666071891785 +v -0.05538126447378537 6.78224893082571e-18 -0.9983421215287664 +v -0.05526043122814736 0.0054426774116735025 -0.9983311392215372 +v -0.05460528380351604 0.010861666903030521 -0.9983201039570214 +v -0.0530226010943901 0.01608423060992413 -0.9983400825820369 +v -0.05171110911178196 0.02141944180552787 -0.9982978741291054 +v 0.8891833126544952 -0.4205523878335953 -0.1736481785774231 +v -0.7330991625785828 0.3467300832271576 -0.583743691444397 +v 0.02599441926411625 0.06275608136579658 0.9974019620904804 +v 0.2582548186182976 0.2582548186182976 0.9305844008922577 +v 0.048264019191265134 0.9824366867542267 0.1736481785774231 +v -0.03957796896184447 -0.03957796896184447 0.9982978740762916 +v -0.0353380455890886 -0.04305953108521357 0.9983179590562977 +v -0.030799121484891716 -0.04609414497633607 0.998337948098941 +v -0.02617562983552251 -0.048971158248381275 0.9983311392131282 +v -0.021193491842668197 -0.05116561766550262 0.9983421216240242 +v 0.989850252866745 -0.048628225922584534 0.12434370815753937 +v -0.5548820495605469 0.502915769815445 0.6616858243942261 +v -0.5727438628673553 -0.7722557187080383 -0.27084046602249146 +v -0.1528758913107191 -0.10214840528498023 -0.982691132968754 +v 0.9510583281517029 -0.14107617735862732 -0.27084046602249146 +v -0.9852223992347717 0.14614393562078476 0.07473009079694748 +v -0.1130345156416297 -0.034288645489141345 -0.9927057772874832 +v 0.3280476927757263 0.9168318808078766 -0.22252093255519867 +v 0.9003756940364838 0.42584599554538727 -0.07473009079694748 +v -0.9026197791099548 -0.42690734565258026 0.024930691346526146 +v -0.0584019207702376 0.07116294612852893 -0.9955934402749207 +v -0.9456450939178467 0.04645656421780592 -0.31848666071891785 +v -0.9510583281517029 0.14107617735862732 0.27084046602249146 +v -0.05496944535258295 -0.005414017806325604 -0.9983530513367586 +v -0.08962360769510269 0.13413120992481709 0.9866662323474884 +v -0.29942767322063446 -0.8368442952632904 -0.4562106430530548 +v 0.9286325573921204 0.045620791614055634 -0.36534103751182556 +v -0.07125234603881836 0.3582097291946411 -0.9305844008922577 +v -0.9948029220104218 0.04887153208255774 -0.07473009079694748 +v -0.8390597999095917 -0.21017354726791382 0.5 +v -0.047779824584722484 0.9725806415081024 0.22252093255519867 +v 0.6955835819244385 -0.41691675782203674 -0.583743691444397 +v -0.8368442952632904 0.29942767322063446 -0.4562106430530548 +v 0.9184115529060364 0.23005011677742004 0.31848666071891785 +v -0.2336168736219406 -0.9326508343219757 -0.27084046602249146 +v -0.04826401919126507 0.9824366867542267 -0.1736481785774231 +v 0.4998001158237457 0.6739024221897125 0.542546272277832 +v 0.05934753884922564 0.048705267239544756 -0.9967388988959625 +v -0.04320614735854796 -0.08083301734501694 0.9956237082198927 +v 0.10565016604959965 -0.08670485392212868 -0.9903807938098907 +v 0.7379892468452454 0.6688744723796844 -0.07473009079694748 +v 0.6456802487373352 0.7123983800411224 0.27084046602249146 +v -0.05505352480804285 -0.03678558799695421 -0.9975304385618303 +v 0.14305436940955588 -0.11740169587801202 -0.9824797005834816 +v 0.8564291596412659 0.5133238881826401 0.024930691346526146 +v -0.6272165179252625 -0.46517546474933624 -0.6234897971153259 +v -0.8639403283596039 -0.04244266822934146 0.5 +v 0.8754029273986816 0.3132241666316986 -0.36534103751182556 +v -0.6132798492908478 -0.36758580803871155 0.6982368230819702 +v -0.3582097291946411 0.07125234603881836 -0.9305844008922577 +v 0.6015045046806335 -0.4461061358451843 0.6616858243942261 +v 0.3057038336992264 -0.646356463432312 -0.6982368230819702 +v 0.1717659756541252 -0.20929744094610214 -0.9623250365257263 +v -0.9541435837745667 0.23900053650140762 0.1736481785774231 +v 0.8564291596412659 0.5133238881826401 -0.024930691346526146 +v -0.6732082962989807 0.24087776988744736 0.6982368230819702 +v -0.8831241130828857 0.22121108323335648 -0.4112870991230011 +v 0.8352162837982178 -0.5006093829870224 -0.22252093255519867 +v 0.7059159576892853 -0.33387337625026703 0.6234897971153259 +v 0.440823569893837 0.23562511056661606 0.8657562136650085 +v 0.73524209856987 -0.2630738243460655 0.6234897971153259 +v -0.19145360589027405 -0.19145360589027405 0.9623250365257263 +v 0.6044838428497314 0.2162877768278122 -0.7660444378852844 +v 0.28079284727573395 -0.1500869020819664 -0.9476327002048492 +v -0.11339499982890416 -0.07576811459939943 -0.990417718756678 +v -0.6456802487373352 0.7123983800411224 0.27084046602249146 +v -0.48308829963207245 0.6513691544532776 0.583743691444397 +v 0.6358223259449005 0.7015218138694763 0.31848666071891785 +v 0.8436802923679352 0.5056825131177902 0.1736481785774231 +v 0.07125234603881836 -0.3582097291946411 -0.9305844008922577 +v 0.7999967932701111 0.5933180749416351 -0.07473009079694748 +v 0.6243835985660553 0.6889011561870575 0.36534103751182556 +v 0.7215037941932678 -0.6539329290390015 0.22252093255519867 +v 0.13358458504080772 0.9005541205406189 -0.4112870991230011 +v 0.8831241130828857 -0.22121108323335648 0.4112870991230011 +v 0.678535133600235 0.033334292471408844 -0.7330518960952759 +v -0.8138698935508728 -0.20386379212141037 -0.542546272277832 +v -0.03967427043244243 0.1307885404676199 0.9903807938098907 +v -0.9196893274784088 0.1364230215549469 -0.36534103751182556 +v 0.22591141611337662 -0.9018889665603638 -0.36534103751182556 +v 0.046593428381300706 0.15359794962683126 0.9868088458006443 +v -0.006690014457262557 0.06792485761953419 -0.9973774696670863 +v -0.6697914600372314 -0.4014575481414795 -0.6234897971153259 +v -0.1081251895566463 -0.044786919149565674 -0.9928419829405144 +v -0.1805665769942229 0.14818716453780922 -0.972082945279266 +v 0.11899254471063614 -0.8021825551986694 -0.583743691444397 +v 0.19464330760343834 0.13005650742475494 0.9719672863218253 +v -0.14541635662317276 0.9803174734115601 0.12434370815753937 +v 0.14650818333029747 -0.9876779317855835 -0.024930691346526146 +v -0.17688670754432678 0.26472966372966766 0.9476327002048492 +v -0.4237259179353714 -0.8958931565284729 0.12434370815753937 +v -0.08809543832862632 0.026723458832817966 -0.9955934403287768 +v -0.06780724383508038 0.22353052970244774 0.9720829449244762 +v 0.052189986010951636 0.12599777517998162 0.9904177187756942 +v -0.01792042310160019 0.09009204901994967 0.9956086105215345 +v -0.12310893461108208 -0.8299330174922943 0.542546272277832 +v -0.7821261882781982 0.5800643265247345 -0.22252093255519867 +v -0.037738047585162145 -0.056478981245143334 -0.997401962072462 +v 0.5006093829870224 0.8352162837982178 0.22252093255519867 +v 0.5244134813547134 -0.5786011219024658 -0.6234897971153259 +v -0.20929744094610214 0.1717659756541252 0.9623250365257263 +v 0.19145360589027405 0.19145360589027405 0.9623250365257263 +v -0.8500467836856842 -0.5094984471797943 -0.12434370815753937 +v 0.48674504458904266 0.8120850324630737 0.31848666071891785 +v 0.0646685177111942 0.0646685177111942 -0.9956387334371577 +v 0.5120476931333542 0.854299932718277 0.07473009079694748 +v 0.11339499982890416 0.07576811459939943 -0.990417718756678 +v -0.789968729019165 0.2826553285121918 0.542546272277832 +v 0.16131823509931564 0 -0.9866662323474884 +v 0.07125234603881836 0.3582097291946411 -0.9305844008922577 +v 0.312269926071167 0.062114352360367775 -0.9476327002048492 +v 0.016033995689464384 -0.05285699874506103 0.9983530513192642 +v 0.10601993650197983 -0.3495008945465088 0.9305844008922577 +v 0.19192341919214623 0.1739492490182921 0.96553837427754 +v -0.03790284900097064 0.05672562398139788 0.9973797321452209 +v -0.580886572599411 0.6409095823764801 0.5 +v 0.9824366867542267 -0.048264019191265106 0.1736481785774231 +v 0.14151670044207706 0.11613976332618665 0.982830447471994 +v -0.07604213452813069 0.05080972935318903 0.9956387336851504 +v 0.2732041925191879 -0.763554573059082 -0.583743691444397 +v 0.12763362750411034 -0.23878572136163712 -0.9623250365257263 +v 0.8229990005493164 0.3892495334148407 0.4112870991230011 +v 0.6745677590370178 -0.6113925874233246 0.4112870991230011 +v 0.3280476927757263 -0.9168318808078766 -0.22252093255519867 +v -0.9824366867542267 -0.04826401919126505 -0.1736481785774231 +v -0.5538528859615326 -0.7467842102050781 -0.36534103751182556 +v 0.1300565096821753 -0.19464331098190568 -0.9719672853944132 +v -0.7999967932701111 0.5933180749416351 0.07473009079694748 +v 0.40500181913375854 0.44685065746307373 -0.7971324920654297 +v 0.0064875245268003695 -0.06586894271794128 0.9975322862681385 +v 0.12310893461108208 0.8299330174922943 -0.542546272277832 +v 0.5859426558017731 -0.790052205324173 -0.1736481785774231 +v 0.42592771351337433 -0.5742970407009125 0.6982368230819702 +v -0.8120850324630737 -0.48674504458904266 -0.31848666071891785 +v -0.5800643265247345 0.7821261882781982 -0.22252093255519867 +v -0.17916974762543655 0.03563907983706453 0.9828996160282455 +v -0.8891833126544952 -0.4205523878335953 0.1736481785774231 +v 0.8380034267902374 0.041168469935655594 0.542546272277832 +v -0.7819346785545349 0.3698275536298752 -0.5 +v -0.045620791614055606 0.9286325573921204 -0.36534103751182556 +v -0.854299932718277 0.5120476931333542 0.07473009079694748 +v -0.3698275536298752 -0.7819346785545349 -0.5 +v 0.11339499982890416 -0.07576811459939943 -0.990417718756678 +v 0.7821261882781982 -0.5800643265247345 0.22252093255519867 +v 0.026606214695112783 0.08770893618089029 -0.995623708285796 +v -0.39752040803432465 0.8404862880706787 -0.36534103751182556 +v -0.789968729019165 0.2826553285121918 -0.542546272277832 +v 0.047176837921142495 -0.9603066146373749 0.27084046602249146 +v -0.15169441023252322 0.04601599618124201 -0.9871595922647063 +v 0.047176837921142606 0.9603066146373749 -0.27084046602249146 +v 0.8299330174922943 0.12310893461108208 -0.542546272277832 +v -0.1589607670903206 -0.11789334565401077 -0.9801725149154663 +v -0.4974377006292343 0.04899333603680134 -0.8657562136650085 +v 0.6423341333866119 0.3850002735853195 -0.6616858243942261 +v -0.6889011561870575 0.6243835985660553 -0.36534103751182556 +v 0.13358458504080772 -0.9005541205406189 0.4112870991230011 +v 0.4974377006292343 -0.04899333603680134 -0.8657562136650085 +v 0.6456802487373352 -0.7123983800411224 0.27084046602249146 +v 0.7808828949928284 0.4680432081222534 0.4112870991230011 +v -0.9603066146373749 -0.047176837921142516 0.27084046602249146 +v 0.2630738243460655 0.73524209856987 -0.6234897971153259 +v 0.5947968363761902 -0.8019906580448151 0.024930691346526146 +v 0.41691675782203674 -0.6955835819244385 -0.583743691444397 +v -0.6739024221897125 0.4998001158237457 -0.542546272277832 +v 0.3221018761396408 0.17216704040765762 -0.9305844008922577 +v 0.5947968363761902 -0.8019906580448151 -0.024930691346526146 +v -0.7479760050773621 -0.03674570471048351 0.6616858243942261 +v 0.8914403021335602 -0.3189624398946762 -0.31848666071891785 +v 0.11791479587554932 -0.14367958344519138 -0.982337087392807 +v 0.20386379212141037 -0.8138698935508728 0.542546272277832 +v -0.02248384201784207 0.07411929387792143 0.9966879474149757 +v -0.6697914600372314 0.4014575481414795 0.6234897971153259 +v 0.8571891486644745 -0.3067071884870529 0.4112870991230011 +v 0.8791800141334534 -0.13041403517127037 -0.4562106430530548 +v -0.5094984471797943 0.8500467836856842 -0.12434370815753937 +v 0.12691925838589668 -0.8556201159954071 -0.5 +v 0.5827008783817291 -0.3492574691772461 0.7330518960952759 +v 0.36758580803871155 0.6132798492908478 -0.6982368230819702 +v -0.48674504458904266 -0.8120850324630737 -0.31848666071891785 +v 0.5827008783817291 0.3492574691772461 -0.7330518960952759 +v -0.18190082056936197 2.2276425455775643e-17 -0.983036988167373 +v -0.6745677590370178 -0.6113925874233246 -0.4112870991230011 +v 0.16250290369343914 -0.08685961176383326 -0.9826209850873889 +v 0.5803731679916382 0.27449607849121094 -0.7660444378852844 +v -0.9003756940364838 -0.42584599554538727 0.07473009079694748 +v -0.12691925838589668 0.8556201159954071 0.5 +v 0.048871532082557706 0.9948029220104218 -0.07473009079694748 +v 0.07637635258446487 0.051033046730011314 -0.9956086105881243 +v 0.26472966372966766 -0.17688670754432678 0.9476327002048492 +v 0.46517546474933624 0.6272165179252625 -0.6234897971153259 +v -0.9093112945556641 0.04467159882187849 0.4112870991230011 +v 0.4680432081222534 0.7808828949928284 0.4112870991230011 +v 0.22513406723737717 -0.22513406723737717 -0.9476327002048492 +v 0.8754029273986816 0.3132241666316986 0.36534103751182556 +v 0.3587247431278229 -0.758459746837616 -0.542546272277832 +v 0.2159607708454132 -0.8621636927127838 -0.4562106430530548 +v -0.8229990005493164 -0.3892495334148407 0.4112870991230011 +v -0.32018662989139557 0.6769777238368988 -0.6616858243942261 +v 0.9541435837745667 0.23900053650140762 0.1736481785774231 +v -0.4680432081222534 0.7808828949928284 -0.4112870991230011 +v 0.8791800141334534 -0.13041403517127037 0.4562106430530548 +v 0.7015218138694763 -0.6358223259449005 -0.31848666071891785 +v -0.6655444800853729 -0.7343151867389679 0.12434370815753937 +v -0.06531454359095609 0.019812950378166765 0.9973774695943782 +v 0.03979191929101937 -0.8099831342697144 -0.583743691444397 +v -0.9261229932308197 0.33137211203575134 0.1736481785774231 +v 0.2826553285121918 0.789968729019165 0.542546272277832 +v -0.029591590166091974 -0.6023506820201874 0.7971324920654297 +v 0.04339654183670988 -0.08118922038040743 0.995593440207754 +v -0.17443392065866753 0.11655302299682603 -0.9775148992273276 +v 0.8754029273986816 -0.3132241666316986 -0.36534103751182556 +v 0.41691675782203674 0.6955835819244385 0.583743691444397 +v -0.14432725310325623 -0.9729752838611603 0.1736481785774231 +v 0.790052205324173 -0.5859426558017731 -0.1736481785774231 +v 0.14107617735862732 0.9510583281517029 0.27084046602249146 +v -0.10565016604959965 0.08670485392212868 -0.9903807938098907 +v -0.40480297803878784 -0.8558839857578278 -0.31848666071891785 +v -0.2630738243460655 0.73524209856987 -0.6234897971153259 +v -0.5056825131177902 0.8436802923679352 0.1736481785774231 +v -0.04269755434160156 -0.035040978972085704 0.9983530513194576 +v 0.23900053650140762 -0.9541435837745667 -0.1736481785774231 +v -0.2408040538430214 -0.9613436758518219 0.12434370815753937 +v -0.14541635662317276 0.9803174734115601 -0.12434370815753937 +v 0.02024731806292626 -0.04888135217144916 0.9985274096363862 +v -0.8352162837982178 -0.5006093829870224 -0.22252093255519867 +v 0.5055383294820786 -0.616000235080719 -0.6036167442798615 +v 0.46517546474933624 -0.6272165179252625 -0.6234897971153259 +v 0.48308829963207245 -0.6513691544532776 -0.583743691444397 +v 0.9725806415081024 0.04777982458472252 0.22252093255519867 +v 0.7724377810955048 -0.11458031833171844 -0.6234897971153259 +v 0.9948029220104218 0.04887153208255768 -0.07473009079694748 +v -0.03741216686708213 -0.03741216686708213 0.9985274096047504 +v 0.14432725310325623 0.9729752838611603 0.1736481785774231 +v 0.502915769815445 -0.5548820495605469 -0.6616858243942261 +v 0.502915769815445 -0.5548820495605469 0.6616858243942261 +v -0.3036755472421646 -0.20290950685739517 -0.9305844008922577 +v -0.05125851717823329 0.07671379293970207 -0.9955781970650703 +v 0.13375852020768114 -0.026606224369446704 0.9904177184838026 +v -0.1751891530432494 0.05314304649161532 -0.9828304473100249 +v 0.5244134813547134 0.5786011219024658 0.6234897971153259 +v -0.043746590130981707 -0.029230535407403502 0.9985495333829811 +v 0.20929744094610214 -0.1717659756541252 -0.9623250365257263 +v 0.1307885404676199 0.03967427043244243 0.9903807938098907 +v 0.04681917408239493 -0.04681917408239493 0.9975304383775065 +v 0.22591141611337662 0.9018889665603638 -0.36534103751182556 +v -0.08021319285035133 -0.40325896441936493 0.911222517490387 +v -0.4757005125284195 -0.43114981055259705 -0.7660444378852844 +v 0.9972822964191437 -0.04899333789944649 -0.024930691346526146 +v -0.7419200241565704 0.4446897357702255 -0.5 +v -0.9603066146373749 0.047176837921142634 -0.27084046602249146 +v 6.090912676798697e-17 0.9947215020656586 0.09953689947724342 +v -0.0486282259225845 0.989850252866745 0.12434370815753937 +v 0.4843960851430893 -0.35925260186195374 0.7971324920654297 +v -0.40500181913375854 0.44685065746307373 -0.7971324920654297 +v 0.17216704040765762 0.3221018761396408 -0.9305844008922577 +v -0.43114981055259705 0.4757005125284195 -0.7660444378852844 +v 0.4205523878335953 -0.8891833126544952 -0.1736481785774231 +v -0.9501613080501556 0.18899884074926376 -0.24668069928884506 +v -0.2533782869577408 0.37920738756656647 0.8895952105522156 +v 0.1036139465868473 0.25014618784189224 -0.9623250365257263 +v 0.6705415546894073 -0.7398285865783691 0.024930691346526146 +v 0.5786011219024658 -0.5244134813547134 0.6234897971153259 +v 0.29140374064445496 0.8144188821315765 -0.5 +v 0.062114352360367775 0.312269926071167 -0.9476327002048492 +v 0.8246757984161377 -0.49429164826869965 0.27084046602249146 +v 0.8299330174922943 -0.12310893461108208 -0.542546272277832 +v 0.7123983800411224 0.6456802487373352 -0.27084046602249146 +v 0.6688744723796844 -0.7379892468452454 -0.07473009079694748 +v 0.10491295531392097 -0.707265704870224 0.6982368230819702 +v -0.41107913851737976 -0.8691538274288177 0.27084046602249146 +v 0.10105867867162001 -0.15124500043055958 -0.9830369885845971 +v 0.8958931565284729 0.4237259179353714 0.12434370815753937 +v 0.3850002735853195 -0.6423341333866119 -0.6616858243942261 +v -0.9685616791248322 -0.2426120713353157 0.024930691346526146 +v -0.8380034267902374 -0.04116846993565554 0.542546272277832 +v -0.17688670754432678 0.26472966372966766 -0.9476327002048492 +v 0.10565016604959965 0.08670485392212868 0.9903807938098907 +v -0.9026197791099548 0.42690734565258026 -0.024930691346526146 +v -0.9445713758468628 0.23660282045602798 -0.22252093255519867 +v 0.4998001158237457 -0.6739024221897125 0.542546272277832 +v 0.786656528711319 -0.19704720377922058 -0.583743691444397 +v -0.00902340758823859 -0.0916161972249078 -0.9955934402458115 +v 0.6358223259449005 -0.7015218138694763 0.31848666071891785 +v 0.13358458504080772 -0.9005541205406189 -0.4112870991230011 +v -0.1819625273346901 -0.726435124874115 -0.6616858243942261 +v 0.6409095823764801 0.580886572599411 0.5 +v 0.0090633779384104 0.09202202302746766 -0.9955628803372653 +v -0.15359795028135428 -0.046593428579848044 0.9868088456800419 +v 0.8138698935508728 -0.20386379212141037 -0.542546272277832 +v 0.580886572599411 0.6409095823764801 0.5 +v -0.9972822964191437 0.04899333789944655 0.024930691346526146 +v -0.7574874460697174 0.18974072486162186 0.6234897971153259 +v 0.14085002636265911 -0.07528590594160386 0.9869501300420703 +v 0.5423293709754944 0.7312464714050293 0.4112870991230011 +v -0.9052614271640778 -0.3239077031612396 0.27084046602249146 +v 2.7926162591813887e-17 0.45606885850429535 0.8895952105522156 +v 0.9972822964191437 0.04899333789944649 0.024930691346526146 +v 4.177474085728189e-18 0.0682233277337797 -0.9973797319649838 +v 0.8390597999095917 -0.21017354726791382 -0.5 +v -0.9725806415081024 0.04777982458472258 -0.22252093255519867 +v -0.0708507319521848 0.05814569323412188 -0.995623708247192 +v -0.4779882878065109 -0.7974753081798553 -0.36534103751182556 +v -0.9018889665603638 0.22591141611337662 0.36534103751182556 +v 0.7015218138694763 0.6358223259449005 -0.31848666071891785 +v 0.12763362750411034 0.23878572136163712 -0.9623250365257263 +v 0.1364230215549469 0.9196893274784088 -0.36534103751182556 +v 0.2732041925191879 0.763554573059082 -0.583743691444397 +v 0.8246757984161377 -0.49429164826869965 -0.27084046602249146 +v -0.9005541205406189 0.13358458504080772 -0.4112870991230011 +v 0.08670485392212868 -0.10565016604959965 0.9903807938098907 +v -0.7051016688346863 -0.25228941440582275 0.6616858243942261 +v 0.3067071884870529 0.8571891486644745 0.4112870991230011 +v 0.04862822592258456 0.989850252866745 0.12434370815753937 +v -0.025338353652730163 0.061172198922623114 -0.9975304385878379 +v 0.47832144796848297 -0.14509722590446472 0.8657562136650085 +v 0.15042413771152496 -0.22512561827898026 -0.9623250365257263 +v -0.7821261882781982 0.5800643265247345 0.22252093255519867 +v 0.8914403021335602 0.3189624398946762 0.31848666071891785 +v 0.12599777517998162 -0.052189986010951636 0.9904177187756942 +v 0.20386379212141037 -0.8138698935508728 -0.542546272277832 +v -0.04824117734089382 -0.04824117734089382 -0.9973797319624562 +v -0.49429164826869965 -0.8246757984161377 0.27084046602249146 +v -0.758459746837616 0.3587247431278229 0.542546272277832 +v 0.29415184259414673 -0.1218416802585125 0.9476327002048492 +v -0.0417437710566068 -0.0417437710566068 0.9980683385478327 +v 0.13375852020768114 -0.026606224369446704 -0.9904177184838026 +v -0.37784695625305176 0.41688986122608185 -0.826238751411438 +v 0.11899254471063614 0.8021825551986694 -0.583743691444397 +v 0.41633328795433044 -0.8802627921104431 -0.22252093255519867 +v -0.1500869020819664 0.28079284727573395 -0.9476327002048492 +v -0.9196893274784088 -0.1364230215549469 -0.36534103751182556 +v 0.5094984471797943 0.8500467836856842 0.12434370815753937 +v -0.8914403021335602 -0.3189624398946762 0.31848666071891785 +v -0.13375852020768114 0.026606224369446704 0.9904177184838026 +v -0.8401444256305695 0.25485503673553467 -0.4781053215265274 +v -0.8621636927127838 0.2159607708454132 -0.4562106430530548 +v 0.5423293709754944 0.7312464714050293 -0.4112870991230011 +v -0.4446897357702255 -0.7419200241565704 0.5 +v 0.0942029245197773 -0.6350645124912262 0.7660444378852844 +v 0.5619637668132782 0.02760750986635685 0.826238751411438 +v 0.34408073127269745 -0.4192635715007782 -0.839764803647995 +v -0.0942029245197773 -0.6350645124912262 0.7660444378852844 +v 0.4461061358451843 -0.6015045046806335 0.6616858243942261 +v -0.7343151867389679 0.6655444800853729 0.12434370815753937 +v 0.8404862880706787 -0.39752040803432465 -0.36534103751182556 +v 0.2826553285121918 -0.789968729019165 -0.542546272277832 +v 0.9018889665603638 0.22591141611337662 0.36534103751182556 +v -0.8246757984161377 0.49429164826869965 0.27084046602249146 +v -0.7960140109062195 -0.5903642177581787 0.12434370815753937 +v 0.0792477046969308 0 -0.9965535605685545 +v -0.04826401919126507 0.9824366867542267 0.1736481785774231 +v 6.052960637678029e-17 0.9885234534740448 -0.14899594336748123 +v -0.0486282259225845 0.989850252866745 -0.12434370815753937 +v -0.02533835365273016 0.06117219892262311 0.9975304385878379 +v -0.3338726758956909 0.9331116080284119 0.12434370815753937 +v 0.8891833126544952 -0.4205523878335953 0.1736481785774231 +v 0.13375852020768114 0.026606224369446704 0.9904177184838026 +v 0.5903642177581787 0.7960140109062195 -0.12434370815753937 +v -0.8390597999095917 0.21017354726791382 -0.5 +v 0.3132241666316986 -0.8754029273986816 -0.36534103751182556 +v 0.8621636927127838 -0.2159607708454132 0.4562106430530548 +v -0.4237259179353714 0.8958931565284729 -0.12434370815753937 +v 0.4998001158237457 -0.6739024221897125 -0.542546272277832 +v -0.8571891486644745 0.3067071884870529 0.4112870991230011 +v 0.20290950685739517 -0.3036755472421646 -0.9305844008922577 +v -0.8144188821315765 0.29140374064445496 -0.5 +v -0.3168545365333557 -0.031207446940243244 -0.9476327002048492 +v 0.3800109326839447 -0.8034656345844269 -0.4562106430530548 +v 0.7343151867389679 0.6655444800853729 0.12434370815753937 +v 0.4237259179353714 0.8958931565284729 0.12434370815753937 +v 0.030698228860273957 -0.10119849909096956 -0.9942557662725449 +v -0.6539329290390015 -0.7215037941932678 0.22252093255519867 +v 0.758459746837616 0.3587247431278229 -0.542546272277832 +v 0.6705415546894073 -0.7398285865783691 -0.024930691346526146 +v 0.6015045046806335 -0.4461061358451843 -0.6616858243942261 +v 0.989850252866745 0.048628225922584534 0.12434370815753937 +v 0.6539329290390015 -0.7215037941932678 -0.22252093255519867 +v -0.0644275164231658 -0.12053540907800198 0.9903807938098907 +v 0.8021825551986694 0.11899254471063614 -0.583743691444397 +v 0.07671379320351962 -0.05125851735451055 0.9955781970412925 +v 0.7288154661655426 0.6605598330497742 -0.1736481785774231 +v 0.45693448185920715 0.7623491287231445 0.4562106430530548 +v -0.40480297803878784 0.8558839857578278 -0.31848666071891785 +v -0.9427979282466007 0.21839511043688525 -0.2466806968989731 +v -0.6141282320022583 0.2904610186815262 0.7330518960952759 +v 0.9632141590118408 0.14287932589650154 -0.22252093255519867 +v -0.9632141590118408 0.14287932589650154 0.22252093255519867 +v -0.8754029273986816 -0.3132241666316986 0.36534103751182556 +v 0.5933180749416351 0.7999967932701111 -0.07473009079694748 +v -0.989850252866745 0.048628225922584596 0.12434370815753937 +v -0.8754029273986816 0.3132241666316986 0.36534103751182556 +v -1.4557567133025928e-17 -0.07924770469693082 -0.9965535605685545 +v 0.11339499982890416 0.07576811459939943 0.990417718756678 +v -0.9824366867542267 0.04826401919126517 0.1736481785774231 +v -0.026538756676018238 0.2694525271654129 0.9623250365257263 +v -0.5423293709754944 0.7312464714050293 0.4112870991230011 +v -0.7960140109062195 -0.5903642177581787 -0.12434370815753937 +v -0.06303013661526453 0.026107936669129916 0.9973797321208978 +v 0.5056825131177902 0.8436802923679352 0.1736481785774231 +v 0.11564320366024816 0.14091164085492972 0.9829684633727145 +v 0.4237259179353714 -0.8958931565284729 0.12434370815753937 +v -0.029591590166091898 0.6023506820201874 -0.7971324920654297 +v -0.064952745490856 0.064952745490856 -0.9956086104754314 +v 0.9876779317855835 0.14650818333029747 -0.024930691346526146 +v -0.08083301773287552 -0.043206147565862676 -0.9956237081869348 +v 0.29942767322063446 0.8368442952632904 0.4562106430530548 +v 0.046613410132643474 0.046613410132643474 0.997552245604057 +v -0.3189624398946762 0.8914403021335602 0.31848666071891785 +v -0.49429164826869965 -0.8246757984161377 -0.27084046602249146 +v 0.03150210157036776 -0.6412400305271149 0.7660444378852844 +v 0.03790284900214302 0.056725623983152465 0.9973797321450628 +v 0.18974072486162186 -0.7574874460697174 0.6234897971153259 +v -0.7960140109062195 0.5903642177581787 0.12434370815753937 +v -0.08670485392212868 -0.10565016604959965 -0.9903807938098907 +v -0.10565016604959965 -0.08670485392212868 0.9903807938098907 +v 0.14085002636265911 -0.07528590594160388 -0.9869501300420703 +v -0.14107617735862732 0.9510583281517029 -0.27084046602249146 +v 0.12159902229905128 -0.6113195419311523 0.7815884649753571 +v -0.17688670754432678 -0.26472966372966766 0.9476327002048492 +v 0.06019422753060055 -0.032174485002550296 -0.9973774696384261 +v 0.06275608136579658 -0.02599441926411625 -0.9974019620904804 +v 0.03508361056447022 -0.714143306016922 0.6982368230819702 +v 0.3467300832271576 -0.7330991625785828 -0.583743691444397 +v 0.34975267946720123 -0.3858926147222519 -0.853290855884552 +v -0.5423293709754944 0.7312464714050293 -0.4112870991230011 +v 0.3363794684410095 0.9401176571846008 0.024930691346526146 +v 0.38244690001010895 0.5156699419021606 -0.7660444378852844 +v -0.2336168736219406 0.9326508343219757 -0.27084046602249146 +v 0.10491295531392097 -0.707265704870224 -0.6982368230819702 +v 0.03217448510975933 -0.060194227731174554 -0.9973774696213824 +v 0.07576811459939943 -0.11339499982890416 -0.990417718756678 +v 0.3892495334148407 -0.8229990005493164 -0.4112870991230011 +v 0.29140374064445496 0.8144188821315765 0.5 +v 0.9052614271640778 -0.3239077031612396 -0.27084046602249146 +v 0.15599651634693146 -0.6227729916572571 0.7660444378852844 +v 0.08670485392212868 0.10565016604959965 0.9903807938098907 +v -0.11458031833171844 0.7724377810955048 -0.6234897971153259 +v 0.9401176571846008 0.3363794684410095 0.024930691346526146 +v 0.05474785802583931 0.05474785802583931 -0.9966901424597296 +v 0.015615143337598385 -0.1585432120099564 -0.9870202783184183 +v -0.23878572136163712 0.12763362750411034 0.9623250365257263 +v -0.32163913547992706 -0.11508432030677795 0.9396926164627075 +v -0.08897461742162704 0.4473056346178055 -0.8895952105522156 +v 0.989850252866745 -0.048628225922584534 -0.12434370815753937 +v 0.28079284727573395 0.1500869020819664 0.9476327002048492 +v -0.4205523878335953 -0.8891833126544952 0.1736481785774231 +v -0.18974072486162186 -0.7574874460697174 -0.6234897971153259 +v -0.9331116080284119 0.3338726758956909 0.12434370815753937 +v 0.48259373009204865 0.2892555445432663 0.826238751411438 +v 0.24388160970210437 0.08726223331879307 0.9655383734590114 +v 0.22267372906208038 0.47080405056476593 0.853290855884552 +v 0.9948029220104218 0.04887153208255768 0.07473009079694748 +v -0.11589090302429161 -0.14121346334520696 0.9828996167603508 +v 0.49429164826869965 -0.8246757984161377 -0.27084046602249146 +v -0.2159607708454132 -0.8621636927127838 0.4562106430530548 +v -0.3046779930591583 -0.09242305532097816 0.9476327002048492 +v -0.055053525017811446 0.0367855881371171 0.9975304385429239 +v -0.13085814641885124 0.13085814641885124 -0.9824797007288648 +v 0.3363794684410095 -0.9401176571846008 0.024930691346526146 +v 0.35925260186195374 -0.4843960851430893 -0.7971324920654297 +v -0.2031705528497696 -0.5678236782550812 0.7971324920654297 +v 0.1805665769942229 -0.14818716453780922 -0.972082945279266 +v 0.026107936508923353 -0.06303013622849168 -0.9973797321522708 +v 0.22512561827898026 -0.15042413771152496 -0.9623250365257263 +v 0.3338726758956909 -0.9331116080284119 0.12434370815753937 +v -0.07859639823436737 -0.2590976133942604 0.9623250365257263 +v 0.14653578028082848 -0.5850036442279816 0.7971324920654297 +v -0.3116179406642914 -0.23111161589622498 -0.9214761853218079 +v 0.17373193055391312 -0.6935767531394958 0.6982368230819702 +v -0.5903642177581787 0.7960140109062195 -0.12434370815753937 +v -0.11899254471063614 -0.8021825551986694 -0.583743691444397 +v -0.22331959754228592 -0.5391412079334259 0.8116856217384338 +v 0.05814569383562698 -0.07085073268512093 -0.9956237081761368 +v 0.9326508343219757 0.2336168736219406 -0.27084046602249146 +v 0.0644275164231658 -0.12053540907800198 0.9903807938098907 +v 0.3702079653739929 -0.4510997086763382 -0.8116856217384338 +v -0.23900053650140762 -0.9541435837745667 0.1736481785774231 +v -0.6456802487373352 -0.7123983800411224 -0.27084046602249146 +v -0.3418664336204529 -0.22842784970998764 -0.911222517490387 +v -0.312269926071167 0.062114352360367775 -0.9476327002048492 +v 0.7604652643203735 -0.5639995038509369 -0.31848666071891785 +v -0.3178304135799408 -0.2608366832137108 -0.911222517490387 +v 0.07227783108634725 0.029938457269463502 -0.9966296061255013 +v 0.485822394490242 -0.5919763147830963 -0.642587810754776 +v 0.007591864227906122 -0.0770814926098874 -0.9966879473988326 +v 0.036025339919743354 0.18111160799852488 0.9825505075429508 +v -0.9641111791133881 0.0949566625058651 -0.24668069928884506 +v -0.442830353975296 -0.8284773230552673 -0.3419138491153717 +v -0.40500181913375854 0.44685065746307373 0.7971324920654297 +v 0.9852223992347717 0.14614393562078476 0.07473009079694748 +v -0.6720004677772522 -0.09968185424804688 0.7330518960952759 +v -0.9852223992347717 -0.14614393562078476 -0.07473009079694748 +v 0.052189986010951636 -0.12599777517998162 0.9904177187756942 +v 0.5800643265247345 0.7821261882781982 -0.22252093255519867 +v 0.2336168736219406 -0.9326508343219757 0.27084046602249146 +v 0.3363794684410095 0.9401176571846008 -0.024930691346526146 +v 0.4461061358451843 -0.6015045046806335 -0.6616858243942261 +v -0.015460460616359856 0.07772498197595014 0.9965535605867635 +v 0.9365380108356476 -0.1389222927391529 -0.31848666071891785 +v 0.9948029220104218 -0.04887153208255768 -0.07473009079694748 +v -0.3495008945465088 -0.10601993650197983 0.9305844008922577 +v -0.3338726758956909 0.9331116080284119 -0.12434370815753937 +v -0.7215037941932678 0.6539329290390015 0.22252093255519867 +v -0.03579855151474476 -0.36346879601478577 0.9305844008922577 +v -0.5565517544746399 0.08255665563046932 -0.826238751411438 +v -0.016033995746228737 -0.05285699893218803 -0.9983530513046097 +v -0.1569779600733377 0.015460979437112471 -0.9872976083928391 +v -1.6873867594601793e-17 -0.0918570565205225 -0.9956086104966381 +v 0.03123549390178662 0.15703142836681483 0.9868796524976322 +v -0.8352162837982178 0.5006093829870224 0.22252093255519867 +v -0.5639995038509369 -0.7604652643203735 -0.31848666071891785 +v -0.6113925874233246 0.6745677590370178 -0.4112870991230011 +v -0.7015218138694763 0.6358223259449005 0.31848666071891785 +v -0.48674504458904266 0.8120850324630737 -0.31848666071891785 +v 0.9196893274784088 -0.1364230215549469 -0.36534103751182556 +v -0.18954821676015854 -0.5297517776489258 0.826238751411438 +v 0.8229990005493164 -0.3892495334148407 -0.4112870991230011 +v -0.3338726758956909 -0.9331116080284119 0.12434370815753937 +v -0.5006093829870224 -0.8352162837982178 0.22252093255519867 +v -0.2776987850666046 0.4156055897474289 -0.8657562136650085 +v 0.37784695625305176 -0.41688986122608185 -0.826238751411438 +v 0.2694525271654129 0.026538756676018238 0.9623250365257263 +v 0.5947968363761902 0.8019906580448151 -0.024930691346526146 +v -0.5859426558017731 0.790052205324173 -0.1736481785774231 +v 0.2694525271654129 -0.026538756676018238 0.9623250365257263 +v -0.11011299422173612 0.2060069216860026 -0.9720829444735729 +v 0.42584599554538727 -0.9003756940364838 0.07473009079694748 +v 0.9972822964191437 0.04899333789944649 -0.024930691346526146 +v 0.8621636927127838 -0.2159607708454132 -0.4562106430530548 +v -0.33137211203575134 -0.9261229932308197 0.1736481785774231 +v -0.037738047585162145 -0.05647898124514333 0.997401962072462 +v -0.2655537724494934 0.0528219323605299 -0.9623250365257263 +v -0.015539635546049181 -0.07812302124262463 0.9965231469705301 +v -0.9803174734115601 0.14541635662317276 -0.12434370815753937 +v 0.3582097291946411 -0.07125234603881836 -0.9305844008922577 +v 0.8034656345844269 0.3800109326839447 0.4562106430530548 +v -0.4461061358451843 0.6015045046806335 0.6616858243942261 +v 0.8500467836856842 -0.5094984471797943 -0.12434370815753937 +v -0.34975267946720123 0.3858926147222519 -0.853290855884552 +v -0.5548820495605469 -0.502915769815445 -0.6616858243942261 +v 0.24056024849414825 0.5086219012737274 0.826238751411438 +v 0.5727438628673553 -0.7722557187080383 0.27084046602249146 +v 0.45097053050994873 0.3013288676738739 0.839764803647995 +v -0.3652879446744919 -0.13070211559534073 0.9214761853218079 +v 0.7799491286277771 0.03831644356250763 -0.6234897971153259 +v -0.7821261882781982 -0.5800643265247345 -0.22252093255519867 +v -0.3850002735853195 -0.6423341333866119 0.6616858243942261 +v -0.14868342876434326 0.007304348517209301 -0.9888308048248291 +v -0.136379007703739 1.6701611938839738e-17 -0.9904177184601843 +v 5.6494730055593e-18 0.09226289328928153 -0.9955781969203346 +v 0.21139593422412872 0.126705851405859 0.9690772891044617 +v 0.22512561827898026 0.15042413771152496 0.9623250365257263 +v 0.23878572136163712 0.12763362750411034 0.9623250365257263 +v 0.032174485002550296 0.06019422753060055 -0.9973774696384261 +v -0.40480297803878784 -0.8558839857578278 0.31848666071891785 +v -0.7330991625785828 0.3467300832271576 0.583743691444397 +v -0.11011299422173612 -0.2060069216860026 0.9720829444735729 +v -0.1513519138097763 -0.25251540541648865 0.955572783946991 +v -0.1500869020819664 -0.28079284727573395 0.9476327002048492 +v 0.22460155098629986 0.13462099439292433 0.9647402847563162 +v 0.08770893618089029 -0.026606214695112783 -0.995623708285796 +v 0.1389222927391529 -0.9365380108356476 0.31848666071891785 +v -0.12763362750411034 -0.23878572136163712 0.9623250365257263 +v -0.646356463432312 -0.3057038336992264 -0.6982368230819702 +v -0.39752040803432465 -0.8404862880706787 -0.36534103751182556 +v -0.026538756676018238 -0.2694525271654129 0.9623250365257263 +v -0.6350645124912262 -0.0942029245197773 0.7660444378852844 +v 0.136379007703739 0 0.9904177184601843 +v 0.07182931527495384 -0.7292952835559845 -0.6799613237380981 +v 0.03674570471048348 -0.7479760050773621 -0.6616858243942261 +v 0.758459746837616 -0.3587247431278229 0.542546272277832 +v -0.23169785737991333 -0.2823246419429779 0.9305844008922577 +v -0.8299330174922943 0.12310893461108208 -0.542546272277832 +v 0.7974753081798553 -0.4779882878065109 -0.36534103751182556 +v -0.8436802923679352 0.5056825131177902 0.1736481785774231 +v -0.11458031833171844 -0.7724377810955048 -0.6234897971153259 +v 0.3280476927757263 -0.9168318808078766 0.22252093255519867 +v -0.2590976133942604 0.07859639823436737 -0.9623250365257263 +v -0.0530291731779808 0.17481376293088355 0.9828996167503579 +v 0.3189624398946762 0.8914403021335602 0.31848666071891785 +v -0.8831241130828857 0.22121108323335648 0.4112870991230011 +v -0.3534435033798218 0.3534435033798218 -0.8657562136650085 +v -0.11589090302429161 -0.14121346334520696 -0.9828996167603508 +v -0.1717659756541252 -0.20929744094610214 0.9623250365257263 +v -0.5933180749416351 -0.7999967932701111 -0.07473009079694748 +v 0.7930475175380707 0.07810836285352707 -0.6036167442798615 +v -0.5965497195720673 0.08848978951573372 -0.7971324920654297 +v -0.04244266822934158 -0.8639403283596039 -0.5 +v -0.1573488776248353 1.926968017742704e-17 -0.9873661331220543 +v -0.1389222927391529 -0.9365380108356476 0.31848666071891785 +v -0.9168318808078766 -0.3280476927757263 0.22252093255519867 +v -0.7123983800411224 0.6456802487373352 0.27084046602249146 +v 0.04566977242129508 -0.22959743757624573 -0.971967285501265 +v -0.048871532082557644 0.9948029220104218 -0.07473009079694748 +v -0.2288672775030136 -0.6396411955356598 0.7330518960952759 +v 0.03967427043244243 -0.1307885404676199 0.9903807938098907 +v -0.40802858769893646 -0.14599496126174927 0.9009688496589661 +v -0.37986163794994354 -0.15734384208917618 0.911222517490387 +v 0.026538756676018238 -0.2694525271654129 -0.9623250365257263 +v 0.026606224369446704 -0.13375852020768114 0.9904177184838026 +v -0.035083610564470354 -0.714143306016922 0.6982368230819702 +v -0.1364230215549469 -0.9196893274784088 0.36534103751182556 +v 0.17688670754432678 -0.26472966372966766 -0.9476327002048492 +v -0.16054144874215126 -0.015811952529475093 -0.9866662323474884 +v -0.42690734565258026 0.9026197791099548 0.024930691346526146 +v -0.9729752838611603 0.14432725310325623 0.1736481785774231 +v -0.1743316269384087 0.008738181892035712 -0.9842959406511436 +v -0.7379892468452454 0.6688744723796844 0.07473009079694748 +v -0.1693991720676422 -0.558434247970581 0.8116856217384338 +v -0.8034656345844269 0.3800109326839447 0.4562106430530548 +v 0.7724377810955048 0.11458031833171844 -0.6234897971153259 +v 0.24200890213251114 -0.9661536514759064 0.07473009079694748 +v 0.8914403021335602 -0.3189624398946762 0.31848666071891785 +v 0.8099831342697144 0.03979191929101944 -0.583743691444397 +v 0.1601380509923473 0.015772221308511802 0.9867377060351147 +v 0.6243835985660553 -0.6889011561870575 0.36534103751182556 +v 0.03508361056447022 -0.714143306016922 -0.6982368230819702 +v 0.49429164826869965 0.8246757984161377 -0.27084046602249146 +v -4.9737122844449824e-17 -0.2707562819123268 -0.9623250365257263 +v -0.9365380108356476 0.1389222927391529 0.31848666071891785 +v -0.4364306926727295 -0.13238979876041412 0.8895952105522156 +v -0.15042413771152496 -0.22512561827898026 -0.9623250365257263 +v 0.7999967932701111 -0.5933180749416351 0.07473009079694748 +v -0.026841834180593563 -0.08848566957642921 -0.9955628803365355 +v 0.989850252866745 0.048628225922584534 -0.12434370815753937 +v -0.5294574946165085 -0.713890790939331 -0.4562106430530548 +v -0.136710736900568 -0.5457798540592194 0.826238751411438 +v -0.7288154661655426 -0.6605598330497742 -0.1736481785774231 +v 0.8691538274288177 -0.41107913851737976 -0.27084046602249146 +v 0.8144188821315765 -0.29140374064445496 0.5 +v -0.050937724895601375 -0.04180351174028876 -0.9975541982476328 +v -3.3848030449748567e-17 -0.18426010311873176 0.9826209854026081 +v -0.09751484170556068 0.490240216255188 -0.8657562136650085 +v -0.5619637668132782 0.027607509866356884 -0.826238751411438 +v 0.07489602709800863 0.022719461450100155 -0.9966271194794554 +v 0.9729752838611603 0.14432725310325623 0.1736481785774231 +v -0.9613436758518219 -0.2408040538430214 0.12434370815753937 +v -0.758459746837616 0.3587247431278229 -0.542546272277832 +v -0.7819346785545349 0.3698275536298752 0.5 +v -6.709119673352248e-17 -0.36522747576236725 -0.9305844008922577 +v -0.070089902728796 0.4725077599287033 -0.8782215714454651 +v -0.3934548944234848 -0.11935323476791382 0.911222517490387 +v -0.6605598330497742 -0.7288154661655426 -0.1736481785774231 +v -0.2461169809103012 0.2019829973578453 0.9476327002048492 +v -0.04989498347200842 -0.029990856702251822 0.9981245187121566 +v 0.14614393562078476 -0.9852223992347717 0.07473009079694748 +v 0.04965151379915045 0.060500543692906174 -0.9966271194788779 +v 0.07227783108634725 0.029938457269463495 0.9966296061255013 +v -0.36346879601478577 0.03579855151474476 -0.9305844008922577 +v 0.9510583281517029 0.14107617735862732 0.27084046602249146 +v 0.7398285865783691 -0.6705415546894073 0.024930691346526146 +v -0.9456450939178467 0.04645656421780592 0.31848666071891785 +v -0.7407726049423218 0.10988323390483856 -0.6616858243942261 +v -0.04899333603680134 0.4974377006292343 -0.8657562136650085 +v 0.7479760050773621 -0.03674570471048355 -0.6616858243942261 +v -0.8144188821315765 0.29140374064445496 0.5 +v 0.9632141590118408 0.14287932589650154 0.22252093255519867 +v 0.051814573413021536 -0.027695466818034167 0.9980895227299207 +v 0.4779882878065109 0.7974753081798553 0.36534103751182556 +v -0.5968815386295319 0.6585572957992554 -0.4562106430530548 +v -0.3892495334148407 -0.8229990005493164 0.4112870991230011 +v 0.47080405056476593 0.22267372906208038 0.853290855884552 +v -0.1036139465868473 0.25014618784189224 0.9623250365257263 +v -0.21017354726791382 -0.8390597999095917 0.5 +v -0.5807522237300873 0.05719910003244877 -0.8116856217384338 +v -0.5800643265247345 -0.7821261882781982 -0.22252093255519867 +v 0.46179617941379547 0.1912822425365448 0.8657562136650085 +v -0.13601554557681084 0.01339636160992086 -0.9903807938098907 +v -0.5619637668132782 -0.027607509866356815 -0.826238751411438 +v 0.40500181913375854 -0.44685065746307373 -0.7971324920654297 +v -0.12053540907800198 0.0644275164231658 0.9903807938098907 +v 0.5423293709754944 -0.7312464714050293 0.4112870991230011 +v 0.46489863097667694 -0.5664806365966797 0.6799613237380981 +v 6.090912676798697e-17 0.9947215020656586 -0.09953689947724342 +v -0.4022168219089508 -0.21498937159776688 0.8895952105522156 +v -0.3363794684410095 -0.9401176571846008 0.024930691346526146 +v 0.41633328795433044 -0.8802627921104431 0.22252093255519867 +v -0.22512561827898026 -0.15042413771152496 0.9623250365257263 +v 0.048628225922584443 -0.989850252866745 0.12434370815753937 +v 0.9018889665603638 -0.22591141611337662 0.36534103751182556 +v 0.4318156987428665 0.20423362404108047 0.8782215714454651 +v -0.5835622251033783 7.146576125534387e-17 -0.8116856217384338 +v -0.14650818333029747 0.9876779317855835 -0.024930691346526146 +v -0.026538756676018238 -0.2694525271654129 -0.9623250365257263 +v 0.14107617735862732 0.9510583281517029 -0.27084046602249146 +v -0.2426120713353157 0.9685616791248322 0.024930691346526146 +v 0.8220046162605286 0.08096038922667503 -0.5631449818611145 +v -0.4318156987428665 -0.20423362404108047 0.8782215714454651 +v -0.3221018761396408 0.17216704040765762 0.9305844008922577 +v -0.24200890213251114 0.9661536514759064 -0.07473009079694748 +v -0.12599777517998162 -0.052189986010951636 -0.9904177187756942 +v -0.18546712398529053 0.09913424402475357 -0.9773987084627151 +v -0.47080405056476593 -0.22267372906208038 0.853290855884552 +v -0.46179617941379547 -0.1912822425365448 0.8657562136650085 +v -0.3800109326839447 -0.8034656345844269 0.4562106430530548 +v 0.11378259355076356 -0.11378259355076356 -0.9867377057319695 +v -0.052189986010951636 -0.12599777517998162 0.9904177187756942 +v -0.023438543081283555 0.47710251808166504 -0.8782215714454651 +v -0.03967427043244243 -0.1307885404676199 0.9903807938098907 +v 0.19145360589027405 -0.19145360589027405 -0.9623250365257263 +v -0.6023506820201874 -0.029591590166091884 -0.7971324920654297 +v 0.9803174734115601 -0.14541635662317276 0.12434370815753937 +v -0.23878572136163712 -0.12763362750411034 0.9623250365257263 +v -0.11339499982890416 0.07576811459939943 0.990417718756678 +v 0.9603066146373749 -0.04717683792114258 -0.27084046602249146 +v -0.19464330760343834 0.13005650742475494 -0.9719672863218253 +v 0.19192341919214623 -0.1739492490182921 -0.96553837427754 +v -0.18321986414114413 0.13570914191160943 -0.9732945555925837 +v -0.3626101016998291 0.19381913542747498 0.911222517490387 +v -0.5086219012737274 -0.24056024849414825 0.826238751411438 +v 0.3587247431278229 0.758459746837616 -0.542546272277832 +v 0.6113925874233246 -0.6745677590370178 -0.4112870991230011 +v -0.19795866310596466 0.14681615680456161 -0.9690772891044617 +v -0.3036755472421646 0.20290950685739517 0.9305844008922577 +v -0.6023506820201874 0.029591590166091957 -0.7971324920654297 +v 0.3698275536298752 0.7819346785545349 -0.5 +v -0.3168545365333557 0.031207446940243244 -0.9476327002048492 +v 0.4022168219089508 0.21498937159776688 0.8895952105522156 +v 0.12407546447681853 -0.10182610812853082 -0.986808845538163 +v -0.36522747576236725 4.472746559192247e-17 0.9305844008922577 +v 0.76211878657341 0.07506214827299118 -0.642587810754776 +v -0.5010915845632553 -0.20755892992019653 0.839764803647995 +v -0.017999598031289518 -0.09049008828662417 0.9955781969053012 +v 0.7479760050773621 0.03674570471048355 -0.6616858243942261 +v 0.1331248192747776 -0.08895115885862527 -0.9868796530381567 +v 0.4516065261497471 0.2135939822256169 0.865566522788776 +v -0.9725806415081024 -0.04777982458472246 0.22252093255519867 +v -0.7974753081798553 -0.4779882878065109 -0.36534103751182556 +v -0.2408040538430214 0.9613436758518219 -0.12434370815753937 +v -0.23900053650140762 0.9541435837745667 -0.1736481785774231 +v -0.9803174734115601 -0.14541635662317276 0.12434370815753937 +v -0.22512561827898026 0.15042413771152496 -0.9623250365257263 +v 0.7407726049423218 0.10988323390483856 -0.6616858243942261 +v 0.34975267946720123 0.3858926147222519 0.853290855884552 +v -0.39175380766391754 -0.185285747051239 0.9009688496589661 +v -0.9729752838611603 -0.14432725310325623 0.1736481785774231 +v 0.7634460791154307 0.048044434097605435 -0.6425878111926334 +v 0.1389222927391529 0.9365380108356476 -0.31848666071891785 +v -0.5823280326633674 -0.025122662099826675 -0.8116856215101037 +v 0.580886572599411 -0.6409095823764801 -0.5 +v -0.42135268449783325 -0.1745299994945526 0.8895952105522156 +v -0.6015045046806335 -0.4461061358451843 -0.6616858243942261 +v 0.6216683983802795 -0.5634474158287048 -0.542546272277832 +v -0.36346879601478577 0.03579855151474476 0.9305844008922577 +v -0.7288154661655426 0.6605598330497742 0.1736481785774231 +v -0.765941321849823 -0.511785626411438 -0.38831406831741333 +v -0.7467842102050781 -0.5538528859615326 -0.36534103751182556 +v -0.7808828949928284 -0.4680432081222534 -0.4112870991230011 +v 0.7815728187561035 0.15546450018882751 -0.6036167442798615 +v 0.786656528711319 0.19704720377922058 -0.583743691444397 +v -0.14614393562078476 0.9852223992347717 -0.07473009079694748 +v -0.2151484452188015 0.14375760406255722 -0.965701162815094 +v -0.23878572136163712 0.12763362750411034 -0.9623250365257263 +v -0.49036283791065216 -0.17545463144779205 0.853290855884552 +v 0.47833436727523804 0.2556750476360321 0.839764803647995 +v 0.3534435033798218 0.3534435033798218 0.8657562136650085 +v -0.3418664336204529 0.22842784970998764 0.911222517490387 +v -0.5823280326633674 0.025122662099826817 -0.8116856215101036 +v 0.31709805130958557 0.38638509809970856 0.8657562136650085 +v -0.5190230309963226 -0.15744391828775406 0.839764803647995 +v -0.3626101016998291 -0.19381913542747498 0.911222517490387 +v -0.8001289069652557 -0.5066926404833794 -0.31848666071891785 +v -0.8246757984161377 -0.49429164826869965 -0.27084046602249146 +v -0.8125343024730682 -0.5145485028624535 -0.27084046602249146 +v -0.7942827939987183 -0.5307227969169617 -0.29466356337070465 +v -0.7722557187080383 -0.5727438628673553 -0.27084046602249146 +v -0.7604652643203735 -0.5639995038509369 -0.31848666071891785 +v 0.9632141590118408 -0.14287932589650154 0.22252093255519867 +v -0.6655444800853729 -0.7343151867389679 -0.12434370815753937 +v 0.7312464714050293 -0.5423293709754944 -0.4112870991230011 +v -0.21139593422412872 0.126705851405859 -0.9690772891044617 +v 0.38638509809970856 0.31709805130958557 0.8657562136650085 +v -0.20690277218818665 0.1105918362736702 -0.9718510955572128 +v -0.8238752946522376 -0.5075789395624437 -0.24668070097070718 +v 0.3836744874715805 0.2845523953437805 0.8782215714454651 +v -0.47832144796848297 -0.14509722590446472 0.8657562136650085 +v -0.3418664336204529 -0.22842784970998764 0.911222517490387 +v 0.694760262966156 -0.5152693092823029 -0.5 +v 0.35393598675727844 0.3207888752222061 0.8782215714454651 +v 0.3224893808364868 0.3224893808364868 0.8895952105522156 +v -0.19629244257945017 0.11749492360447052 -0.9731198084158295 +v -0.25014618784189224 0.1036139465868473 -0.9623250365257263 +v -0.5786011219024658 -0.5244134813547134 -0.6234897971153259 +v -1.517304213574266e-16 -0.8259819447994232 -0.5631449818611145 +v 0.04116846993565552 -0.8380034267902374 -0.542546272277832 +v -0.039791919291019516 -0.8099831342697144 -0.583743691444397 +v -0.7215037941932678 -0.6539329290390015 -0.22252093255519867 +v -0.21287323481318393 0.1578775596897726 -0.9638465374067406 +v 0.45693448185920715 -0.7623491287231445 -0.4562106430530548 +v 0.042442668229341424 -0.8639403283596039 -0.5 +v -0.7330991625785828 -0.3467300832271576 -0.583743691444397 +v 0.2732041925191879 0.763554573059082 0.583743691444397 +v 0.33387337625026703 0.7059159576892853 0.6234897971153259 +v -0.8055078685283661 -0.538223147392273 -0.24668069928884506 +v 0.3587247431278229 0.758459746837616 0.542546272277832 +v -0.6423341333866119 -0.3850002735853195 -0.6616858243942261 +v -0.8123309101181292 -0.500659499165743 -0.2945831989566651 +v -0.7810830175876617 -0.5219029784202576 -0.3419138491153717 +v -0.7633387751984231 -0.5458283359715712 -0.34191383436535067 +v -0.7123983800411224 -0.6456802487373352 -0.27084046602249146 +v -0.7988020998961928 -0.4923404732955502 -0.34191385428654725 +v -0.7846278995275497 -0.5242716073989868 -0.3302002549171448 +v -0.7743190228939056 -0.5453198701143265 -0.31848666071891785 +v -0.792206562853808 -0.5081695758108847 -0.3350902312094679 +v -0.791227787733078 -0.5286815166473389 -0.30657511204481125 +v -0.781750758150318 -0.5446728914057707 -0.30036249023382405 +v -0.7740752176346911 -0.5368845740392466 -0.3328477801613669 +v -0.8017935782152699 -0.5147672902499025 -0.3003780404610794 +v -0.7466115057468414 -0.5826791375875473 -0.31848666071891785 +v -0.8424780368804932 -0.45031389594078064 -0.29466356337070465 +v -0.8691538274288177 -0.41107913851737976 -0.27084046602249146 +v -0.8558839857578278 -0.40480297803878784 -0.31848666071891785 +v -0.8240411579608917 -0.46679744869470596 -0.31848666071891785 +v -0.8271583961412649 -0.4770568841417503 -0.2926883038813539 +v -0.8891833126544952 -0.4205523878335953 -0.1736481785774231 +v -0.8284773230552673 -0.442830353975296 -0.3419138491153717 +v -0.8180630952119827 -0.4767712466418743 -0.31848666071891785 +v -0.809832031247255 -0.4786161930791755 -0.33529820086136036 +v -0.8802627921104431 -0.41633328795433044 -0.22252093255519867 +v -0.8243116342344736 -0.4737380208180497 -0.3061519715790836 +v -0.7488953769207001 -0.5003959089517593 -0.43374887108802795 +v -0.713890790939331 -0.5294574946165085 -0.4562106430530548 +v -0.8678929507732391 -0.35949303209781647 -0.3419138491153717 +v -0.8754029273986816 -0.3132241666316986 -0.36534103751182556 +v -0.7445679306983948 -0.5243674516677856 -0.4112870991230011 +v -0.7419200241565704 -0.4446897357702255 -0.5 +v -0.042697554341601565 -0.035040978972085704 -0.9983530513194576 +f 1 437 499 +f 1 1553 1615 +f 1 1801 1863 +f 1 3289 3351 +f 1 2235 2297 +f 1 313 375 +f 1 251 313 +f 1 189 251 +f 1 127 189 +f 1 375 437 +f 1 1615 1677 +f 1 2483 2545 +f 1 2607 2669 +f 1 3041 3103 +f 1 2979 3041 +f 1 3165 3227 +f 1 3227 3289 +f 1 2545 2607 +f 1 2297 2359 +f 1 3103 3165 +f 1 1429 1491 +f 1 1491 1553 +f 1 3785 3847 +f 1 3847 3909 +f 1 3 65 +f 1 3909 3 +f 1 1739 1801 +f 1 3475 3537 +f 1 3661 3723 +f 1 3599 3661 +f 1 3537 3599 +f 1 1367 1429 +f 1 1243 1305 +f 1 1305 1367 +f 1 499 561 +f 1 1181 1243 +f 1 2111 2173 +f 1 1987 2049 +f 1 2049 2111 +f 1 2359 2421 +f 1 3351 3413 +f 1 2917 2979 +f 1 2731 2793 +f 1 2855 2917 +f 1 2793 2855 +f 1 871 933 +f 1 933 995 +f 1 995 1057 +f 1 1057 1119 +f 1 1119 1181 +f 1 809 871 +f 1 2421 2483 +f 1 1863 1925 +f 1 3413 3475 +f 1 2669 2731 +f 1 65 127 +f 1 1925 1987 +f 1 1677 1739 +f 1 685 747 +f 1 623 685 +f 1 2173 2235 +f 1 747 809 +f 1 3723 3785 +f 1 561 623 +f 2 2978 2916 +f 2 1676 1614 +f 2 2048 1986 +f 2 1738 1676 +f 2 1986 1924 +f 2 1800 1738 +f 2 1924 1862 +f 2 1862 1800 +f 2 436 374 +f 2 2296 2234 +f 2 1304 1242 +f 2 1242 1180 +f 2 1428 1366 +f 2 1366 1304 +f 2 1118 1056 +f 2 1180 1118 +f 2 1614 1552 +f 2 1490 1428 +f 2 250 188 +f 2 3474 3412 +f 2 2668 2606 +f 2 2544 2482 +f 2 2792 2730 +f 2 2854 2792 +f 2 2916 2854 +f 2 3908 3846 +f 2 3970 3908 +f 2 126 64 +f 2 3288 3226 +f 2 3350 3288 +f 2 3412 3350 +f 2 3784 3722 +f 2 3536 3474 +f 2 3598 3536 +f 2 3722 3660 +f 2 3660 3598 +f 2 2482 2420 +f 2 2420 2358 +f 2 3846 3784 +f 2 1552 1490 +f 2 2110 2048 +f 2 2172 2110 +f 2 2234 2172 +f 2 1056 994 +f 2 64 3970 +f 2 622 560 +f 2 188 126 +f 2 684 622 +f 2 3164 3102 +f 2 2606 2544 +f 2 3226 3164 +f 2 2358 2296 +f 2 2730 2668 +f 2 312 250 +f 2 808 746 +f 2 994 932 +f 2 374 312 +f 2 560 498 +f 2 3102 3040 +f 2 870 808 +f 2 932 870 +f 2 746 684 +f 2 498 436 +f 2 3040 2978 +f 3 5194 65 +f 3 3909 5194 +f 4 4158 3910 +f 4 3999 4158 +f 4 4695 66 +f 4 3910 4695 +f 4 66 3999 +f 5 5953 67 +f 5 3911 4546 +f 5 67 5906 +f 5 4483 5953 +f 5 5906 3911 +f 5 4546 4483 +f 6 68 4483 +f 6 4483 3912 +f 6 4838 68 +f 6 3912 4838 +f 7 8 5860 +f 7 4838 4837 +f 7 4837 3913 +f 7 4839 4838 +f 7 69 4839 +f 7 3913 5863 +f 7 5860 69 +f 7 5863 8 +f 8 70 5860 +f 8 5863 3914 +f 8 9 70 +f 8 3914 9 +f 9 71 70 +f 9 3916 10 +f 9 3915 3916 +f 9 3914 3915 +f 9 10 72 +f 9 72 71 +f 10 3916 11 +f 10 11 72 +f 11 3918 12 +f 11 3917 3918 +f 11 12 74 +f 11 73 72 +f 11 3916 3917 +f 11 74 73 +f 12 13 75 +f 12 75 74 +f 12 3918 3919 +f 12 3919 13 +f 13 5678 75 +f 13 3919 14 +f 13 14 5678 +f 14 3919 3920 +f 14 3920 15 +f 14 15 5678 +f 15 77 5678 +f 15 3920 3921 +f 15 3921 16 +f 15 16 77 +f 16 78 77 +f 16 17 79 +f 16 79 78 +f 16 3921 3922 +f 16 3922 17 +f 17 18 80 +f 17 3923 3924 +f 17 3924 18 +f 17 80 79 +f 17 3922 3923 +f 18 19 80 +f 18 3924 19 +f 19 3926 20 +f 19 81 80 +f 19 20 82 +f 19 82 81 +f 19 3924 3925 +f 19 3925 3926 +f 20 83 82 +f 20 3926 3927 +f 20 21 83 +f 20 3927 21 +f 21 22 5476 +f 21 3927 22 +f 21 5476 83 +f 22 23 5476 +f 22 3927 3928 +f 22 3928 23 +f 23 3930 24 +f 23 3929 3930 +f 23 4923 85 +f 23 85 5476 +f 23 3928 3929 +f 23 24 4923 +f 24 25 4923 +f 24 3931 25 +f 24 3930 3931 +f 25 26 5145 +f 25 3931 5082 +f 25 5082 26 +f 25 5145 87 +f 25 87 4923 +f 26 27 5145 +f 26 5082 27 +f 27 28 90 +f 27 5082 3933 +f 27 89 5145 +f 27 3933 28 +f 27 90 89 +f 28 3933 3934 +f 28 3934 29 +f 28 5567 90 +f 28 29 5567 +f 29 3935 5455 +f 29 30 5567 +f 29 5455 30 +f 29 3934 3935 +f 30 5714 92 +f 30 31 5714 +f 30 92 5567 +f 30 5349 31 +f 30 5455 5349 +f 31 32 5790 +f 31 5790 5714 +f 31 5349 32 +f 32 5349 3938 +f 32 3938 5218 +f 32 5218 33 +f 32 33 5635 +f 32 5635 5790 +f 33 5218 5586 +f 33 5586 34 +f 33 34 5866 +f 33 5866 5635 +f 34 5586 35 +f 34 35 5866 +f 35 5586 5838 +f 35 36 5569 +f 35 5838 36 +f 35 5569 5866 +f 36 37 5963 +f 36 5838 5782 +f 36 5782 37 +f 36 5963 5569 +f 37 99 5963 +f 37 100 99 +f 37 4886 38 +f 37 38 100 +f 37 5782 4886 +f 38 4886 3944 +f 38 101 100 +f 38 39 101 +f 38 3944 6043 +f 38 6043 39 +f 39 6043 40 +f 39 40 101 +f 40 6043 3946 +f 40 3946 3947 +f 40 5365 102 +f 40 102 101 +f 40 3947 41 +f 40 41 5365 +f 41 42 5365 +f 41 3947 3948 +f 41 3948 42 +f 42 104 5365 +f 42 43 104 +f 42 3948 43 +f 43 44 105 +f 43 105 104 +f 43 3948 3949 +f 43 3949 44 +f 44 106 105 +f 44 3950 3951 +f 44 3951 45 +f 44 3949 3950 +f 44 107 106 +f 44 45 107 +f 45 5952 107 +f 45 3951 3952 +f 45 46 5952 +f 45 3952 46 +f 46 47 5952 +f 46 5227 47 +f 46 3952 5227 +f 47 6075 5883 +f 47 48 6075 +f 47 5227 48 +f 47 5883 5952 +f 48 6062 6075 +f 48 5227 5993 +f 48 5993 49 +f 48 49 6062 +f 49 5414 111 +f 49 5993 3955 +f 49 3955 5099 +f 49 50 5414 +f 49 5099 50 +f 49 111 6062 +f 50 51 5414 +f 50 5099 51 +f 51 113 5414 +f 51 52 114 +f 51 114 113 +f 51 3958 52 +f 51 5099 3957 +f 51 3957 3958 +f 52 53 115 +f 52 115 114 +f 52 3958 3959 +f 52 3959 53 +f 53 3959 54 +f 53 54 115 +f 54 116 115 +f 54 3960 5495 +f 54 3959 3960 +f 54 55 116 +f 54 5495 55 +f 55 56 117 +f 55 3961 56 +f 55 117 116 +f 55 5495 3961 +f 56 57 119 +f 56 119 118 +f 56 118 117 +f 56 3962 3963 +f 56 3963 57 +f 56 3961 3962 +f 57 3963 58 +f 57 58 119 +f 58 3963 3964 +f 58 3964 5278 +f 58 120 119 +f 58 5278 120 +f 59 5278 3965 +f 59 121 5278 +f 59 60 122 +f 59 3965 3966 +f 59 3966 60 +f 59 122 121 +f 60 3966 4639 +f 60 4637 122 +f 60 4638 4637 +f 60 4639 4638 +f 61 4638 3967 +f 61 4547 123 +f 61 3967 4547 +f 61 123 4638 +f 62 124 5447 +f 62 4420 4482 +f 62 5447 3968 +f 62 4482 4419 +f 62 4419 124 +f 62 3968 4420 +f 63 4191 125 +f 63 4351 4191 +f 63 3969 4351 +f 63 4771 3969 +f 63 125 4771 +f 64 126 4909 +f 64 4909 4908 +f 64 4908 3970 +f 65 5193 127 +f 65 5194 5193 +f 66 3998 3999 +f 66 128 3998 +f 66 4695 4696 +f 66 4696 4697 +f 66 4697 128 +f 67 4355 5906 +f 67 5695 4355 +f 67 5953 129 +f 67 129 5695 +f 68 130 4484 +f 68 4484 4483 +f 68 4840 130 +f 68 4839 4840 +f 68 4838 4839 +f 69 5860 131 +f 69 131 4839 +f 70 132 5860 +f 70 71 133 +f 70 133 132 +f 71 72 133 +f 72 73 134 +f 72 134 133 +f 73 135 134 +f 73 74 135 +f 74 75 136 +f 74 136 135 +f 75 137 136 +f 75 76 137 +f 75 5678 76 +f 76 5678 77 +f 76 139 138 +f 76 77 139 +f 76 138 137 +f 77 78 140 +f 77 140 139 +f 78 79 141 +f 78 141 140 +f 79 142 141 +f 79 80 142 +f 80 81 142 +f 81 144 143 +f 81 143 142 +f 81 82 144 +f 82 83 144 +f 83 5476 84 +f 83 84 145 +f 83 145 144 +f 84 85 5277 +f 84 146 145 +f 84 5277 146 +f 84 5476 85 +f 85 4923 86 +f 85 86 5277 +f 86 148 5277 +f 86 87 4981 +f 86 4923 87 +f 86 4981 148 +f 87 5145 88 +f 87 88 4981 +f 88 5145 89 +f 88 89 150 +f 88 150 4981 +f 89 151 150 +f 89 90 5988 +f 89 5988 151 +f 90 91 5995 +f 90 5995 5988 +f 90 5567 91 +f 91 5567 92 +f 91 5974 5995 +f 91 92 5974 +f 92 5714 93 +f 92 93 5974 +f 93 5714 5790 +f 93 5790 94 +f 93 155 5974 +f 93 94 5828 +f 93 5828 155 +f 94 5635 95 +f 94 5790 5635 +f 94 95 5828 +f 95 5742 157 +f 95 157 5828 +f 95 5635 5866 +f 95 5866 96 +f 95 96 5742 +f 96 5866 97 +f 96 97 5742 +f 97 98 160 +f 97 5866 5569 +f 97 5569 98 +f 97 159 5742 +f 97 160 159 +f 98 99 160 +f 98 5963 99 +f 98 5569 5963 +f 99 5724 161 +f 99 161 160 +f 99 100 5724 +f 100 101 5724 +f 101 163 5724 +f 101 102 5174 +f 101 5174 163 +f 102 5365 103 +f 102 103 5174 +f 103 5365 104 +f 103 165 5174 +f 103 104 165 +f 104 105 166 +f 104 166 165 +f 105 167 166 +f 105 106 167 +f 106 107 5489 +f 106 168 167 +f 106 5489 168 +f 107 5952 6025 +f 107 6025 5489 +f 108 5924 5717 +f 108 5717 6025 +f 108 6025 5952 +f 108 5952 5924 +f 109 6059 5949 +f 109 5883 6075 +f 109 6075 6059 +f 109 5949 5924 +f 109 5924 5883 +f 110 111 6071 +f 110 6075 6062 +f 110 6059 6075 +f 110 6062 111 +f 110 6071 6059 +f 111 5414 112 +f 111 112 173 +f 111 173 6071 +f 112 174 173 +f 112 113 174 +f 112 5414 113 +f 113 175 174 +f 113 114 175 +f 114 176 175 +f 114 115 176 +f 115 116 177 +f 115 177 176 +f 116 117 179 +f 116 178 177 +f 116 179 178 +f 117 180 179 +f 117 118 180 +f 118 119 181 +f 118 181 180 +f 119 120 182 +f 119 182 181 +f 120 121 5449 +f 120 5449 182 +f 120 5278 121 +f 121 183 5449 +f 121 122 183 +f 122 4636 184 +f 122 4637 4636 +f 122 184 183 +f 123 4547 4548 +f 123 185 4637 +f 123 4548 185 +f 123 4637 4638 +f 124 4419 186 +f 124 186 5169 +f 124 4548 5447 +f 124 5169 4548 +f 125 187 4770 +f 125 4770 4771 +f 125 4191 4190 +f 125 4190 187 +f 126 188 4909 +f 127 5191 189 +f 127 5192 5191 +f 127 5193 5192 +f 128 3997 3998 +f 128 190 3997 +f 128 4698 190 +f 128 4697 4698 +f 129 191 5581 +f 129 5953 4485 +f 129 5581 5695 +f 129 4486 191 +f 129 4485 4486 +f 130 192 4485 +f 130 4485 4484 +f 130 4840 4841 +f 130 4841 192 +f 131 193 4840 +f 131 4840 4839 +f 131 4953 193 +f 131 5860 132 +f 131 132 4953 +f 132 194 4953 +f 132 133 194 +f 133 134 196 +f 133 195 194 +f 133 196 195 +f 134 197 196 +f 134 135 197 +f 135 136 198 +f 135 198 197 +f 136 137 198 +f 137 138 199 +f 137 199 198 +f 138 200 199 +f 138 139 201 +f 138 201 200 +f 139 140 201 +f 140 202 201 +f 140 141 202 +f 141 204 203 +f 141 142 204 +f 141 203 202 +f 142 143 205 +f 142 205 204 +f 143 206 205 +f 143 144 206 +f 144 145 206 +f 145 207 206 +f 145 208 207 +f 145 146 208 +f 146 147 208 +f 146 5277 147 +f 147 209 208 +f 147 5148 209 +f 147 148 5148 +f 147 5277 148 +f 148 149 5148 +f 148 4981 149 +f 149 5685 211 +f 149 211 5148 +f 149 150 5685 +f 149 4981 150 +f 150 151 5372 +f 150 5372 5685 +f 151 5988 152 +f 151 152 5372 +f 152 5995 153 +f 152 5988 5995 +f 152 214 5372 +f 152 153 214 +f 153 5995 5974 +f 153 215 214 +f 153 5537 215 +f 153 5974 154 +f 153 154 5537 +f 154 5974 155 +f 154 155 5537 +f 155 5828 156 +f 155 217 5537 +f 155 156 217 +f 156 5828 157 +f 156 218 217 +f 156 219 218 +f 156 157 219 +f 157 5742 158 +f 157 220 219 +f 157 158 220 +f 158 159 220 +f 158 5742 159 +f 159 160 222 +f 159 222 221 +f 159 221 220 +f 160 161 4957 +f 160 4957 222 +f 161 5724 162 +f 161 162 4957 +f 162 5812 224 +f 162 5724 163 +f 162 163 5812 +f 162 224 4957 +f 163 5174 164 +f 163 164 5812 +f 164 227 226 +f 164 165 227 +f 164 226 5812 +f 164 5174 165 +f 165 5208 227 +f 165 166 5208 +f 166 167 5208 +f 167 229 5208 +f 167 168 229 +f 168 231 230 +f 168 5489 169 +f 168 169 231 +f 168 230 229 +f 169 6088 231 +f 169 5489 170 +f 169 170 6088 +f 170 5717 6087 +f 170 6087 6088 +f 170 5489 5717 +f 171 172 233 +f 171 5949 172 +f 171 6087 5949 +f 171 233 6088 +f 171 6088 6087 +f 172 173 234 +f 172 5949 6071 +f 172 234 233 +f 172 6071 173 +f 173 235 234 +f 173 174 235 +f 174 236 235 +f 174 175 237 +f 174 237 236 +f 175 176 238 +f 175 238 237 +f 176 177 238 +f 177 178 240 +f 177 240 239 +f 177 239 238 +f 178 179 241 +f 178 241 240 +f 179 180 241 +f 180 181 242 +f 180 242 241 +f 181 243 242 +f 181 182 243 +f 182 5449 244 +f 182 244 243 +f 183 5040 245 +f 183 245 5449 +f 183 184 5040 +f 184 4635 246 +f 184 4636 4635 +f 184 246 5040 +f 185 4636 4637 +f 185 247 4636 +f 185 4549 247 +f 185 4548 4549 +f 186 4419 4452 +f 186 4452 4450 +f 186 4450 248 +f 186 248 5169 +f 187 4190 4189 +f 187 4189 249 +f 187 249 4769 +f 187 4769 4770 +f 188 4911 4910 +f 188 250 4911 +f 188 4910 4909 +f 189 5191 251 +f 190 252 3997 +f 190 4698 4699 +f 190 4699 252 +f 191 253 5581 +f 191 4486 4487 +f 191 4487 253 +f 192 4486 4485 +f 192 254 5280 +f 192 4842 254 +f 192 4841 4842 +f 192 5280 4486 +f 193 5788 255 +f 193 4953 5788 +f 193 4841 4840 +f 193 255 4841 +f 194 4962 256 +f 194 195 4962 +f 194 256 4953 +f 195 257 4962 +f 195 196 257 +f 196 197 258 +f 196 258 257 +f 197 260 259 +f 197 198 260 +f 197 259 258 +f 198 199 6005 +f 198 6005 260 +f 199 261 6005 +f 199 262 261 +f 199 200 262 +f 200 201 263 +f 200 263 262 +f 201 202 263 +f 202 264 263 +f 202 203 265 +f 202 265 264 +f 203 266 265 +f 203 204 266 +f 204 205 266 +f 205 206 268 +f 205 267 266 +f 205 268 267 +f 206 207 268 +f 207 269 268 +f 207 208 270 +f 207 270 269 +f 208 209 270 +f 209 210 271 +f 209 5148 210 +f 209 271 270 +f 210 272 271 +f 210 5148 211 +f 210 211 272 +f 211 5685 212 +f 211 5532 273 +f 211 212 5532 +f 211 273 272 +f 212 5656 5532 +f 212 213 5656 +f 212 5685 5372 +f 212 5372 213 +f 213 214 5656 +f 213 5372 214 +f 214 215 276 +f 214 276 5656 +f 215 5537 216 +f 215 216 277 +f 215 277 276 +f 216 217 5285 +f 216 5537 217 +f 216 5285 278 +f 216 278 277 +f 217 218 5285 +f 218 280 5285 +f 218 219 5776 +f 218 5776 280 +f 219 220 5065 +f 219 5065 5776 +f 220 221 5065 +f 221 222 5254 +f 221 5254 283 +f 221 283 5065 +f 222 4957 223 +f 222 223 5254 +f 223 285 5254 +f 223 224 286 +f 223 286 285 +f 223 4957 224 +f 224 5812 225 +f 224 225 287 +f 224 287 286 +f 225 226 287 +f 225 5812 226 +f 226 5386 288 +f 226 288 287 +f 226 227 5386 +f 227 5208 228 +f 227 228 5386 +f 228 5021 290 +f 228 290 5386 +f 228 5208 229 +f 228 229 5021 +f 229 230 5021 +f 230 4970 292 +f 230 231 4970 +f 230 292 5021 +f 231 6088 232 +f 231 232 4970 +f 232 294 4970 +f 232 6088 233 +f 232 295 294 +f 232 233 295 +f 233 234 295 +f 234 235 297 +f 234 296 295 +f 234 297 296 +f 235 236 297 +f 236 237 5401 +f 236 298 297 +f 236 5401 298 +f 237 238 5401 +f 238 300 5401 +f 238 239 300 +f 239 240 301 +f 239 301 300 +f 240 241 303 +f 240 303 302 +f 240 302 301 +f 241 242 303 +f 242 243 304 +f 242 304 303 +f 243 244 306 +f 243 306 305 +f 243 305 304 +f 244 5449 245 +f 244 245 5170 +f 244 5170 306 +f 245 5040 307 +f 245 307 5170 +f 246 308 5040 +f 246 4635 308 +f 247 4635 4636 +f 247 4549 4550 +f 247 4550 309 +f 247 309 4634 +f 247 4634 4635 +f 248 4450 310 +f 248 4550 5169 +f 248 310 4551 +f 248 4551 4550 +f 249 4768 4769 +f 249 4767 4768 +f 249 311 4767 +f 249 4189 311 +f 250 312 4912 +f 250 4912 4911 +f 251 5190 5189 +f 251 5189 313 +f 251 5191 5190 +f 252 4000 3997 +f 252 314 4000 +f 252 4699 314 +f 253 4487 4488 +f 253 4488 315 +f 253 5109 4417 +f 253 315 5109 +f 253 4417 5581 +f 254 316 5280 +f 254 4842 316 +f 255 5788 4999 +f 255 4999 317 +f 255 317 4843 +f 255 4843 4842 +f 255 4842 4841 +f 256 4962 5783 +f 256 5783 318 +f 256 4999 4953 +f 256 318 4999 +f 257 319 5783 +f 257 5105 319 +f 257 258 5105 +f 257 5783 4962 +f 258 320 5105 +f 258 259 321 +f 258 321 320 +f 259 260 6019 +f 259 6056 321 +f 259 6019 6056 +f 260 6064 6019 +f 260 6005 6064 +f 261 262 6000 +f 261 6000 6005 +f 262 6093 6000 +f 262 324 6093 +f 262 325 324 +f 262 263 325 +f 263 264 325 +f 264 265 326 +f 264 326 325 +f 265 327 326 +f 265 266 328 +f 265 328 327 +f 266 267 328 +f 267 268 330 +f 267 330 329 +f 267 329 328 +f 268 269 330 +f 269 270 331 +f 269 331 330 +f 270 332 331 +f 270 271 332 +f 271 5874 333 +f 271 272 5874 +f 271 333 332 +f 272 273 5460 +f 272 5460 5874 +f 273 5532 274 +f 273 274 5460 +f 274 5656 275 +f 274 5532 5656 +f 274 337 336 +f 274 336 5460 +f 274 275 337 +f 275 276 338 +f 275 338 337 +f 275 5656 276 +f 276 277 339 +f 276 339 338 +f 277 278 339 +f 278 5610 340 +f 278 279 5610 +f 278 5285 279 +f 278 340 339 +f 279 280 5610 +f 279 5285 280 +f 280 281 343 +f 280 343 342 +f 280 5776 281 +f 280 342 5610 +f 281 5065 282 +f 281 282 344 +f 281 344 343 +f 281 5776 5065 +f 282 283 5357 +f 282 5357 344 +f 282 5065 283 +f 283 284 5357 +f 283 5254 284 +f 284 346 5357 +f 284 347 346 +f 284 285 347 +f 284 5254 285 +f 285 348 347 +f 285 286 348 +f 286 287 348 +f 287 288 349 +f 287 349 348 +f 288 5386 289 +f 288 289 350 +f 288 350 349 +f 289 5386 290 +f 289 352 351 +f 289 351 350 +f 289 290 352 +f 290 5036 352 +f 290 5021 291 +f 290 291 5036 +f 291 5021 292 +f 291 292 5036 +f 292 4970 293 +f 292 293 5711 +f 292 354 5036 +f 292 5711 354 +f 293 4970 294 +f 293 294 5711 +f 294 356 5711 +f 294 357 356 +f 294 295 357 +f 295 296 357 +f 296 297 359 +f 296 359 358 +f 296 358 357 +f 297 298 360 +f 297 360 359 +f 298 5522 360 +f 298 5401 299 +f 298 299 5522 +f 299 300 5522 +f 299 5401 300 +f 300 362 5522 +f 300 301 362 +f 301 363 362 +f 301 364 363 +f 301 302 364 +f 302 303 364 +f 303 365 364 +f 303 366 365 +f 303 304 366 +f 304 305 366 +f 305 5503 367 +f 305 306 5503 +f 305 367 366 +f 306 5170 368 +f 306 368 5503 +f 307 369 5170 +f 307 308 369 +f 307 5040 308 +f 308 4633 370 +f 308 4635 4634 +f 308 370 369 +f 308 4634 4633 +f 309 4550 4551 +f 309 4551 371 +f 309 371 4634 +f 310 4450 4451 +f 310 4451 4449 +f 310 372 4552 +f 310 4552 4551 +f 310 4449 372 +f 311 4192 373 +f 311 373 4767 +f 311 4189 4192 +f 312 374 4912 +f 313 5188 375 +f 313 5189 5188 +f 314 4001 4000 +f 314 376 4001 +f 314 4699 4700 +f 314 4701 376 +f 314 4700 4701 +f 315 4488 4489 +f 315 4489 377 +f 315 377 5109 +f 316 4842 4843 +f 316 4844 378 +f 316 4843 4844 +f 316 378 4488 +f 316 4488 5280 +f 317 5889 4843 +f 317 5898 5889 +f 317 5891 5898 +f 317 4999 5891 +f 318 380 5890 +f 318 5890 5891 +f 318 5783 380 +f 318 5891 4999 +f 319 5105 381 +f 319 381 5783 +f 320 382 5105 +f 320 383 382 +f 320 321 383 +f 321 6056 383 +f 322 6056 6019 +f 322 384 6056 +f 322 6064 5398 +f 322 5398 5045 +f 322 6019 6064 +f 322 5045 384 +f 323 6093 385 +f 323 5398 6000 +f 323 5045 5398 +f 323 385 5045 +f 323 6000 6093 +f 324 5787 6093 +f 324 325 5787 +f 325 326 387 +f 325 387 5787 +f 326 388 387 +f 326 327 389 +f 326 389 388 +f 327 390 389 +f 327 328 390 +f 328 329 390 +f 329 392 391 +f 329 330 392 +f 329 391 390 +f 330 331 393 +f 330 393 392 +f 331 332 394 +f 331 394 393 +f 332 395 394 +f 332 333 395 +f 333 5874 334 +f 333 334 395 +f 334 396 395 +f 334 5497 396 +f 334 335 5497 +f 334 5460 335 +f 334 5874 5460 +f 335 336 5497 +f 335 5460 336 +f 336 398 5497 +f 336 337 398 +f 337 399 398 +f 337 400 399 +f 337 338 400 +f 338 339 400 +f 339 5407 401 +f 339 401 400 +f 339 340 5407 +f 340 5610 341 +f 340 341 5407 +f 341 5610 342 +f 341 342 403 +f 341 403 5407 +f 342 404 403 +f 342 5383 404 +f 342 343 5383 +f 343 5393 5383 +f 343 344 5393 +f 344 5357 345 +f 344 345 5393 +f 345 5357 346 +f 345 408 407 +f 345 346 408 +f 345 407 5393 +f 346 5290 408 +f 346 347 5290 +f 347 348 5290 +f 348 410 5290 +f 348 349 411 +f 348 411 410 +f 349 350 411 +f 350 351 413 +f 350 413 412 +f 350 412 411 +f 351 352 413 +f 352 5036 353 +f 352 414 413 +f 352 5161 414 +f 352 353 5161 +f 353 5036 354 +f 353 354 5161 +f 354 416 5161 +f 354 355 416 +f 354 5711 355 +f 355 356 417 +f 355 417 416 +f 355 5711 356 +f 356 357 418 +f 356 418 417 +f 357 419 418 +f 357 5492 419 +f 357 358 5492 +f 358 359 5492 +f 359 5518 421 +f 359 360 5518 +f 359 421 5492 +f 360 361 5518 +f 360 5522 361 +f 361 423 5518 +f 361 5522 362 +f 361 362 424 +f 361 424 423 +f 362 363 424 +f 363 425 424 +f 363 364 425 +f 364 426 425 +f 364 365 427 +f 364 427 426 +f 365 366 428 +f 365 428 427 +f 366 367 428 +f 367 5503 429 +f 367 429 428 +f 368 430 5503 +f 368 5170 369 +f 368 369 430 +f 369 431 430 +f 369 432 431 +f 369 370 432 +f 370 4632 432 +f 370 4633 4632 +f 371 4551 4552 +f 371 433 4633 +f 371 4552 433 +f 371 4633 4634 +f 372 5445 434 +f 372 4553 4552 +f 372 434 4553 +f 372 4449 5445 +f 373 5526 435 +f 373 4192 4193 +f 373 4766 4767 +f 373 435 4766 +f 373 4193 5526 +f 374 436 4914 +f 374 4913 4912 +f 374 4914 4913 +f 375 5188 5187 +f 375 5187 437 +f 376 438 4002 +f 376 4701 4702 +f 376 4702 438 +f 376 4002 4001 +f 377 5615 5731 +f 377 439 5615 +f 377 4489 4490 +f 377 4490 439 +f 377 5731 5109 +f 378 4844 4845 +f 378 4845 440 +f 378 5456 4489 +f 378 4489 4488 +f 378 440 5456 +f 379 5425 5889 +f 379 5890 441 +f 379 5889 5898 +f 379 441 5425 +f 379 5898 5890 +f 380 4885 5890 +f 380 442 4885 +f 380 5783 381 +f 380 381 443 +f 380 443 442 +f 381 382 444 +f 381 444 443 +f 381 5105 382 +f 382 383 444 +f 383 384 6113 +f 383 445 444 +f 383 6056 384 +f 383 6113 445 +f 384 5045 6113 +f 385 6093 5881 +f 385 5881 5080 +f 385 5080 5045 +f 386 5787 387 +f 386 449 448 +f 386 387 449 +f 386 5881 5787 +f 386 448 5881 +f 387 388 449 +f 388 389 450 +f 388 450 449 +f 389 451 450 +f 389 452 451 +f 389 390 452 +f 390 453 452 +f 390 391 453 +f 391 392 453 +f 392 393 454 +f 392 454 453 +f 393 455 454 +f 393 394 455 +f 394 395 457 +f 394 456 455 +f 394 457 456 +f 395 4917 457 +f 395 396 4917 +f 396 5497 397 +f 396 397 4917 +f 397 5497 398 +f 397 398 460 +f 397 459 4917 +f 397 460 459 +f 398 5089 460 +f 398 399 5089 +f 399 400 5089 +f 400 462 5089 +f 400 401 462 +f 401 402 464 +f 401 5407 402 +f 401 463 462 +f 401 464 463 +f 402 5407 403 +f 402 465 464 +f 402 403 465 +f 403 404 465 +f 404 405 466 +f 404 5383 405 +f 404 466 465 +f 405 467 466 +f 405 468 467 +f 405 406 468 +f 405 5393 406 +f 405 5383 5393 +f 406 407 5409 +f 406 5409 468 +f 406 5393 407 +f 407 408 5409 +f 408 409 470 +f 408 470 5409 +f 408 5290 409 +f 409 471 470 +f 409 5290 410 +f 409 5175 471 +f 409 410 5175 +f 410 411 5175 +f 411 473 5175 +f 411 412 4974 +f 411 4974 473 +f 412 413 4974 +f 413 414 475 +f 413 475 4974 +f 414 5161 415 +f 414 4986 476 +f 414 476 475 +f 414 415 4986 +f 415 5161 416 +f 415 416 4986 +f 416 478 4986 +f 416 417 4984 +f 416 4984 478 +f 417 418 4984 +f 418 481 480 +f 418 419 481 +f 418 480 4984 +f 419 482 481 +f 419 420 482 +f 419 5492 420 +f 420 5492 421 +f 420 421 482 +f 421 5518 422 +f 421 483 482 +f 421 422 483 +f 422 5518 423 +f 422 485 484 +f 422 484 483 +f 422 423 485 +f 423 486 485 +f 423 424 486 +f 424 425 486 +f 425 426 487 +f 425 487 486 +f 426 427 488 +f 426 488 487 +f 427 490 489 +f 427 489 488 +f 427 428 490 +f 428 429 491 +f 428 491 490 +f 429 5503 430 +f 429 430 491 +f 430 492 491 +f 430 431 492 +f 431 4980 493 +f 431 493 492 +f 431 432 4980 +f 432 494 4980 +f 432 4632 4631 +f 432 4631 494 +f 433 495 4632 +f 433 4632 4633 +f 433 4553 495 +f 433 4552 4553 +f 434 496 4554 +f 434 4554 4553 +f 434 5445 496 +f 435 5526 497 +f 435 497 4765 +f 435 4765 4766 +f 436 5034 4914 +f 436 498 5034 +f 437 5187 499 +f 438 4003 4002 +f 438 4702 4703 +f 438 4703 500 +f 438 500 4003 +f 439 4491 501 +f 439 4490 4491 +f 439 501 5615 +f 440 4845 4846 +f 440 4846 502 +f 440 502 5456 +f 441 5452 503 +f 441 4885 5452 +f 441 5890 4885 +f 441 503 4982 +f 441 4982 5425 +f 442 504 4885 +f 442 505 504 +f 442 443 505 +f 443 444 5342 +f 443 5342 505 +f 444 506 5342 +f 444 507 506 +f 444 445 507 +f 445 446 6117 +f 445 6117 6118 +f 445 6118 507 +f 445 6113 446 +f 446 6110 6117 +f 446 6113 6110 +f 447 509 6094 +f 447 6094 6110 +f 447 6110 5080 +f 447 5080 448 +f 447 448 509 +f 448 5080 5881 +f 448 511 510 +f 448 449 511 +f 448 510 509 +f 449 450 511 +f 450 512 511 +f 450 451 512 +f 451 514 513 +f 451 452 514 +f 451 513 512 +f 452 453 514 +f 453 516 515 +f 453 454 516 +f 453 515 514 +f 454 455 516 +f 455 517 516 +f 455 456 517 +f 456 457 5624 +f 456 518 517 +f 456 5624 518 +f 457 458 5624 +f 457 4917 458 +f 458 521 520 +f 458 459 521 +f 458 4917 459 +f 458 520 5624 +f 459 460 522 +f 459 522 521 +f 460 5089 461 +f 460 461 522 +f 461 5089 462 +f 461 523 522 +f 461 462 524 +f 461 524 523 +f 462 463 5041 +f 462 5041 524 +f 463 464 5041 +f 464 526 5041 +f 464 465 5707 +f 464 5707 526 +f 465 466 5707 +f 466 528 5707 +f 466 467 5015 +f 466 5015 528 +f 467 5001 5015 +f 467 468 5001 +f 468 469 5001 +f 468 5409 469 +f 469 5379 5001 +f 469 470 5379 +f 469 5409 470 +f 470 5719 532 +f 470 471 5719 +f 470 532 5379 +f 471 472 5719 +f 471 5175 472 +f 472 473 5605 +f 472 5605 534 +f 472 5175 473 +f 472 534 5719 +f 473 4974 474 +f 473 474 5605 +f 474 475 5642 +f 474 5642 5605 +f 474 4974 475 +f 475 537 5642 +f 475 476 537 +f 476 538 537 +f 476 539 538 +f 476 477 539 +f 476 4986 477 +f 477 540 539 +f 477 4986 478 +f 477 478 540 +f 478 5131 540 +f 478 479 5131 +f 478 4984 479 +f 479 4984 480 +f 479 480 5131 +f 480 481 5228 +f 480 5228 542 +f 480 542 5131 +f 481 482 5228 +f 482 544 5228 +f 482 483 545 +f 482 545 544 +f 483 484 545 +f 484 5067 546 +f 484 485 5067 +f 484 546 545 +f 485 486 5067 +f 486 487 548 +f 486 548 5067 +f 487 550 549 +f 487 488 550 +f 487 549 548 +f 488 489 551 +f 488 551 550 +f 489 490 551 +f 490 552 551 +f 490 491 552 +f 491 492 554 +f 491 553 552 +f 491 554 553 +f 492 493 554 +f 493 555 554 +f 493 4980 555 +f 494 4631 556 +f 494 556 4980 +f 495 4553 4554 +f 495 4631 4632 +f 495 5059 557 +f 495 4554 5059 +f 495 4630 4631 +f 495 557 4630 +f 496 4555 4554 +f 496 5445 4448 +f 496 558 4555 +f 496 4448 558 +f 497 5443 559 +f 497 5526 4195 +f 497 4764 4765 +f 497 559 4763 +f 497 4763 4764 +f 497 4195 5443 +f 498 560 5034 +f 499 5187 4011 +f 499 4011 4017 +f 499 4017 561 +f 500 562 4018 +f 500 4004 4003 +f 500 4018 4004 +f 500 4703 562 +f 501 563 5774 +f 501 5774 4415 +f 501 4415 5615 +f 501 4492 563 +f 501 4491 4492 +f 502 564 4945 +f 502 4846 564 +f 502 4945 5456 +f 503 5270 4982 +f 503 5440 565 +f 503 565 5270 +f 503 5452 5440 +f 504 566 5104 +f 504 5104 5440 +f 504 5440 4885 +f 504 505 566 +f 505 5342 567 +f 505 567 566 +f 506 568 5342 +f 506 507 568 +f 507 6118 569 +f 507 569 568 +f 508 6117 6094 +f 508 6094 6097 +f 508 6097 570 +f 508 6118 6117 +f 508 570 6118 +f 509 510 6072 +f 509 6072 6094 +f 510 572 6072 +f 510 511 572 +f 511 512 574 +f 511 574 573 +f 511 573 572 +f 512 513 575 +f 512 575 574 +f 513 514 575 +f 514 515 577 +f 514 577 576 +f 514 576 575 +f 515 516 5578 +f 515 5578 577 +f 516 517 5578 +f 517 518 579 +f 517 579 5578 +f 518 519 5628 +f 518 5628 580 +f 518 5624 519 +f 518 580 579 +f 519 520 5628 +f 519 5624 520 +f 520 5176 582 +f 520 582 5628 +f 520 521 5176 +f 521 5410 5176 +f 521 522 5410 +f 522 5406 5410 +f 522 523 5406 +f 523 5380 5406 +f 523 524 5380 +f 524 525 5380 +f 524 5041 525 +f 525 588 587 +f 525 587 5380 +f 525 526 588 +f 525 5041 526 +f 526 5707 527 +f 526 589 588 +f 526 527 589 +f 527 590 589 +f 527 5707 528 +f 527 528 590 +f 528 591 590 +f 528 529 591 +f 528 5015 529 +f 529 5001 530 +f 529 5015 5001 +f 529 530 592 +f 529 592 591 +f 530 5001 531 +f 530 531 592 +f 531 5379 532 +f 531 593 592 +f 531 5001 5379 +f 531 532 593 +f 532 533 594 +f 532 5719 533 +f 532 594 593 +f 533 5719 534 +f 533 596 595 +f 533 595 594 +f 533 534 596 +f 534 5605 535 +f 534 535 596 +f 535 5605 536 +f 535 598 597 +f 535 536 598 +f 535 597 596 +f 536 5605 5642 +f 536 5069 598 +f 536 5642 537 +f 536 537 5069 +f 537 538 5069 +f 538 539 600 +f 538 600 5069 +f 539 540 5085 +f 539 601 600 +f 539 5085 601 +f 540 541 5085 +f 540 5131 541 +f 541 603 5085 +f 541 542 5066 +f 541 5066 603 +f 541 5131 542 +f 542 543 5066 +f 542 5228 543 +f 543 5228 544 +f 543 605 5066 +f 543 544 606 +f 543 606 605 +f 544 545 606 +f 545 607 606 +f 545 546 607 +f 546 608 607 +f 546 5067 547 +f 546 547 608 +f 547 5467 609 +f 547 5067 548 +f 547 548 5467 +f 547 609 608 +f 548 549 5467 +f 549 611 5467 +f 549 550 611 +f 550 551 612 +f 550 612 611 +f 551 613 612 +f 551 552 614 +f 551 614 613 +f 552 553 615 +f 552 615 614 +f 553 554 615 +f 554 555 617 +f 554 616 615 +f 554 617 616 +f 555 4980 556 +f 555 556 618 +f 555 618 617 +f 556 4630 4629 +f 556 4629 618 +f 556 4631 4630 +f 557 619 4630 +f 557 5059 619 +f 558 4448 4454 +f 558 4556 4555 +f 558 620 4556 +f 558 4454 4447 +f 558 4447 620 +f 559 5443 621 +f 559 621 4763 +f 560 4212 4203 +f 560 622 4212 +f 560 4203 5034 +f 561 5010 623 +f 561 4017 5010 +f 562 624 4018 +f 562 4705 624 +f 562 4703 4704 +f 562 4704 4705 +f 563 625 5774 +f 563 4492 4493 +f 563 4493 625 +f 564 4847 4848 +f 564 4848 626 +f 564 5738 4945 +f 564 626 5738 +f 564 4846 4847 +f 565 5104 627 +f 565 5440 5104 +f 565 627 4847 +f 565 4847 5270 +f 566 567 628 +f 566 628 5104 +f 567 5342 568 +f 567 629 628 +f 567 568 629 +f 568 631 630 +f 568 569 631 +f 568 630 629 +f 569 632 631 +f 569 570 632 +f 569 6118 570 +f 570 6097 632 +f 571 572 633 +f 571 633 6097 +f 571 6072 572 +f 571 6097 6072 +f 572 634 633 +f 572 573 634 +f 573 635 634 +f 573 636 635 +f 573 574 636 +f 574 575 637 +f 574 637 636 +f 575 576 637 +f 576 638 637 +f 576 577 638 +f 577 5578 578 +f 577 578 639 +f 577 639 638 +f 578 640 639 +f 578 579 641 +f 578 641 640 +f 578 5578 579 +f 579 580 5375 +f 579 5375 641 +f 580 581 5375 +f 580 5628 581 +f 581 5628 582 +f 581 644 643 +f 581 643 5375 +f 581 582 644 +f 582 5632 644 +f 582 5176 583 +f 582 583 5632 +f 583 5176 5410 +f 583 584 5632 +f 583 5410 584 +f 584 585 647 +f 584 647 646 +f 584 5406 585 +f 584 5410 5406 +f 584 646 5632 +f 585 5406 5380 +f 585 5380 586 +f 585 586 647 +f 586 5380 587 +f 586 587 649 +f 586 648 647 +f 586 649 648 +f 587 650 649 +f 587 588 650 +f 588 589 650 +f 589 590 652 +f 589 651 650 +f 589 652 651 +f 590 591 653 +f 590 653 652 +f 591 592 5861 +f 591 5861 653 +f 592 593 5861 +f 593 5727 5861 +f 593 594 5727 +f 594 595 5696 +f 594 5696 5727 +f 595 596 5832 +f 595 657 5696 +f 595 5832 657 +f 596 597 5832 +f 597 659 5832 +f 597 598 660 +f 597 660 659 +f 598 5069 599 +f 598 599 660 +f 599 5675 661 +f 599 5069 600 +f 599 661 660 +f 599 600 5675 +f 600 601 5675 +f 601 5108 5675 +f 601 5085 602 +f 601 602 5108 +f 602 665 664 +f 602 603 665 +f 602 5085 603 +f 602 664 5108 +f 603 604 666 +f 603 666 665 +f 603 5066 604 +f 604 605 5528 +f 604 5528 666 +f 604 5066 605 +f 605 606 5528 +f 606 668 5528 +f 606 669 668 +f 606 607 669 +f 607 608 670 +f 607 670 669 +f 608 609 5765 +f 608 5765 670 +f 609 610 5765 +f 609 5467 610 +f 610 611 673 +f 610 673 672 +f 610 672 5765 +f 610 5467 611 +f 611 674 673 +f 611 612 674 +f 612 675 674 +f 612 613 675 +f 613 614 675 +f 614 676 675 +f 614 615 676 +f 615 677 676 +f 615 616 678 +f 615 678 677 +f 616 617 679 +f 616 679 678 +f 617 618 679 +f 618 680 679 +f 618 4629 4628 +f 618 4628 680 +f 619 4629 4630 +f 619 4556 681 +f 619 5059 4556 +f 619 681 4629 +f 620 682 4557 +f 620 4557 4556 +f 620 4447 682 +f 621 4209 683 +f 621 4762 4763 +f 621 683 4762 +f 621 5443 4209 +f 622 684 4942 +f 622 4942 4212 +f 623 5010 685 +f 624 4019 4018 +f 624 686 4019 +f 624 4706 686 +f 624 4705 4706 +f 625 4493 687 +f 625 687 4360 +f 625 4360 4414 +f 625 4414 5774 +f 626 4493 5738 +f 626 688 4494 +f 626 4848 4849 +f 626 4494 4493 +f 626 4849 688 +f 627 689 4848 +f 627 4848 4847 +f 627 690 689 +f 627 628 690 +f 627 5104 628 +f 628 629 690 +f 629 691 690 +f 629 630 691 +f 630 692 691 +f 630 631 692 +f 631 632 694 +f 631 694 693 +f 631 693 692 +f 632 6097 633 +f 632 633 694 +f 633 634 696 +f 633 695 694 +f 633 696 695 +f 634 635 696 +f 635 697 696 +f 635 636 698 +f 635 698 697 +f 636 637 698 +f 637 699 698 +f 637 700 699 +f 637 638 700 +f 638 701 700 +f 638 639 701 +f 639 640 701 +f 640 5550 702 +f 640 641 5550 +f 640 702 701 +f 641 642 5257 +f 641 5375 642 +f 641 5257 5550 +f 642 5375 643 +f 642 643 5257 +f 643 705 5257 +f 643 644 5720 +f 643 5720 705 +f 644 5632 645 +f 644 645 5530 +f 644 5530 5720 +f 645 5997 5530 +f 645 646 5997 +f 645 5632 646 +f 646 647 5442 +f 646 5442 5997 +f 647 648 5442 +f 648 5437 710 +f 648 710 5442 +f 648 649 5437 +f 649 650 5737 +f 649 5737 5437 +f 650 651 5670 +f 650 5670 5737 +f 651 5444 5670 +f 651 652 5444 +f 652 653 5220 +f 652 5220 5444 +f 653 5861 654 +f 653 5196 5220 +f 653 654 5196 +f 654 655 5196 +f 654 5861 655 +f 655 5861 5727 +f 655 656 5276 +f 655 5727 656 +f 655 5276 717 +f 655 717 5196 +f 656 657 5276 +f 656 5696 657 +f 656 5727 5696 +f 657 719 5276 +f 657 658 720 +f 657 720 719 +f 657 5832 658 +f 658 5956 720 +f 658 659 5956 +f 658 5832 659 +f 659 660 5956 +f 660 722 5956 +f 660 661 722 +f 661 5675 662 +f 661 723 722 +f 661 662 5147 +f 661 5147 723 +f 662 5675 663 +f 662 663 5147 +f 663 725 5147 +f 663 5675 5108 +f 663 5108 664 +f 663 726 725 +f 663 664 726 +f 664 665 727 +f 664 727 726 +f 665 728 727 +f 665 666 728 +f 666 5528 667 +f 666 729 728 +f 666 667 729 +f 667 730 729 +f 667 5528 668 +f 667 668 730 +f 668 669 5516 +f 668 5516 730 +f 669 670 5516 +f 670 732 5516 +f 670 5765 671 +f 670 671 733 +f 670 733 732 +f 671 672 733 +f 671 5765 672 +f 672 734 733 +f 672 673 735 +f 672 735 734 +f 673 674 736 +f 673 736 735 +f 674 675 736 +f 675 676 737 +f 675 737 736 +f 676 738 737 +f 676 739 738 +f 676 677 739 +f 677 5593 739 +f 677 678 5593 +f 678 740 5593 +f 678 741 740 +f 678 679 741 +f 679 680 5643 +f 679 5643 741 +f 680 742 5643 +f 680 4628 4627 +f 680 4627 742 +f 681 743 4628 +f 681 4557 4558 +f 681 4558 743 +f 681 4556 4557 +f 681 4628 4629 +f 682 744 4557 +f 682 4455 4939 +f 682 4447 4455 +f 682 4939 744 +f 683 4761 4762 +f 683 4209 4210 +f 683 4210 745 +f 683 745 4761 +f 684 4941 4942 +f 684 746 4941 +f 685 5011 747 +f 685 5010 5009 +f 685 5009 5011 +f 686 4020 4019 +f 686 748 4020 +f 686 4706 4707 +f 686 4707 748 +f 687 4493 4494 +f 687 5432 4360 +f 687 4494 4495 +f 687 4495 749 +f 687 749 5432 +f 688 750 4494 +f 688 4849 4850 +f 688 4850 750 +f 689 690 751 +f 689 751 4849 +f 689 4849 4848 +f 690 752 751 +f 690 691 752 +f 691 754 753 +f 691 753 752 +f 691 692 754 +f 692 693 754 +f 693 755 754 +f 693 756 755 +f 693 694 756 +f 694 5789 756 +f 694 695 5789 +f 695 5879 5789 +f 695 696 5879 +f 696 697 5879 +f 697 759 5879 +f 697 760 759 +f 697 698 760 +f 698 699 760 +f 699 761 760 +f 699 700 761 +f 700 762 761 +f 700 701 763 +f 700 763 762 +f 701 702 6131 +f 701 6131 763 +f 702 703 6131 +f 702 5550 703 +f 703 704 6133 +f 703 5257 704 +f 703 5550 5257 +f 703 6133 765 +f 703 765 6131 +f 704 705 6133 +f 704 5257 705 +f 705 767 6133 +f 705 706 767 +f 705 5720 706 +f 706 707 769 +f 706 769 768 +f 706 768 767 +f 706 5720 5530 +f 706 5530 707 +f 707 5997 708 +f 707 5530 5997 +f 707 770 769 +f 707 708 770 +f 708 5997 5442 +f 708 5442 709 +f 708 709 770 +f 709 5442 710 +f 709 710 771 +f 709 771 770 +f 710 711 5232 +f 710 5232 772 +f 710 772 771 +f 710 5437 711 +f 711 5737 712 +f 711 5437 5737 +f 711 712 4997 +f 711 4997 5232 +f 712 5708 4997 +f 712 5737 5670 +f 712 5670 713 +f 712 713 5708 +f 713 5444 714 +f 713 5670 5444 +f 713 714 5708 +f 714 5220 715 +f 714 5444 5220 +f 714 776 5708 +f 714 715 776 +f 715 5220 5196 +f 715 5196 716 +f 715 778 777 +f 715 777 776 +f 715 716 778 +f 716 5196 717 +f 716 779 778 +f 716 717 779 +f 717 718 5149 +f 717 5276 718 +f 717 5149 779 +f 718 5276 719 +f 718 719 5149 +f 719 720 781 +f 719 781 5149 +f 720 5956 721 +f 720 783 782 +f 720 782 781 +f 720 721 783 +f 721 5956 722 +f 721 784 783 +f 721 722 784 +f 722 785 784 +f 722 723 785 +f 723 724 785 +f 723 5147 724 +f 724 786 785 +f 724 725 786 +f 724 5147 725 +f 725 726 6054 +f 725 6054 787 +f 725 787 786 +f 726 727 6054 +f 727 6049 6054 +f 727 728 6049 +f 728 790 6049 +f 728 729 790 +f 729 791 790 +f 729 792 791 +f 729 730 792 +f 730 731 792 +f 730 5516 731 +f 731 793 792 +f 731 732 793 +f 731 5516 732 +f 732 795 794 +f 732 733 795 +f 732 794 793 +f 733 734 795 +f 734 796 795 +f 734 735 797 +f 734 797 796 +f 735 736 797 +f 736 799 798 +f 736 737 799 +f 736 798 797 +f 737 738 799 +f 738 800 799 +f 738 739 801 +f 738 801 800 +f 739 5593 801 +f 740 803 802 +f 740 741 803 +f 740 802 5593 +f 741 5643 5598 +f 741 5598 803 +f 742 4627 804 +f 742 804 5598 +f 742 5598 5643 +f 743 4627 4628 +f 743 4626 4627 +f 743 805 4626 +f 743 4558 805 +f 744 4558 4557 +f 744 4559 4558 +f 744 4939 806 +f 744 806 4559 +f 745 4210 4211 +f 745 4760 4761 +f 745 807 4759 +f 745 4759 4760 +f 745 4211 807 +f 746 808 4941 +f 747 5011 5012 +f 747 5012 809 +f 748 5183 4023 +f 748 810 5183 +f 748 4023 4020 +f 748 4707 810 +f 749 811 5432 +f 749 4495 5419 +f 749 5419 811 +f 750 4495 4494 +f 750 812 4495 +f 750 4850 812 +f 751 4851 4850 +f 751 813 4851 +f 751 4850 4849 +f 751 752 813 +f 752 814 813 +f 752 815 814 +f 752 753 815 +f 753 754 816 +f 753 816 815 +f 754 755 816 +f 755 817 816 +f 755 756 817 +f 756 5789 757 +f 756 818 817 +f 756 757 818 +f 757 758 819 +f 757 5879 758 +f 757 819 818 +f 757 5789 5879 +f 758 5879 759 +f 758 820 819 +f 758 759 820 +f 759 821 820 +f 759 760 822 +f 759 822 821 +f 760 761 822 +f 761 823 822 +f 761 824 823 +f 761 762 824 +f 762 763 824 +f 763 825 824 +f 763 764 825 +f 763 6131 764 +f 764 6131 765 +f 764 6130 826 +f 764 826 825 +f 764 765 6130 +f 765 766 5545 +f 765 6133 766 +f 765 5545 6130 +f 766 767 5771 +f 766 6133 767 +f 766 5771 5545 +f 767 768 5744 +f 767 5744 5771 +f 768 5650 5744 +f 768 769 5650 +f 769 770 5650 +f 770 771 5918 +f 770 5918 832 +f 770 832 5650 +f 771 772 5918 +f 772 5232 773 +f 772 835 834 +f 772 773 835 +f 772 834 5918 +f 773 774 836 +f 773 836 835 +f 773 5232 4997 +f 773 4997 774 +f 774 5708 775 +f 774 775 836 +f 774 4997 5708 +f 775 838 837 +f 775 776 838 +f 775 837 836 +f 775 5708 776 +f 776 5764 838 +f 776 777 5764 +f 777 778 5834 +f 777 5834 5764 +f 778 779 5834 +f 779 780 841 +f 779 5149 780 +f 779 841 5834 +f 780 842 841 +f 780 781 843 +f 780 843 842 +f 780 5149 781 +f 781 782 5356 +f 781 5356 843 +f 782 783 5356 +f 783 845 5356 +f 783 846 845 +f 783 784 846 +f 784 785 846 +f 785 847 846 +f 785 786 848 +f 785 848 847 +f 786 787 848 +f 787 5601 849 +f 787 849 848 +f 787 6054 788 +f 787 788 5601 +f 788 6054 789 +f 788 789 5601 +f 789 5026 5601 +f 789 6049 790 +f 789 6054 6049 +f 789 790 5026 +f 790 791 5645 +f 790 5645 5026 +f 791 792 5499 +f 791 5499 5645 +f 792 854 5499 +f 792 793 854 +f 793 856 855 +f 793 855 854 +f 793 794 856 +f 794 857 856 +f 794 795 857 +f 795 796 857 +f 796 859 858 +f 796 797 859 +f 796 858 857 +f 797 798 860 +f 797 860 859 +f 798 861 860 +f 798 799 861 +f 799 800 862 +f 799 862 861 +f 800 863 862 +f 800 801 863 +f 801 802 863 +f 801 5593 802 +f 802 864 863 +f 802 803 864 +f 803 5598 4920 +f 803 4920 865 +f 803 865 864 +f 804 4627 4626 +f 804 866 5598 +f 804 4626 4625 +f 804 4625 866 +f 805 867 4626 +f 805 4558 4559 +f 805 4559 867 +f 806 4456 4445 +f 806 4445 868 +f 806 4560 4559 +f 806 868 4560 +f 806 4939 4456 +f 807 5483 869 +f 807 4214 5483 +f 807 869 4759 +f 807 4211 4214 +f 808 870 4943 +f 808 4943 4940 +f 808 4940 4941 +f 809 5012 871 +f 810 4708 4709 +f 810 4709 872 +f 810 4707 4708 +f 810 872 5183 +f 811 5419 5847 +f 811 4361 5432 +f 811 5847 873 +f 811 873 4361 +f 812 5824 4496 +f 812 4496 4495 +f 812 4850 4851 +f 812 4851 4852 +f 812 4852 874 +f 812 874 5824 +f 813 875 4851 +f 813 876 875 +f 813 814 876 +f 814 815 877 +f 814 877 876 +f 815 816 5057 +f 815 5057 877 +f 816 817 5223 +f 816 878 5057 +f 816 5223 878 +f 817 879 5223 +f 817 818 879 +f 818 819 881 +f 818 881 880 +f 818 880 879 +f 819 820 882 +f 819 882 881 +f 820 821 882 +f 821 822 884 +f 821 883 882 +f 821 884 883 +f 822 823 885 +f 822 885 884 +f 823 824 885 +f 824 886 885 +f 824 825 886 +f 825 826 887 +f 825 887 886 +f 826 6130 827 +f 826 888 887 +f 826 889 888 +f 826 827 889 +f 827 6130 5545 +f 827 828 889 +f 827 5545 828 +f 828 891 890 +f 828 5771 829 +f 828 5545 5771 +f 828 829 891 +f 828 890 889 +f 829 5771 5744 +f 829 830 891 +f 829 5744 830 +f 830 5744 5650 +f 830 831 892 +f 830 892 891 +f 830 5650 831 +f 831 5650 832 +f 831 893 892 +f 831 832 4988 +f 831 4988 893 +f 832 5918 833 +f 832 833 4988 +f 833 5918 834 +f 833 834 896 +f 833 896 895 +f 833 895 4988 +f 834 835 896 +f 835 898 897 +f 835 897 896 +f 835 836 898 +f 836 837 898 +f 837 838 5219 +f 837 899 898 +f 837 5219 899 +f 838 4907 5219 +f 838 839 4907 +f 838 5764 839 +f 839 5764 5834 +f 839 840 4907 +f 839 5834 840 +f 840 5834 841 +f 840 902 4907 +f 840 4965 902 +f 840 841 4965 +f 841 842 4965 +f 842 904 4965 +f 842 905 904 +f 842 843 905 +f 843 844 905 +f 843 5356 844 +f 844 845 907 +f 844 5356 845 +f 844 907 906 +f 844 906 905 +f 845 846 907 +f 846 847 5583 +f 846 908 907 +f 846 5583 908 +f 847 848 5583 +f 848 911 910 +f 848 849 911 +f 848 910 5583 +f 849 912 911 +f 849 850 912 +f 849 5601 850 +f 850 5601 851 +f 850 5330 912 +f 850 851 5330 +f 851 5026 852 +f 851 5601 5026 +f 851 852 5330 +f 852 853 914 +f 852 914 5330 +f 852 5645 853 +f 852 5026 5645 +f 853 5499 854 +f 853 916 915 +f 853 854 916 +f 853 915 914 +f 853 5645 5499 +f 854 855 917 +f 854 917 916 +f 855 918 917 +f 855 856 918 +f 856 857 918 +f 857 919 918 +f 857 858 919 +f 858 921 920 +f 858 859 921 +f 858 920 919 +f 859 860 921 +f 860 861 922 +f 860 922 921 +f 861 924 923 +f 861 862 924 +f 861 923 922 +f 862 925 924 +f 862 863 925 +f 863 864 5448 +f 863 5448 925 +f 864 5602 926 +f 864 926 5448 +f 864 865 5602 +f 865 927 5602 +f 865 4920 927 +f 866 4920 5598 +f 866 928 4920 +f 866 4625 4624 +f 866 4624 928 +f 867 4625 4626 +f 867 929 4625 +f 867 4559 4560 +f 867 4560 929 +f 868 4445 930 +f 868 4561 4560 +f 868 930 4561 +f 869 931 4758 +f 869 4758 4759 +f 869 5483 931 +f 870 4944 4943 +f 870 932 4944 +f 871 5012 5013 +f 871 5013 933 +f 872 4028 5183 +f 872 4710 934 +f 872 4709 4710 +f 872 934 4034 +f 872 4034 4028 +f 873 4412 4361 +f 873 5847 935 +f 873 4362 4412 +f 873 935 4362 +f 874 4852 4853 +f 874 4853 936 +f 874 936 5824 +f 875 937 4852 +f 875 4852 4851 +f 875 876 937 +f 876 877 938 +f 876 938 937 +f 877 939 938 +f 877 5057 939 +f 878 5223 940 +f 878 940 5057 +f 879 880 942 +f 879 942 941 +f 879 941 5223 +f 880 881 942 +f 881 944 943 +f 881 882 944 +f 881 943 942 +f 882 883 944 +f 883 945 944 +f 883 884 945 +f 884 946 945 +f 884 885 946 +f 885 886 947 +f 885 947 946 +f 886 949 948 +f 886 887 949 +f 886 948 947 +f 887 888 949 +f 888 951 950 +f 888 889 951 +f 888 950 949 +f 889 890 952 +f 889 952 951 +f 890 891 952 +f 891 953 952 +f 891 892 953 +f 892 893 955 +f 892 955 954 +f 892 954 953 +f 893 894 955 +f 893 4988 894 +f 894 895 5075 +f 894 4988 895 +f 894 956 955 +f 894 5075 956 +f 895 5553 5075 +f 895 896 5553 +f 896 5279 5553 +f 896 897 5279 +f 897 898 5571 +f 897 5571 5279 +f 898 899 5571 +f 899 961 5571 +f 899 900 962 +f 899 5219 900 +f 899 962 961 +f 900 5219 4907 +f 900 963 962 +f 900 901 963 +f 900 4907 901 +f 901 902 5008 +f 901 4907 902 +f 901 5008 963 +f 902 4965 903 +f 902 903 5008 +f 903 5035 5008 +f 903 904 5035 +f 903 4965 904 +f 904 967 966 +f 904 905 967 +f 904 966 5035 +f 905 968 967 +f 905 906 968 +f 906 907 6023 +f 906 6023 968 +f 907 908 6023 +f 908 6076 6023 +f 908 5583 909 +f 908 909 6076 +f 909 5644 6076 +f 909 5583 910 +f 909 910 5644 +f 910 911 5412 +f 910 5412 5644 +f 911 912 974 +f 911 974 973 +f 911 973 5412 +f 912 5469 974 +f 912 5330 913 +f 912 913 5469 +f 913 5330 914 +f 913 914 5469 +f 914 5666 5469 +f 914 915 5666 +f 915 916 5095 +f 915 5095 5666 +f 916 978 5095 +f 916 917 978 +f 917 980 979 +f 917 979 978 +f 917 918 980 +f 918 981 980 +f 918 919 981 +f 919 920 982 +f 919 982 981 +f 920 983 982 +f 920 921 983 +f 921 922 983 +f 922 984 983 +f 922 923 984 +f 923 985 984 +f 923 924 985 +f 924 986 985 +f 924 925 986 +f 925 987 986 +f 925 5448 987 +f 926 5602 988 +f 926 988 5448 +f 927 989 5602 +f 927 928 5135 +f 927 5135 989 +f 927 4920 928 +f 928 4624 4623 +f 928 4623 990 +f 928 990 5135 +f 929 4560 4561 +f 929 4624 4625 +f 929 4561 991 +f 929 991 4624 +f 930 4457 4444 +f 930 4445 4457 +f 930 992 4562 +f 930 4562 4561 +f 930 4444 992 +f 931 4757 4758 +f 931 993 4757 +f 931 5483 4218 +f 931 5625 993 +f 931 4218 5625 +f 932 994 4944 +f 933 5245 995 +f 933 4033 5245 +f 933 5013 4033 +f 934 4711 996 +f 934 4710 4711 +f 934 996 4034 +f 935 4498 4499 +f 935 997 4362 +f 935 4499 997 +f 935 5847 4498 +f 936 4853 4854 +f 936 998 4498 +f 936 4854 998 +f 936 4498 5824 +f 937 938 1000 +f 937 1000 999 +f 937 4853 4852 +f 937 999 4853 +f 938 1001 1000 +f 938 939 1001 +f 939 5057 940 +f 939 940 1001 +f 940 1002 1001 +f 940 5223 941 +f 940 941 1003 +f 940 1003 1002 +f 941 5634 1003 +f 941 942 5634 +f 942 1004 5634 +f 942 943 1004 +f 943 944 1005 +f 943 1005 1004 +f 944 1006 1005 +f 944 1007 1006 +f 944 945 1007 +f 945 1008 1007 +f 945 946 1008 +f 946 947 1009 +f 946 1009 1008 +f 947 948 1009 +f 948 949 1011 +f 948 1011 1010 +f 948 1010 1009 +f 949 950 1011 +f 950 1012 1011 +f 950 951 1012 +f 951 952 1014 +f 951 1013 1012 +f 951 1014 1013 +f 952 1015 1014 +f 952 953 1015 +f 953 1016 1015 +f 953 954 1016 +f 954 955 5256 +f 954 5256 1016 +f 955 956 5256 +f 956 1018 5256 +f 956 1019 1018 +f 956 957 1019 +f 956 5075 957 +f 957 958 1020 +f 957 5075 5553 +f 957 5553 958 +f 957 1020 1019 +f 958 5279 959 +f 958 5553 5279 +f 958 959 1021 +f 958 1021 1020 +f 959 5571 960 +f 959 5279 5571 +f 959 5343 1021 +f 959 960 5343 +f 960 5571 961 +f 960 5651 5343 +f 960 961 5651 +f 961 962 5326 +f 961 5326 5651 +f 962 963 4924 +f 962 4924 5326 +f 963 5122 4924 +f 963 964 5122 +f 963 5008 964 +f 964 965 5122 +f 964 5008 965 +f 965 5525 5122 +f 965 966 5525 +f 965 5008 5035 +f 965 5035 966 +f 966 967 4884 +f 966 4884 5525 +f 967 968 4989 +f 967 4989 4884 +f 968 6023 969 +f 968 969 5488 +f 968 5488 1030 +f 968 1030 4989 +f 969 6023 970 +f 969 970 5488 +f 970 6076 971 +f 970 6023 6076 +f 970 971 1032 +f 970 1032 5488 +f 971 6076 5644 +f 971 1033 1032 +f 971 5309 1033 +f 971 972 5309 +f 971 5644 972 +f 972 973 5309 +f 972 5644 5412 +f 972 5412 973 +f 973 1035 5309 +f 973 1036 1035 +f 973 974 1036 +f 974 5469 975 +f 974 975 1036 +f 975 1037 1036 +f 975 976 1037 +f 975 5469 976 +f 976 1038 1037 +f 976 977 1039 +f 976 1039 1038 +f 976 5469 5666 +f 976 5666 977 +f 977 1040 1039 +f 977 978 1040 +f 977 5666 5095 +f 977 5095 978 +f 978 979 1040 +f 979 1041 1040 +f 979 980 1042 +f 979 1042 1041 +f 980 981 1043 +f 980 1043 1042 +f 981 982 1043 +f 982 1044 1043 +f 982 983 1044 +f 983 1045 1044 +f 983 984 1045 +f 984 1047 1046 +f 984 1046 1045 +f 984 985 1047 +f 985 986 1048 +f 985 1048 1047 +f 986 987 1048 +f 987 1049 1048 +f 987 5448 988 +f 987 988 1050 +f 987 1050 1049 +f 988 5602 4996 +f 988 4996 1050 +f 989 1051 4996 +f 989 4996 5602 +f 989 5135 1051 +f 990 1052 5135 +f 990 4623 1052 +f 991 1053 4622 +f 991 4562 1053 +f 991 4622 4623 +f 991 4561 4562 +f 991 4623 4624 +f 992 4563 4562 +f 992 1054 4563 +f 992 4444 1054 +f 993 5625 1055 +f 993 4756 4757 +f 993 1055 4755 +f 993 4755 4756 +f 994 4228 4944 +f 994 4813 4228 +f 994 1056 4813 +f 995 5245 5244 +f 995 5244 1057 +f 996 4711 1058 +f 996 1058 4035 +f 996 4035 4034 +f 997 4500 1059 +f 997 4499 4500 +f 997 4411 4362 +f 997 4363 4411 +f 997 1059 4363 +f 998 4499 4498 +f 998 4854 1060 +f 998 1060 4499 +f 999 4855 4854 +f 999 1061 4855 +f 999 4854 4853 +f 999 1000 5733 +f 999 5733 1061 +f 1000 1001 1062 +f 1000 1062 5733 +f 1001 1063 1062 +f 1001 1002 1064 +f 1001 1064 1063 +f 1002 1003 1064 +f 1003 5634 1065 +f 1003 1065 1064 +f 1004 1066 5634 +f 1004 1005 1067 +f 1004 1067 1066 +f 1005 1006 1068 +f 1005 1068 1067 +f 1006 1007 1068 +f 1007 1069 1068 +f 1007 1008 1069 +f 1008 1009 1071 +f 1008 1070 1069 +f 1008 1071 1070 +f 1009 1010 1072 +f 1009 1072 1071 +f 1010 1011 1072 +f 1011 1012 1073 +f 1011 1073 1072 +f 1012 1074 1073 +f 1012 1013 1075 +f 1012 1075 1074 +f 1013 1014 1075 +f 1014 1076 1075 +f 1014 5142 1076 +f 1014 1015 5142 +f 1015 1016 5142 +f 1016 1017 1079 +f 1016 1079 1078 +f 1016 1078 5142 +f 1016 5256 1017 +f 1017 1018 1079 +f 1017 5256 1018 +f 1018 1080 1079 +f 1018 1019 1080 +f 1019 1020 1081 +f 1019 1081 1080 +f 1020 5369 1082 +f 1020 1021 5369 +f 1020 1082 1081 +f 1021 5343 1022 +f 1021 5689 5369 +f 1021 1022 5689 +f 1022 5651 1023 +f 1022 1023 5590 +f 1022 5343 5651 +f 1022 5590 5689 +f 1023 5589 5590 +f 1023 5651 5589 +f 1024 5224 5589 +f 1024 5326 4924 +f 1024 1025 5224 +f 1024 5589 5326 +f 1024 4924 1025 +f 1025 4924 5122 +f 1025 5122 1026 +f 1025 1087 5224 +f 1025 1088 1087 +f 1025 1026 1088 +f 1026 1027 5932 +f 1026 5122 1027 +f 1026 5932 1088 +f 1027 6012 5932 +f 1027 5525 6012 +f 1027 5122 5525 +f 1028 5691 6012 +f 1028 5690 5691 +f 1028 4884 5690 +f 1028 6012 4884 +f 1029 5374 5690 +f 1029 4989 1030 +f 1029 5690 4989 +f 1029 1030 5374 +f 1030 1092 5374 +f 1030 5488 1031 +f 1030 1093 1092 +f 1030 1031 1093 +f 1031 5488 1032 +f 1031 1094 1093 +f 1031 1032 1094 +f 1032 1033 5478 +f 1032 5478 1094 +f 1033 1034 5478 +f 1033 5309 1034 +f 1034 1035 1096 +f 1034 1096 5478 +f 1034 5309 1035 +f 1035 1097 1096 +f 1035 1036 1097 +f 1036 1099 1098 +f 1036 1037 1099 +f 1036 1098 1097 +f 1037 1038 5207 +f 1037 5207 1099 +f 1038 1039 5207 +f 1039 1040 1102 +f 1039 1102 1101 +f 1039 1101 5207 +f 1040 1041 1103 +f 1040 1103 1102 +f 1041 1042 1103 +f 1042 1104 1103 +f 1042 1043 1104 +f 1043 5740 1105 +f 1043 1044 5740 +f 1043 1105 1104 +f 1044 1045 5740 +f 1045 1107 5740 +f 1045 1108 1107 +f 1045 1046 1108 +f 1046 6036 5992 +f 1046 5992 1108 +f 1046 1047 6036 +f 1047 1048 6036 +f 1048 1110 6036 +f 1048 1049 1111 +f 1048 1111 1110 +f 1049 1050 1111 +f 1050 1051 1113 +f 1050 1113 1112 +f 1050 4996 1051 +f 1050 1112 1111 +f 1051 1114 1113 +f 1051 1052 1114 +f 1051 5135 1052 +f 1052 4622 4621 +f 1052 4621 1114 +f 1052 4623 4622 +f 1053 4563 1115 +f 1053 4562 4563 +f 1053 1115 4622 +f 1054 4564 4563 +f 1054 1116 4564 +f 1054 4444 4458 +f 1054 4443 1116 +f 1054 4458 4443 +f 1055 5888 1117 +f 1055 5625 5888 +f 1055 1117 4755 +f 1056 1118 4813 +f 1057 5244 1119 +f 1058 4711 4712 +f 1058 4036 4035 +f 1058 5433 4036 +f 1058 1120 5433 +f 1058 4713 1120 +f 1058 4712 4713 +f 1059 4501 1121 +f 1059 1121 4363 +f 1059 4500 4501 +f 1060 4854 4855 +f 1060 4856 1122 +f 1060 4855 4856 +f 1060 4500 4499 +f 1060 1122 4500 +f 1061 5733 1123 +f 1061 1123 4855 +f 1062 1125 1124 +f 1062 1063 1125 +f 1062 1124 5733 +f 1063 1064 1125 +f 1064 1065 1126 +f 1064 1126 1125 +f 1065 1066 1127 +f 1065 5634 1066 +f 1065 1127 1126 +f 1066 1128 1127 +f 1066 1129 1128 +f 1066 1067 1129 +f 1067 1068 1129 +f 1068 1131 1130 +f 1068 1130 1129 +f 1068 1069 1131 +f 1069 1132 1131 +f 1069 1070 1132 +f 1070 1133 1132 +f 1070 1071 1133 +f 1071 1072 1134 +f 1071 1134 1133 +f 1072 1073 1134 +f 1073 1136 1135 +f 1073 1074 1136 +f 1073 1135 1134 +f 1074 1075 5205 +f 1074 5205 1136 +f 1075 1076 5205 +f 1076 1077 5524 +f 1076 5524 1138 +f 1076 1138 5205 +f 1076 5142 1077 +f 1077 1078 5524 +f 1077 5142 1078 +f 1078 1140 5524 +f 1078 1079 5325 +f 1078 5325 1140 +f 1079 1080 5325 +f 1080 1081 1142 +f 1080 1142 5325 +f 1081 1144 1143 +f 1081 1082 1144 +f 1081 1143 1142 +f 1082 1083 1145 +f 1082 5369 1083 +f 1082 1145 1144 +f 1083 1084 1146 +f 1083 5689 1084 +f 1083 5369 5689 +f 1083 1146 1145 +f 1084 5426 1146 +f 1084 1085 5426 +f 1084 5689 5590 +f 1084 5590 1085 +f 1085 5224 1086 +f 1085 5590 5224 +f 1085 1086 5426 +f 1086 1148 5426 +f 1086 1087 1148 +f 1086 5224 1087 +f 1087 1088 6021 +f 1087 6021 1149 +f 1087 1149 1148 +f 1088 1089 6021 +f 1088 5932 1089 +f 1089 1090 6089 +f 1089 5932 1090 +f 1089 6089 6021 +f 1090 5932 5691 +f 1090 1091 5561 +f 1090 5561 6089 +f 1090 5691 1091 +f 1091 1153 5561 +f 1091 5691 5374 +f 1091 1092 1153 +f 1091 5374 1092 +f 1092 5755 1154 +f 1092 1154 1153 +f 1092 1093 5755 +f 1093 1094 5755 +f 1094 1156 5755 +f 1094 1095 1157 +f 1094 1157 1156 +f 1094 5478 1095 +f 1095 5478 1096 +f 1095 1096 5138 +f 1095 5138 1157 +f 1096 1097 5138 +f 1097 1098 1159 +f 1097 1159 5138 +f 1098 1099 1160 +f 1098 1160 1159 +f 1099 1100 1162 +f 1099 1162 1161 +f 1099 1161 1160 +f 1099 5207 1100 +f 1100 5775 1162 +f 1100 1101 5775 +f 1100 5207 1101 +f 1101 1102 5775 +f 1102 1164 5775 +f 1102 1103 1165 +f 1102 1165 1164 +f 1103 1104 1165 +f 1104 1105 1166 +f 1104 1166 1165 +f 1105 5740 1106 +f 1105 5319 1167 +f 1105 1167 1166 +f 1105 1106 5319 +f 1106 5740 1107 +f 1106 1107 5319 +f 1107 1169 5319 +f 1107 1108 1169 +f 1108 5992 1170 +f 1108 1170 1169 +f 1109 5979 5992 +f 1109 6036 1110 +f 1109 1110 5979 +f 1109 5992 6036 +f 1110 1172 5781 +f 1110 5781 5979 +f 1110 1111 1173 +f 1110 1173 1172 +f 1111 5366 1173 +f 1111 1112 5366 +f 1112 1113 1174 +f 1112 1174 5366 +f 1113 1114 1175 +f 1113 1175 1174 +f 1114 4620 1176 +f 1114 1176 1175 +f 1114 4621 4620 +f 1115 4563 4564 +f 1115 1177 4621 +f 1115 4621 4622 +f 1115 4564 1177 +f 1116 5213 1178 +f 1116 4443 5213 +f 1116 1178 4565 +f 1116 4565 4564 +f 1117 5888 4227 +f 1117 1179 4754 +f 1117 4754 4755 +f 1117 4227 1179 +f 1118 1180 4810 +f 1118 4810 4809 +f 1118 4809 4813 +f 1119 5244 5243 +f 1119 5243 5242 +f 1119 5242 1181 +f 1120 4714 1182 +f 1120 4713 4714 +f 1120 1182 5433 +f 1121 4502 1183 +f 1121 1183 5416 +f 1121 4410 4363 +f 1121 5416 4410 +f 1121 4501 4502 +f 1122 5917 4501 +f 1122 4856 4857 +f 1122 4857 1184 +f 1122 4501 4500 +f 1122 1184 5917 +f 1123 4856 4855 +f 1123 5733 1124 +f 1123 1185 5431 +f 1123 5431 4856 +f 1123 1124 1185 +f 1124 1125 1186 +f 1124 1186 1185 +f 1125 1188 1187 +f 1125 1187 1186 +f 1125 1126 1188 +f 1126 1189 1188 +f 1126 1127 1189 +f 1127 1128 1190 +f 1127 1190 1189 +f 1128 1129 1190 +f 1129 1191 1190 +f 1129 1130 1191 +f 1130 1131 1193 +f 1130 1193 1192 +f 1130 1192 1191 +f 1131 1132 1193 +f 1132 1133 1194 +f 1132 1194 1193 +f 1133 1195 1194 +f 1133 1196 1195 +f 1133 1134 1196 +f 1134 1197 1196 +f 1134 1135 1197 +f 1135 1198 1197 +f 1135 1136 1198 +f 1136 5205 1137 +f 1136 5071 1198 +f 1136 1137 5071 +f 1137 1138 5071 +f 1137 5205 1138 +f 1138 1139 1200 +f 1138 1200 5071 +f 1138 5524 1139 +f 1139 5524 1140 +f 1139 5203 1201 +f 1139 1201 1200 +f 1139 1140 5203 +f 1140 1141 5203 +f 1140 5325 1141 +f 1141 1142 1204 +f 1141 1204 1203 +f 1141 1203 5203 +f 1141 5325 1142 +f 1142 5221 1204 +f 1142 1143 5221 +f 1143 1144 5221 +f 1144 1145 4614 +f 1144 1206 5221 +f 1144 4614 1206 +f 1145 1146 4614 +f 1146 5426 1147 +f 1146 1208 4614 +f 1146 1147 1209 +f 1146 1209 1208 +f 1147 5426 1148 +f 1147 1148 1209 +f 1148 6024 1210 +f 1148 1149 6024 +f 1148 1210 1209 +f 1149 1150 6024 +f 1149 6021 1150 +f 1150 6028 1212 +f 1150 1151 6028 +f 1150 1212 6024 +f 1150 6021 1151 +f 1151 1152 6028 +f 1151 6089 1152 +f 1151 6021 6089 +f 1152 6067 6028 +f 1152 1153 6067 +f 1152 6089 5561 +f 1152 5561 1153 +f 1153 6068 6067 +f 1153 1154 6068 +f 1154 5766 1216 +f 1154 1155 5766 +f 1154 5755 1155 +f 1154 1216 6068 +f 1155 1156 5766 +f 1155 5755 1156 +f 1156 5056 1218 +f 1156 1218 5766 +f 1156 1157 5056 +f 1157 1158 5056 +f 1157 5138 1158 +f 1158 1159 1220 +f 1158 5138 1159 +f 1158 1220 5056 +f 1159 1160 1221 +f 1159 1221 1220 +f 1160 1222 1221 +f 1160 1161 1223 +f 1160 1223 1222 +f 1161 1162 1223 +f 1162 5315 1224 +f 1162 1163 5315 +f 1162 1224 1223 +f 1162 5775 1163 +f 1163 1164 5315 +f 1163 5775 1164 +f 1164 1226 5315 +f 1164 1165 1226 +f 1165 1227 1226 +f 1165 1166 1227 +f 1166 1228 1227 +f 1166 1229 1228 +f 1166 1167 1229 +f 1167 1230 1229 +f 1167 1168 1230 +f 1167 5319 1168 +f 1168 5319 1169 +f 1168 1169 1230 +f 1169 1232 1231 +f 1169 1170 1232 +f 1169 1231 1230 +f 1170 5992 5971 +f 1170 5971 1232 +f 1171 1233 5971 +f 1171 5979 5781 +f 1171 5971 5979 +f 1171 5781 1233 +f 1172 1173 1234 +f 1172 1234 5781 +f 1173 5366 1235 +f 1173 1235 1234 +f 1174 1175 1237 +f 1174 1236 5366 +f 1174 1237 1236 +f 1175 1176 1238 +f 1175 1238 1237 +f 1176 4619 1238 +f 1176 4620 4619 +f 1177 4565 1239 +f 1177 1239 4620 +f 1177 4620 4621 +f 1177 4564 4565 +f 1178 5213 1240 +f 1178 4566 4565 +f 1178 1240 4566 +f 1179 4753 4754 +f 1179 1241 4753 +f 1179 4230 1241 +f 1179 4227 4230 +f 1180 4811 4810 +f 1180 1242 4811 +f 1181 5242 5241 +f 1181 5241 1243 +f 1182 1244 4043 +f 1182 4715 1244 +f 1182 4714 4715 +f 1182 4043 5433 +f 1183 4503 1245 +f 1183 4502 4503 +f 1183 1245 5416 +f 1184 1246 5917 +f 1184 4858 1246 +f 1184 4857 4858 +f 1185 1186 6001 +f 1185 6001 1247 +f 1185 1247 5431 +f 1186 1248 6001 +f 1186 1187 1248 +f 1187 1249 1248 +f 1187 1188 1249 +f 1188 1189 1251 +f 1188 1251 1250 +f 1188 1250 1249 +f 1189 1252 1251 +f 1189 1190 1252 +f 1190 1191 1253 +f 1190 1253 1252 +f 1191 1192 1254 +f 1191 1254 1253 +f 1192 1193 1255 +f 1192 1255 1254 +f 1193 1194 1255 +f 1194 1256 1255 +f 1194 1195 1257 +f 1194 1257 1256 +f 1195 1196 1258 +f 1195 1258 1257 +f 1196 1197 1258 +f 1197 1259 1258 +f 1197 1198 1260 +f 1197 1260 1259 +f 1198 5071 1199 +f 1198 1199 1260 +f 1199 5071 1200 +f 1199 1261 1260 +f 1199 1262 1261 +f 1199 1200 1262 +f 1200 1201 1262 +f 1201 5203 1202 +f 1201 1263 1262 +f 1201 1202 1263 +f 1202 5203 1203 +f 1202 1203 1264 +f 1202 1264 1263 +f 1203 1265 1264 +f 1203 1266 1265 +f 1203 1204 1266 +f 1204 5746 1266 +f 1204 5221 1205 +f 1204 1205 5746 +f 1205 1206 5746 +f 1205 5221 1206 +f 1206 1268 5746 +f 1206 1207 1268 +f 1206 4614 1207 +f 1207 1208 1269 +f 1207 1269 1268 +f 1207 4614 1208 +f 1208 1209 5693 +f 1208 1270 1269 +f 1208 5693 1270 +f 1209 1210 5693 +f 1210 6024 1211 +f 1210 1272 5693 +f 1210 1273 1272 +f 1210 1211 1273 +f 1211 6024 1212 +f 1211 1274 1273 +f 1211 1212 1274 +f 1212 6028 1213 +f 1212 1213 1274 +f 1213 1214 5840 +f 1213 5840 1275 +f 1213 1275 1274 +f 1213 6028 1214 +f 1214 6067 1215 +f 1214 1215 5840 +f 1214 6028 6067 +f 1215 6067 6068 +f 1215 1277 5840 +f 1215 6068 1216 +f 1215 1216 4808 +f 1215 4808 1277 +f 1216 5766 1217 +f 1216 1217 4808 +f 1217 5766 1218 +f 1217 5146 1279 +f 1217 1218 5146 +f 1217 1279 4808 +f 1218 5056 1219 +f 1218 1219 5146 +f 1219 5056 1220 +f 1219 1281 5146 +f 1219 4985 1281 +f 1219 1220 4985 +f 1220 1221 4985 +f 1221 1222 1284 +f 1221 1283 4985 +f 1221 1284 1283 +f 1222 1223 1285 +f 1222 1285 1284 +f 1223 1224 1285 +f 1224 1225 5556 +f 1224 5556 1286 +f 1224 1286 1285 +f 1224 5315 1225 +f 1225 1226 5556 +f 1225 5315 1226 +f 1226 1288 5556 +f 1226 1227 1289 +f 1226 1289 1288 +f 1227 1228 1289 +f 1228 1229 1290 +f 1228 1290 1289 +f 1229 1291 1290 +f 1229 1292 1291 +f 1229 1230 1292 +f 1230 1231 1292 +f 1231 1293 1292 +f 1231 1232 1293 +f 1232 5971 1233 +f 1232 1294 1293 +f 1232 1233 1294 +f 1233 5781 1234 +f 1233 1234 1295 +f 1233 1295 1294 +f 1234 1235 1296 +f 1234 1296 1295 +f 1235 1298 1297 +f 1235 5366 1236 +f 1235 1236 1298 +f 1235 1297 1296 +f 1236 1299 1298 +f 1236 1237 1299 +f 1237 1238 1299 +f 1238 4619 1300 +f 1238 1300 1299 +f 1239 4565 4566 +f 1239 1301 4618 +f 1239 4618 4619 +f 1239 4566 1301 +f 1239 4619 4620 +f 1240 5213 4442 +f 1240 1302 4567 +f 1240 4567 4566 +f 1240 4442 1302 +f 1241 4752 4753 +f 1241 4233 1303 +f 1241 1303 4751 +f 1241 4751 4752 +f 1241 4230 4233 +f 1242 1304 4811 +f 1243 5241 1305 +f 1244 4715 1306 +f 1244 4044 4043 +f 1244 4050 4044 +f 1244 1306 4050 +f 1245 1307 4365 +f 1245 4365 4409 +f 1245 4503 4504 +f 1245 4504 1307 +f 1245 4409 5416 +f 1246 4503 5917 +f 1246 4858 1308 +f 1246 1308 4503 +f 1247 6001 1309 +f 1247 4859 4858 +f 1247 1309 4859 +f 1247 4858 5431 +f 1248 1310 6001 +f 1248 1311 1310 +f 1248 1249 1311 +f 1249 5297 1311 +f 1249 1250 5297 +f 1250 1312 5297 +f 1250 1313 1312 +f 1250 1251 1313 +f 1251 1252 1313 +f 1252 1314 1313 +f 1252 1315 1314 +f 1252 1253 1315 +f 1253 1254 1315 +f 1254 1317 1316 +f 1254 1255 1317 +f 1254 1316 1315 +f 1255 1256 1317 +f 1256 1318 1317 +f 1256 1257 1319 +f 1256 1319 1318 +f 1257 1258 1319 +f 1258 1320 1319 +f 1258 1259 1320 +f 1259 1322 1321 +f 1259 1260 1322 +f 1259 1321 1320 +f 1260 1261 1322 +f 1261 1262 1323 +f 1261 1323 1322 +f 1262 1263 1324 +f 1262 1324 1323 +f 1263 1326 1325 +f 1263 1325 1324 +f 1263 1264 1326 +f 1264 1265 1326 +f 1265 1327 1326 +f 1265 1266 1327 +f 1266 5746 1267 +f 1266 1328 1327 +f 1266 1329 1328 +f 1266 1267 1329 +f 1267 1330 1329 +f 1267 5746 1268 +f 1267 1268 1330 +f 1268 1269 5061 +f 1268 5061 1330 +f 1269 1270 5061 +f 1270 1332 5061 +f 1270 1271 1332 +f 1270 5693 1271 +f 1271 5693 1272 +f 1271 1272 1334 +f 1271 1334 1333 +f 1271 1333 1332 +f 1272 5943 1334 +f 1272 1273 5943 +f 1273 1274 5943 +f 1274 1275 1336 +f 1274 1336 5943 +f 1275 1337 1336 +f 1275 5700 1337 +f 1275 5840 1276 +f 1275 1276 5700 +f 1276 1277 5700 +f 1276 5840 1277 +f 1277 5295 1339 +f 1277 4808 1278 +f 1277 1278 5295 +f 1277 1339 5700 +f 1278 4808 1279 +f 1278 1279 5295 +f 1279 5721 1341 +f 1279 1280 5721 +f 1279 1341 5295 +f 1279 5146 1280 +f 1280 1281 5721 +f 1280 5146 1281 +f 1281 5484 5721 +f 1281 1282 5484 +f 1281 4985 1282 +f 1282 1283 5198 +f 1282 4985 1283 +f 1282 5198 1344 +f 1282 1344 5484 +f 1283 1284 5198 +f 1284 1285 1347 +f 1284 1346 5198 +f 1284 1347 1346 +f 1285 1286 1348 +f 1285 1348 1347 +f 1286 1287 5023 +f 1286 5556 1287 +f 1286 5023 1348 +f 1287 5556 1288 +f 1287 1288 5023 +f 1288 5536 5023 +f 1288 1289 5536 +f 1289 1290 1351 +f 1289 1351 5536 +f 1290 1291 1352 +f 1290 1352 1351 +f 1291 1353 1352 +f 1291 1292 1354 +f 1291 1354 1353 +f 1292 1293 1354 +f 1293 1294 1356 +f 1293 1355 1354 +f 1293 1356 1355 +f 1294 1295 1356 +f 1295 1357 1356 +f 1295 1296 1358 +f 1295 1358 1357 +f 1296 1297 1358 +f 1297 1298 1360 +f 1297 1360 1359 +f 1297 1359 1358 +f 1298 1299 5668 +f 1298 5668 1360 +f 1299 1361 5668 +f 1299 1300 1361 +f 1300 4618 5864 +f 1300 5864 1362 +f 1300 4619 4618 +f 1300 1362 1361 +f 1301 1363 4618 +f 1301 5312 1363 +f 1301 4567 5312 +f 1301 4566 4567 +f 1302 4460 4441 +f 1302 4441 1364 +f 1302 1364 4568 +f 1302 4442 4460 +f 1302 4568 4567 +f 1303 4233 4234 +f 1303 4241 1365 +f 1303 4234 4241 +f 1303 1365 4751 +f 1304 4812 4811 +f 1304 1366 4812 +f 1305 4049 5130 +f 1305 5130 1367 +f 1305 5241 4049 +f 1306 4717 1368 +f 1306 4716 4717 +f 1306 4715 4716 +f 1306 1368 4050 +f 1307 5363 1369 +f 1307 4504 5363 +f 1307 1369 4365 +f 1308 4858 4859 +f 1308 4860 1370 +f 1308 4859 4860 +f 1308 4504 4503 +f 1308 4505 4504 +f 1308 1370 4505 +f 1309 6001 1310 +f 1309 1310 1371 +f 1309 1371 4859 +f 1310 1372 1371 +f 1310 1311 5428 +f 1310 5428 1372 +f 1311 1373 5428 +f 1311 5297 1373 +f 1312 1375 1374 +f 1312 1313 1375 +f 1312 1374 5297 +f 1313 1314 5597 +f 1313 5597 1375 +f 1314 1376 5597 +f 1314 1315 1377 +f 1314 1377 1376 +f 1315 1378 1377 +f 1315 1316 1378 +f 1316 1317 1379 +f 1316 1379 1378 +f 1317 1318 1379 +f 1318 1381 1380 +f 1318 1319 1381 +f 1318 1380 1379 +f 1319 1320 1382 +f 1319 1382 1381 +f 1320 1321 1382 +f 1321 1384 1383 +f 1321 1322 1384 +f 1321 1383 1382 +f 1322 1385 1384 +f 1322 1323 1385 +f 1323 1324 1386 +f 1323 1386 1385 +f 1324 1325 1387 +f 1324 1387 1386 +f 1325 1326 1387 +f 1326 1327 1388 +f 1326 1388 1387 +f 1327 1328 5024 +f 1327 1389 1388 +f 1327 5024 1389 +f 1328 1329 5024 +f 1329 1391 5024 +f 1329 1330 1392 +f 1329 1392 1391 +f 1330 5061 1331 +f 1330 1331 1392 +f 1331 5061 1332 +f 1331 1393 1392 +f 1331 1332 5557 +f 1331 5557 1393 +f 1332 1333 5557 +f 1333 1395 5557 +f 1333 5073 1395 +f 1333 1334 5073 +f 1334 5943 1335 +f 1334 1335 5073 +f 1335 5943 1336 +f 1335 5062 1397 +f 1335 1397 5073 +f 1335 1336 5062 +f 1336 1337 5062 +f 1337 5331 5062 +f 1337 5700 1338 +f 1337 1338 5331 +f 1338 5700 1339 +f 1338 1339 5513 +f 1338 5513 5331 +f 1339 1402 1401 +f 1339 1401 5513 +f 1339 1340 1402 +f 1339 5295 1340 +f 1340 1341 5168 +f 1340 5295 1341 +f 1340 5168 1402 +f 1341 1342 5168 +f 1341 5721 1342 +f 1342 1343 5852 +f 1342 5852 5168 +f 1342 5721 1343 +f 1343 1405 5852 +f 1343 1344 5538 +f 1343 5721 5484 +f 1343 5538 1405 +f 1343 5484 1344 +f 1344 1345 5538 +f 1344 5198 1345 +f 1345 1407 5538 +f 1345 1346 1408 +f 1345 1408 1407 +f 1345 5198 1346 +f 1346 1347 1408 +f 1347 1409 1408 +f 1347 1348 1409 +f 1348 1349 1410 +f 1348 1410 1409 +f 1348 5023 1349 +f 1349 1350 1411 +f 1349 5023 1350 +f 1349 1411 1410 +f 1350 1412 1411 +f 1350 1413 1412 +f 1350 1351 1413 +f 1350 5536 1351 +f 1350 5023 5536 +f 1351 1414 1413 +f 1351 1352 1414 +f 1352 1353 1414 +f 1353 1415 1414 +f 1353 1416 1415 +f 1353 1354 1416 +f 1354 1355 1416 +f 1355 1356 1418 +f 1355 1417 1416 +f 1355 1418 1417 +f 1356 5858 1418 +f 1356 1357 5858 +f 1357 1419 5858 +f 1357 1420 1419 +f 1357 1358 1420 +f 1358 1359 1420 +f 1359 1360 1421 +f 1359 1421 1420 +f 1360 1422 1421 +f 1360 5668 1422 +f 1361 5614 5668 +f 1361 1362 5016 +f 1361 5016 1423 +f 1361 1423 5614 +f 1362 5864 1424 +f 1362 1424 5016 +f 1363 1425 4617 +f 1363 4617 4618 +f 1363 5312 1425 +f 1364 1426 4568 +f 1364 4441 5077 +f 1364 5077 1426 +f 1365 4241 5575 +f 1365 5575 1427 +f 1365 4750 4751 +f 1365 1427 4750 +f 1366 1428 4994 +f 1366 4244 4812 +f 1366 4994 4244 +f 1367 5129 1429 +f 1367 5130 5129 +f 1368 4717 4718 +f 1368 4718 1430 +f 1368 4051 4050 +f 1368 1430 4051 +f 1369 5363 1431 +f 1369 5049 4408 +f 1369 1431 5049 +f 1369 4408 4365 +f 1370 1432 4505 +f 1370 4860 4861 +f 1370 4861 1432 +f 1371 1433 4860 +f 1371 4860 4859 +f 1371 1372 1433 +f 1372 1434 1433 +f 1372 5428 1434 +f 1373 1435 5428 +f 1373 5297 1374 +f 1373 1374 1436 +f 1373 1436 1435 +f 1374 1375 1436 +f 1375 1437 1436 +f 1375 5597 1437 +f 1376 1438 5597 +f 1376 1439 1438 +f 1376 1377 1439 +f 1377 1440 1439 +f 1377 1378 1440 +f 1378 1441 1440 +f 1378 1379 1441 +f 1379 1380 1441 +f 1380 1442 1441 +f 1380 1381 1442 +f 1381 1444 1443 +f 1381 1382 1444 +f 1381 1443 1442 +f 1382 5875 1444 +f 1382 1383 5875 +f 1383 1384 5875 +f 1384 1446 5875 +f 1384 5405 1446 +f 1384 1385 5405 +f 1385 1386 5405 +f 1386 1387 1449 +f 1386 1449 1448 +f 1386 1448 5405 +f 1387 1388 1449 +f 1388 1389 5734 +f 1388 1450 1449 +f 1388 5734 1450 +f 1389 5024 1390 +f 1389 1390 5734 +f 1390 1452 5734 +f 1390 1391 1452 +f 1390 5024 1391 +f 1391 1453 1452 +f 1391 1454 1453 +f 1391 1392 1454 +f 1392 1393 1454 +f 1393 5557 1394 +f 1393 1455 1454 +f 1393 1394 1455 +f 1394 5557 1395 +f 1394 4613 1456 +f 1394 1456 1455 +f 1394 1395 4613 +f 1395 1396 4983 +f 1395 4983 4613 +f 1395 5073 1396 +f 1396 1397 4983 +f 1396 5073 1397 +f 1397 5062 1398 +f 1397 1459 4983 +f 1397 1398 1460 +f 1397 1460 1459 +f 1398 5062 1399 +f 1398 1399 1461 +f 1398 1461 1460 +f 1399 5808 1461 +f 1399 1400 5808 +f 1399 5062 5331 +f 1399 5331 1400 +f 1400 1401 5808 +f 1400 5513 1401 +f 1400 5331 5513 +f 1401 5862 5808 +f 1401 1402 5862 +f 1402 5168 1403 +f 1402 5473 5862 +f 1402 1403 5473 +f 1403 5168 1404 +f 1403 1404 5043 +f 1403 5043 5473 +f 1404 5168 5852 +f 1404 5852 1405 +f 1404 1466 5043 +f 1404 1467 1466 +f 1404 1405 1467 +f 1405 5538 1406 +f 1405 5763 1467 +f 1405 1406 5763 +f 1406 5538 1407 +f 1406 1407 5763 +f 1407 1469 5763 +f 1407 1470 1469 +f 1407 1408 1470 +f 1408 1409 1470 +f 1409 1410 1471 +f 1409 1471 1470 +f 1410 1473 1472 +f 1410 1411 1473 +f 1410 1472 1471 +f 1411 1474 1473 +f 1411 1412 1474 +f 1412 1413 1474 +f 1413 1475 1474 +f 1413 1414 1475 +f 1414 1476 1475 +f 1414 1477 1476 +f 1414 1415 1477 +f 1415 1416 1478 +f 1415 1478 1477 +f 1416 1417 1478 +f 1417 1418 1479 +f 1417 1479 1478 +f 1418 1480 1479 +f 1418 5858 1480 +f 1419 1481 5858 +f 1419 1482 1481 +f 1419 1420 1482 +f 1420 1483 1482 +f 1420 1421 1483 +f 1421 1422 1483 +f 1422 5614 1484 +f 1422 5668 5614 +f 1422 1484 1483 +f 1423 1485 5614 +f 1423 5016 1485 +f 1424 1486 5016 +f 1424 5864 4616 +f 1424 4615 1486 +f 1424 4616 4615 +f 1425 4616 4617 +f 1425 5312 4569 +f 1425 4569 1487 +f 1425 1487 4616 +f 1426 4569 4568 +f 1426 4570 4569 +f 1426 1488 4570 +f 1426 5077 1488 +f 1427 5575 1489 +f 1427 1489 4749 +f 1427 4749 4750 +f 1428 1490 4994 +f 1429 5129 1491 +f 1430 1492 4052 +f 1430 4718 4719 +f 1430 4719 1492 +f 1430 4052 4051 +f 1431 4507 1493 +f 1431 4506 4507 +f 1431 5363 4506 +f 1431 1493 5049 +f 1432 1494 4506 +f 1432 4506 4505 +f 1432 4862 1494 +f 1432 4861 4862 +f 1433 4861 4860 +f 1433 1495 4861 +f 1433 1434 1496 +f 1433 1496 1495 +f 1434 1497 1496 +f 1434 1435 1497 +f 1434 5428 1435 +f 1435 1436 1497 +f 1436 1498 1497 +f 1436 1499 1498 +f 1436 1437 1499 +f 1437 1500 1499 +f 1437 1438 1500 +f 1437 5597 1438 +f 1438 1439 1500 +f 1439 1440 1501 +f 1439 1501 1500 +f 1440 1441 5827 +f 1440 5827 1502 +f 1440 1502 1501 +f 1441 1442 5827 +f 1442 1504 5827 +f 1442 1443 1504 +f 1443 1505 1504 +f 1443 1444 1505 +f 1444 1445 1506 +f 1444 5875 1445 +f 1444 1506 1505 +f 1445 4995 1507 +f 1445 1446 4995 +f 1445 1507 1506 +f 1445 5875 1446 +f 1446 1447 4995 +f 1446 5405 1447 +f 1447 1509 4995 +f 1447 1448 1509 +f 1447 5405 1448 +f 1448 1449 5454 +f 1448 1510 1509 +f 1448 5454 1510 +f 1449 1450 5454 +f 1450 5734 1451 +f 1450 1451 1512 +f 1450 1512 5454 +f 1451 5734 1452 +f 1451 1452 1514 +f 1451 1514 1513 +f 1451 1513 1512 +f 1452 1453 1514 +f 1453 5404 1515 +f 1453 1515 1514 +f 1453 1454 5404 +f 1454 1455 5404 +f 1455 1517 5404 +f 1455 1518 1517 +f 1455 1456 1518 +f 1456 1457 1519 +f 1456 4613 1457 +f 1456 1519 1518 +f 1457 1458 4959 +f 1457 4959 1519 +f 1457 4613 4983 +f 1457 4983 1458 +f 1458 1459 4959 +f 1458 4983 1459 +f 1459 1521 4959 +f 1459 1460 1522 +f 1459 1522 1521 +f 1460 1523 1522 +f 1460 1461 1523 +f 1461 5808 1462 +f 1461 1462 1524 +f 1461 1524 1523 +f 1462 5808 1463 +f 1462 1525 1524 +f 1462 1463 1525 +f 1463 5862 1464 +f 1463 1464 1525 +f 1463 5808 5862 +f 1464 5862 5473 +f 1464 1526 1525 +f 1464 5473 1465 +f 1464 1527 1526 +f 1464 1465 1527 +f 1465 5473 5043 +f 1465 5043 1466 +f 1465 1466 1527 +f 1466 1467 1529 +f 1466 1528 1527 +f 1466 1529 1528 +f 1467 1468 5850 +f 1467 5850 1529 +f 1467 5763 1468 +f 1468 1469 5850 +f 1468 5763 1469 +f 1469 1470 5998 +f 1469 5998 5850 +f 1470 1471 1532 +f 1470 1532 5998 +f 1471 1472 1534 +f 1471 1534 1533 +f 1471 1533 1532 +f 1472 1473 1535 +f 1472 1535 1534 +f 1473 1474 1535 +f 1474 1475 1537 +f 1474 1537 1536 +f 1474 1536 1535 +f 1475 1476 1537 +f 1476 5594 1538 +f 1476 1477 5594 +f 1476 1538 1537 +f 1477 1478 5594 +f 1478 1479 5592 +f 1478 5592 5594 +f 1479 1480 5665 +f 1479 5665 5592 +f 1480 5877 5665 +f 1480 5858 1481 +f 1480 1481 5877 +f 1481 1482 1543 +f 1481 5920 5877 +f 1481 1543 5920 +f 1482 1483 1544 +f 1482 1544 1543 +f 1483 1546 1545 +f 1483 1484 1546 +f 1483 1545 1544 +f 1484 5614 1485 +f 1484 1485 1546 +f 1485 1547 1546 +f 1485 5016 1486 +f 1485 1486 1547 +f 1486 1548 1547 +f 1486 4615 1548 +f 1487 1549 4678 +f 1487 4569 4570 +f 1487 4570 5796 +f 1487 5796 1549 +f 1487 4678 4615 +f 1487 4615 4616 +f 1488 4440 4906 +f 1488 5077 4440 +f 1488 4571 4570 +f 1488 1550 4571 +f 1488 4906 1550 +f 1489 5359 1551 +f 1489 4748 4749 +f 1489 1551 4747 +f 1489 4747 4748 +f 1489 5575 5359 +f 1490 4990 4994 +f 1490 4991 4990 +f 1490 1552 4991 +f 1491 5128 5127 +f 1491 5127 1553 +f 1491 5129 5128 +f 1492 1554 4055 +f 1492 4055 4052 +f 1492 4719 1554 +f 1493 1555 4367 +f 1493 4367 4407 +f 1493 4507 4508 +f 1493 4407 5049 +f 1493 4508 1555 +f 1494 4507 4506 +f 1494 4862 1556 +f 1494 1556 4507 +f 1495 1496 5439 +f 1495 5439 1557 +f 1495 4862 4861 +f 1495 1557 4863 +f 1495 4863 4862 +f 1496 1497 5982 +f 1496 1558 5439 +f 1496 5982 1558 +f 1497 1560 1559 +f 1497 1498 1560 +f 1497 1559 5982 +f 1498 1499 1560 +f 1499 1500 1561 +f 1499 1561 1560 +f 1500 1562 1561 +f 1500 1501 1563 +f 1500 1563 1562 +f 1501 1502 1564 +f 1501 1564 1563 +f 1502 1503 1564 +f 1502 5827 1503 +f 1503 1504 1565 +f 1503 1565 1564 +f 1503 5827 1504 +f 1504 1566 1565 +f 1504 1567 1566 +f 1504 1505 1567 +f 1505 1506 1567 +f 1506 1568 1567 +f 1506 5350 1568 +f 1506 1507 5350 +f 1507 4995 1508 +f 1507 1508 5350 +f 1508 1570 5350 +f 1508 4995 1509 +f 1508 1571 1570 +f 1508 1509 1571 +f 1509 1510 1571 +f 1510 5454 1511 +f 1510 1511 5063 +f 1510 5063 1572 +f 1510 1572 1571 +f 1511 5454 1512 +f 1511 1512 5063 +f 1512 1574 5063 +f 1512 1575 1574 +f 1512 1513 1575 +f 1513 1514 1575 +f 1514 5851 1576 +f 1514 1515 5851 +f 1514 1576 1575 +f 1515 5404 1516 +f 1515 1516 5930 +f 1515 5930 5851 +f 1516 1517 5841 +f 1516 5841 5930 +f 1516 5404 1517 +f 1517 1518 6083 +f 1517 6083 5841 +f 1518 1519 5682 +f 1518 5682 6083 +f 1519 5946 5682 +f 1519 4959 1520 +f 1519 1520 5946 +f 1520 1521 4922 +f 1520 4922 5946 +f 1520 4959 1521 +f 1521 1522 4922 +f 1522 1584 4922 +f 1522 1523 1584 +f 1523 1524 1585 +f 1523 1585 1584 +f 1524 1586 1585 +f 1524 1587 1586 +f 1524 1525 1587 +f 1525 1526 1588 +f 1525 1588 1587 +f 1526 1527 1588 +f 1527 5140 1589 +f 1527 1528 5140 +f 1527 1589 1588 +f 1528 1529 5140 +f 1529 1530 5493 +f 1529 5493 5140 +f 1529 5850 1530 +f 1530 1531 1592 +f 1530 5850 1531 +f 1530 1592 5493 +f 1531 1593 1592 +f 1531 5998 1532 +f 1531 1532 1593 +f 1531 5850 5998 +f 1532 1594 1593 +f 1532 1533 1595 +f 1532 1595 1594 +f 1533 1534 1595 +f 1534 1535 1596 +f 1534 1596 1595 +f 1535 1597 1596 +f 1535 1536 1598 +f 1535 1598 1597 +f 1536 1537 1598 +f 1537 1600 1599 +f 1537 1599 1598 +f 1537 1538 1600 +f 1538 5594 1539 +f 1538 5204 1600 +f 1538 1539 5204 +f 1539 5594 1540 +f 1539 1540 5204 +f 1540 5594 5592 +f 1540 5592 1541 +f 1540 1602 5204 +f 1540 1603 1602 +f 1540 1541 1603 +f 1541 5665 1542 +f 1541 5592 5665 +f 1541 1542 1603 +f 1542 5665 5877 +f 1542 5877 5920 +f 1542 5920 1604 +f 1542 1604 1603 +f 1543 1544 1606 +f 1543 1605 5920 +f 1543 1606 1605 +f 1544 1545 1607 +f 1544 1607 1606 +f 1545 1546 1607 +f 1546 1547 1609 +f 1546 1609 1608 +f 1546 1608 1607 +f 1547 4611 1609 +f 1547 1548 4611 +f 1548 4678 5423 +f 1548 1610 4611 +f 1548 4615 4678 +f 1548 5423 1610 +f 1549 1611 4678 +f 1549 5796 1611 +f 1550 4572 4571 +f 1550 1612 4572 +f 1550 4906 5554 +f 1550 5554 1612 +f 1551 1613 4747 +f 1551 5359 5741 +f 1551 5741 1613 +f 1552 4992 4991 +f 1552 1614 4992 +f 1553 5126 1615 +f 1553 5127 5126 +f 1554 5457 4059 +f 1554 4720 4721 +f 1554 1616 5457 +f 1554 4721 1616 +f 1554 4059 4055 +f 1554 4719 4720 +f 1555 1617 6042 +f 1555 4509 1617 +f 1555 4508 4509 +f 1555 6042 4367 +f 1556 1618 4508 +f 1556 4862 4863 +f 1556 4864 1618 +f 1556 4863 4864 +f 1556 4508 4507 +f 1557 5439 1619 +f 1557 1619 4863 +f 1558 5982 1620 +f 1558 1620 5439 +f 1559 1621 5982 +f 1559 6052 1621 +f 1559 1560 6052 +f 1560 1561 6095 +f 1560 6095 1622 +f 1560 1622 6052 +f 1561 1623 6095 +f 1561 1562 1624 +f 1561 1624 1623 +f 1562 1563 1624 +f 1563 1625 1624 +f 1563 1564 1625 +f 1564 1626 1625 +f 1564 1565 1626 +f 1565 1627 1626 +f 1565 1566 1628 +f 1565 1628 1627 +f 1566 1629 1628 +f 1566 1567 1629 +f 1567 1568 1629 +f 1568 5350 1569 +f 1568 1630 1629 +f 1568 1569 1630 +f 1569 1631 1630 +f 1569 1570 1632 +f 1569 1632 1631 +f 1569 5350 1570 +f 1570 1633 1632 +f 1570 1571 1633 +f 1571 1572 1633 +f 1572 5063 1573 +f 1572 1573 1634 +f 1572 1634 1633 +f 1573 5063 1574 +f 1573 1574 1636 +f 1573 1635 1634 +f 1573 1636 1635 +f 1574 1637 1636 +f 1574 1575 1637 +f 1575 1576 1637 +f 1576 5851 1577 +f 1576 1639 1638 +f 1576 1577 1639 +f 1576 1638 1637 +f 1577 1578 5199 +f 1577 5851 5930 +f 1577 5930 1578 +f 1577 5199 1639 +f 1578 5841 1579 +f 1578 1579 5655 +f 1578 5655 5199 +f 1578 5930 5841 +f 1579 6083 1580 +f 1579 1580 5655 +f 1579 5841 6083 +f 1580 5751 1642 +f 1580 1642 5655 +f 1580 5682 1581 +f 1580 6083 5682 +f 1580 1581 5751 +f 1581 5946 1582 +f 1581 5682 5946 +f 1581 1582 5466 +f 1581 5466 5751 +f 1582 4922 1583 +f 1582 5946 4922 +f 1582 1583 5466 +f 1583 1645 5466 +f 1583 1584 1646 +f 1583 4922 1584 +f 1583 1646 1645 +f 1584 1585 1646 +f 1585 1586 1647 +f 1585 1647 1646 +f 1586 1649 1648 +f 1586 1587 1649 +f 1586 1648 1647 +f 1587 1588 5435 +f 1587 5435 1649 +f 1588 1589 5435 +f 1589 5140 1590 +f 1589 1590 5215 +f 1589 1651 5435 +f 1589 5215 1651 +f 1590 5140 1591 +f 1590 1591 5215 +f 1591 5140 5493 +f 1591 1654 1653 +f 1591 1592 1654 +f 1591 1653 5215 +f 1591 5493 1592 +f 1592 1593 1654 +f 1593 1594 1655 +f 1593 1655 1654 +f 1594 5502 1656 +f 1594 1595 5502 +f 1594 1656 1655 +f 1595 1596 5502 +f 1596 1597 5229 +f 1596 5229 5502 +f 1597 1659 5229 +f 1597 1598 1659 +f 1598 1599 1661 +f 1598 1661 1660 +f 1598 1660 1659 +f 1599 1662 1661 +f 1599 1600 1662 +f 1600 1601 1662 +f 1600 5204 1601 +f 1601 1663 1662 +f 1601 5204 1602 +f 1601 1602 1663 +f 1602 1665 1664 +f 1602 1603 1665 +f 1602 1664 1663 +f 1603 1666 1665 +f 1603 1604 1666 +f 1604 1667 1666 +f 1604 1605 1667 +f 1604 5920 1605 +f 1605 1606 1667 +f 1606 1607 1668 +f 1606 1668 1667 +f 1607 1669 1668 +f 1607 1670 1669 +f 1607 1608 1670 +f 1608 1671 1670 +f 1608 1609 1671 +f 1609 4611 1671 +f 1610 6126 4611 +f 1610 5423 6051 +f 1610 6051 6126 +f 1611 4677 4678 +f 1611 5548 4677 +f 1611 5197 1673 +f 1611 1673 5548 +f 1611 5796 4572 +f 1611 4572 5197 +f 1612 1674 4573 +f 1612 4573 4572 +f 1612 5554 1674 +f 1613 1675 4746 +f 1613 5741 5639 +f 1613 4746 4747 +f 1613 5639 1675 +f 1614 1676 4992 +f 1615 5126 1677 +f 1616 4722 1678 +f 1616 4721 4722 +f 1616 1678 5457 +f 1617 6009 6042 +f 1617 4509 4510 +f 1617 4510 1679 +f 1617 1679 6009 +f 1618 1680 4509 +f 1618 4509 4508 +f 1618 4864 4865 +f 1618 4865 1680 +f 1619 1681 4864 +f 1619 5439 1620 +f 1619 4864 4863 +f 1619 1620 5779 +f 1619 5779 1681 +f 1620 5982 1621 +f 1620 1621 1682 +f 1620 1682 5779 +f 1621 6052 1683 +f 1621 1683 1682 +f 1622 6095 1684 +f 1622 1684 6052 +f 1623 1685 6047 +f 1623 1624 1686 +f 1623 1686 1685 +f 1623 6047 6095 +f 1624 1687 1686 +f 1624 1625 1687 +f 1625 1626 1688 +f 1625 1688 1687 +f 1626 1627 1688 +f 1627 1628 1690 +f 1627 1690 1689 +f 1627 1689 1688 +f 1628 1629 1690 +f 1629 1630 1691 +f 1629 1691 1690 +f 1630 1693 1692 +f 1630 1631 1693 +f 1630 1692 1691 +f 1631 1632 5510 +f 1631 5510 1693 +f 1632 1633 5510 +f 1633 1695 5510 +f 1633 1634 4958 +f 1633 4958 1695 +f 1634 1635 4958 +f 1635 1698 1697 +f 1635 1697 4958 +f 1635 1636 1698 +f 1636 1637 1699 +f 1636 1699 1698 +f 1637 1638 1699 +f 1638 1700 1699 +f 1638 1701 1700 +f 1638 1639 1701 +f 1639 1640 5686 +f 1639 5199 1640 +f 1639 5686 1701 +f 1640 5199 5655 +f 1640 5655 1641 +f 1640 5848 5686 +f 1640 1641 5848 +f 1641 1642 5913 +f 1641 5913 5848 +f 1641 5655 1642 +f 1642 1643 5913 +f 1642 5751 1643 +f 1643 5479 1705 +f 1643 1644 5479 +f 1643 5751 5466 +f 1643 5466 1644 +f 1643 1705 5913 +f 1644 5466 1645 +f 1644 1645 5479 +f 1645 1707 5479 +f 1645 1708 1707 +f 1645 1646 1708 +f 1646 1709 1708 +f 1646 1647 1709 +f 1647 1648 1709 +f 1648 1710 1709 +f 1648 1649 1710 +f 1649 5435 1650 +f 1649 1650 1712 +f 1649 1712 1711 +f 1649 1711 1710 +f 1650 5435 1651 +f 1650 1651 1712 +f 1651 1652 4976 +f 1651 4976 1713 +f 1651 1713 1712 +f 1651 5215 1652 +f 1652 1653 4976 +f 1652 5215 1653 +f 1653 1654 1716 +f 1653 1716 1715 +f 1653 1715 4976 +f 1654 1655 1716 +f 1655 5587 1717 +f 1655 1717 1716 +f 1655 1656 5587 +f 1656 1657 5587 +f 1656 5502 1657 +f 1657 1719 5587 +f 1657 1658 1720 +f 1657 1720 1719 +f 1657 5502 1658 +f 1658 1721 1720 +f 1658 1659 1721 +f 1658 5229 1659 +f 1658 5502 5229 +f 1659 1722 1721 +f 1659 1660 1722 +f 1660 1661 1722 +f 1661 1662 1723 +f 1661 1723 1722 +f 1662 1663 1725 +f 1662 1725 1724 +f 1662 1724 1723 +f 1663 1664 1725 +f 1664 1726 1725 +f 1664 1727 1726 +f 1664 1665 1727 +f 1665 1728 1727 +f 1665 1666 1728 +f 1666 1667 1728 +f 1667 1668 1729 +f 1667 1729 1728 +f 1668 1731 1730 +f 1668 1669 1731 +f 1668 1730 1729 +f 1669 1670 1732 +f 1669 1732 1731 +f 1670 1733 1732 +f 1670 1671 1733 +f 1671 4611 6070 +f 1671 6070 1733 +f 1672 6044 6109 +f 1672 6109 6090 +f 1672 6051 6044 +f 1672 6090 6051 +f 1673 1735 5548 +f 1673 5197 1735 +f 1674 4574 4573 +f 1674 1736 4574 +f 1674 4463 4438 +f 1674 4438 1736 +f 1674 5554 4463 +f 1675 5639 4250 +f 1675 4250 4257 +f 1675 4745 4746 +f 1675 1737 4745 +f 1675 4257 1737 +f 1676 1738 4993 +f 1676 4993 4992 +f 1677 4817 1739 +f 1677 4065 4817 +f 1677 5126 4065 +f 1678 4723 1740 +f 1678 1740 4066 +f 1678 4722 4723 +f 1678 4066 5457 +f 1679 1741 6009 +f 1679 4510 4511 +f 1679 4511 1741 +f 1680 4510 4509 +f 1680 4866 1742 +f 1680 4865 4866 +f 1680 1742 4510 +f 1681 4865 4864 +f 1681 1743 4865 +f 1681 5779 1743 +f 1682 1744 5779 +f 1682 1683 1744 +f 1683 6027 1745 +f 1683 1745 1744 +f 1683 6052 6027 +f 1684 6095 6047 +f 1684 1746 6027 +f 1684 6027 6052 +f 1684 6047 1746 +f 1685 1747 6047 +f 1685 1748 1747 +f 1685 1686 1748 +f 1686 1687 1748 +f 1687 1688 1750 +f 1687 1750 1749 +f 1687 1749 1748 +f 1688 1689 1750 +f 1689 1690 1752 +f 1689 1751 1750 +f 1689 1752 1751 +f 1690 5723 1752 +f 1690 1691 5723 +f 1691 1692 5723 +f 1692 1693 1755 +f 1692 1755 1754 +f 1692 1754 5723 +f 1693 5510 1694 +f 1693 1694 1755 +f 1694 1695 5894 +f 1694 1756 1755 +f 1694 5510 1695 +f 1694 5894 1756 +f 1695 4958 1696 +f 1695 1696 5661 +f 1695 5661 5894 +f 1696 5977 5661 +f 1696 1697 5977 +f 1696 4958 1697 +f 1697 1698 5948 +f 1697 5948 5977 +f 1698 1699 5948 +f 1699 1700 1762 +f 1699 1762 1761 +f 1699 1761 5948 +f 1700 1701 1762 +f 1701 1702 1763 +f 1701 1763 1762 +f 1701 5686 1702 +f 1702 5686 5848 +f 1702 5848 1703 +f 1702 1703 5317 +f 1702 5317 1764 +f 1702 1764 1763 +f 1703 1704 5317 +f 1703 5848 5913 +f 1703 5913 1704 +f 1704 5913 1705 +f 1704 1705 1767 +f 1704 1766 5317 +f 1704 1767 1766 +f 1705 5479 1706 +f 1705 1706 1767 +f 1706 1768 1767 +f 1706 4916 1768 +f 1706 5479 1707 +f 1706 1707 4916 +f 1707 5616 4916 +f 1707 1708 5616 +f 1708 1709 5616 +f 1709 1772 1771 +f 1709 1710 1772 +f 1709 1771 5616 +f 1710 1711 1772 +f 1711 1773 1772 +f 1711 1774 1773 +f 1711 1712 1774 +f 1712 1775 1774 +f 1712 1713 1775 +f 1713 1714 1776 +f 1713 1776 1775 +f 1713 4976 1714 +f 1714 1777 1776 +f 1714 1715 1777 +f 1714 4976 1715 +f 1715 1716 1777 +f 1716 1717 1778 +f 1716 1778 1777 +f 1717 1718 5477 +f 1717 5477 1779 +f 1717 1779 1778 +f 1717 5587 1718 +f 1718 5587 1719 +f 1718 1719 5477 +f 1719 1720 5976 +f 1719 5976 5477 +f 1720 1721 5340 +f 1720 5340 5976 +f 1721 1783 5340 +f 1721 1722 1784 +f 1721 1784 1783 +f 1722 1723 1784 +f 1723 1785 1784 +f 1723 1724 1786 +f 1723 1786 1785 +f 1724 1725 1786 +f 1725 1787 1786 +f 1725 1726 1787 +f 1726 1789 1788 +f 1726 1727 1789 +f 1726 1788 1787 +f 1727 1728 1789 +f 1728 1790 1789 +f 1728 1791 1790 +f 1728 1729 1791 +f 1729 1730 1791 +f 1730 1792 1791 +f 1730 1731 1793 +f 1730 1793 1792 +f 1731 1732 1793 +f 1732 1795 1794 +f 1732 1733 1795 +f 1732 1794 1793 +f 1733 6091 6120 +f 1733 6120 1795 +f 1733 6070 6091 +f 1734 6111 1796 +f 1734 6091 6109 +f 1734 1796 6120 +f 1734 6109 6111 +f 1734 6120 6091 +f 1735 1797 6030 +f 1735 6030 5548 +f 1735 5197 4574 +f 1735 4574 1797 +f 1736 4438 1798 +f 1736 1798 4575 +f 1736 4575 4574 +f 1737 4807 4744 +f 1737 4744 4745 +f 1737 1799 4807 +f 1737 4257 1799 +f 1738 1800 5338 +f 1738 5338 4260 +f 1738 4260 4993 +f 1739 4816 1801 +f 1739 4817 4816 +f 1740 1802 4067 +f 1740 4723 1802 +f 1740 4067 4066 +f 1741 1803 4369 +f 1741 4369 4405 +f 1741 4511 4512 +f 1741 4512 1803 +f 1741 4405 6009 +f 1742 1804 4511 +f 1742 4511 4510 +f 1742 4866 1804 +f 1743 5779 1744 +f 1743 4866 4865 +f 1743 1744 1805 +f 1743 1805 4867 +f 1743 4867 4866 +f 1744 1807 1806 +f 1744 1745 1807 +f 1744 1806 1805 +f 1745 6027 1746 +f 1745 1808 1807 +f 1745 1746 1808 +f 1746 1747 1808 +f 1746 6047 1747 +f 1747 1809 1808 +f 1747 1748 1809 +f 1748 1810 1809 +f 1748 1749 1810 +f 1749 1811 1810 +f 1749 1750 1811 +f 1750 1751 1812 +f 1750 1812 1811 +f 1751 1813 1812 +f 1751 1814 1813 +f 1751 1752 1814 +f 1752 5723 1753 +f 1752 1753 1814 +f 1753 1815 1814 +f 1753 5394 1815 +f 1753 5723 1754 +f 1753 1754 5394 +f 1754 1755 5250 +f 1754 5250 5394 +f 1755 1756 5250 +f 1756 1818 5250 +f 1756 1757 1819 +f 1756 1819 1818 +f 1756 5894 1757 +f 1757 5661 1758 +f 1757 5894 5661 +f 1757 5446 1819 +f 1757 1758 5446 +f 1758 5661 5977 +f 1758 5994 5446 +f 1758 5977 1759 +f 1758 1759 5994 +f 1759 5977 5948 +f 1759 5948 1760 +f 1759 1760 5994 +f 1760 1761 5702 +f 1760 5948 1761 +f 1760 1822 5994 +f 1760 5702 1822 +f 1761 1762 5729 +f 1761 5729 5702 +f 1762 1763 5729 +f 1763 1764 1825 +f 1763 1825 5729 +f 1764 5317 1765 +f 1764 1765 5159 +f 1764 5159 1826 +f 1764 1826 1825 +f 1765 5317 1766 +f 1765 1766 5544 +f 1765 5544 5159 +f 1766 5786 5544 +f 1766 1767 5786 +f 1767 1768 5786 +f 1768 1830 5786 +f 1768 1769 5162 +f 1768 4916 1769 +f 1768 5162 1830 +f 1769 1770 5162 +f 1769 5616 1770 +f 1769 4916 5616 +f 1770 1771 1833 +f 1770 1833 1832 +f 1770 1832 5162 +f 1770 5616 1771 +f 1771 1772 1833 +f 1772 1834 1833 +f 1772 1773 1835 +f 1772 1835 1834 +f 1773 1774 1835 +f 1774 1775 1836 +f 1774 1836 1835 +f 1775 1837 1836 +f 1775 1838 1837 +f 1775 1776 1838 +f 1776 1777 1838 +f 1777 1839 1838 +f 1777 1778 1840 +f 1777 1840 1839 +f 1778 5371 1840 +f 1778 1779 5371 +f 1779 5477 1780 +f 1779 1780 5371 +f 1780 5477 1781 +f 1780 1781 5704 +f 1780 5704 5371 +f 1781 5976 1782 +f 1781 1782 5485 +f 1781 5485 5704 +f 1781 5477 5976 +f 1782 5976 5340 +f 1782 1844 5485 +f 1782 5340 1783 +f 1782 1783 1844 +f 1783 1784 1846 +f 1783 1846 1845 +f 1783 1845 1844 +f 1784 1785 1846 +f 1785 1786 1847 +f 1785 1847 1846 +f 1786 1848 1847 +f 1786 1849 1848 +f 1786 1787 1849 +f 1787 1788 1849 +f 1788 1850 1849 +f 1788 1851 1850 +f 1788 1789 1851 +f 1789 1852 1851 +f 1789 1790 1852 +f 1790 1853 1852 +f 1790 1791 1853 +f 1791 1854 1853 +f 1791 1792 1854 +f 1792 1855 1854 +f 1792 1793 1855 +f 1793 1794 1855 +f 1794 1795 1856 +f 1794 1856 1855 +f 1795 6120 5916 +f 1795 5916 1857 +f 1795 1857 1856 +f 1796 6111 4674 +f 1796 1858 6120 +f 1796 4674 4673 +f 1796 4673 1858 +f 1797 4574 4575 +f 1797 5577 1859 +f 1797 4575 5577 +f 1797 1859 4674 +f 1797 4674 6030 +f 1798 5487 4575 +f 1798 4464 5134 +f 1798 4438 4464 +f 1798 5134 1860 +f 1798 1860 5487 +f 1799 4258 5430 +f 1799 1861 4807 +f 1799 5430 1861 +f 1799 4257 4258 +f 1800 5337 5338 +f 1800 1862 5337 +f 1801 4816 1863 +f 1802 4068 4067 +f 1802 1864 4068 +f 1802 4723 4724 +f 1802 4724 4725 +f 1802 4725 1864 +f 1803 5672 4369 +f 1803 4513 1865 +f 1803 4512 4513 +f 1803 1865 5672 +f 1804 4512 4511 +f 1804 5474 4512 +f 1804 1866 5474 +f 1804 4867 4868 +f 1804 4868 1866 +f 1804 4866 4867 +f 1805 1867 4867 +f 1805 1868 1867 +f 1805 1806 1868 +f 1806 1869 1868 +f 1806 1807 1869 +f 1807 5275 1869 +f 1807 1808 5275 +f 1808 1809 1870 +f 1808 1870 5275 +f 1809 1810 1871 +f 1809 1871 1870 +f 1810 1872 1871 +f 1810 1873 1872 +f 1810 1811 1873 +f 1811 1874 1873 +f 1811 1812 1874 +f 1812 1813 1874 +f 1813 1814 1876 +f 1813 1876 1875 +f 1813 1875 1874 +f 1814 1877 1876 +f 1814 1815 1877 +f 1815 1816 1877 +f 1815 5394 1816 +f 1816 5250 1817 +f 1816 1878 1877 +f 1816 5394 5250 +f 1816 1817 1878 +f 1817 1818 5630 +f 1817 5630 1879 +f 1817 5250 1818 +f 1817 1879 1878 +f 1818 1819 5630 +f 1819 5446 1820 +f 1819 1881 5630 +f 1819 5107 1881 +f 1819 1820 5107 +f 1820 5994 1821 +f 1820 5446 5994 +f 1820 1821 5107 +f 1821 1822 1883 +f 1821 1883 5107 +f 1821 5994 1822 +f 1822 1823 5919 +f 1822 5919 1884 +f 1822 5702 1823 +f 1822 1884 1883 +f 1823 5702 5729 +f 1823 5729 1824 +f 1823 1824 5641 +f 1823 5641 5919 +f 1824 1825 5048 +f 1824 5048 5641 +f 1824 5729 1825 +f 1825 1826 5048 +f 1826 5159 1827 +f 1826 1888 5048 +f 1826 5052 1888 +f 1826 1827 5052 +f 1827 5544 1828 +f 1827 5159 5544 +f 1827 1828 5392 +f 1827 5392 5052 +f 1828 5786 1829 +f 1828 5544 5786 +f 1828 1829 5392 +f 1829 5786 1830 +f 1829 1891 5392 +f 1829 1830 1892 +f 1829 1892 1891 +f 1830 1831 1892 +f 1830 5162 1831 +f 1831 1893 1892 +f 1831 1894 1893 +f 1831 1832 1894 +f 1831 5162 1832 +f 1832 1895 1894 +f 1832 1833 1895 +f 1833 1834 1895 +f 1834 5301 1896 +f 1834 1896 1895 +f 1834 1835 5301 +f 1835 1836 5301 +f 1836 5617 5301 +f 1836 1837 5617 +f 1837 1838 5269 +f 1837 5269 5617 +f 1838 1839 1900 +f 1838 1900 5269 +f 1839 1901 1900 +f 1839 5395 1901 +f 1839 1840 5395 +f 1840 1841 5395 +f 1840 5371 1841 +f 1841 5674 5395 +f 1841 5673 5674 +f 1841 5371 5673 +f 1842 5704 1843 +f 1842 5697 5673 +f 1842 5673 5704 +f 1842 1843 5697 +f 1843 5704 5485 +f 1843 1906 1905 +f 1843 1844 1906 +f 1843 5485 1844 +f 1843 1905 5697 +f 1844 1845 1906 +f 1845 1846 1908 +f 1845 1907 1906 +f 1845 1908 1907 +f 1846 1847 1909 +f 1846 1909 1908 +f 1847 1910 1909 +f 1847 1848 1910 +f 1848 1849 1910 +f 1849 1911 1910 +f 1849 1850 1911 +f 1850 1913 1912 +f 1850 1851 1913 +f 1850 1912 1911 +f 1851 1852 1913 +f 1852 1914 1913 +f 1852 1853 1914 +f 1853 1915 1914 +f 1853 1854 1916 +f 1853 1916 1915 +f 1854 1855 5152 +f 1854 5152 1916 +f 1855 5388 1917 +f 1855 1856 5388 +f 1855 1917 5152 +f 1856 1857 5818 +f 1856 1918 5388 +f 1856 5818 1918 +f 1857 5916 1919 +f 1857 1919 5818 +f 1858 5916 6120 +f 1858 5870 5916 +f 1858 4673 4672 +f 1858 4672 1920 +f 1858 1920 5870 +f 1859 5577 1921 +f 1859 1921 4673 +f 1859 4673 4674 +f 1860 1922 5487 +f 1860 5134 1922 +f 1861 5430 1923 +f 1861 4806 4807 +f 1861 1923 4806 +f 1862 1924 5337 +f 1863 4816 4815 +f 1863 4814 1925 +f 1863 4815 4814 +f 1864 1926 4071 +f 1864 4071 4068 +f 1864 4725 4726 +f 1864 4726 1926 +f 1865 4514 1927 +f 1865 4513 4514 +f 1865 1927 5672 +f 1866 4869 1928 +f 1866 1928 5474 +f 1866 4868 4869 +f 1867 1868 1929 +f 1867 4868 4867 +f 1867 1929 4868 +f 1868 1869 1930 +f 1868 1930 1929 +f 1869 6082 1931 +f 1869 5275 6082 +f 1869 1931 1930 +f 1870 1871 1933 +f 1870 1933 1932 +f 1870 1932 5275 +f 1871 1934 1933 +f 1871 1872 1934 +f 1872 1873 1935 +f 1872 1935 1934 +f 1873 1936 1935 +f 1873 1874 1936 +f 1874 1875 1936 +f 1875 1937 1936 +f 1875 1876 1937 +f 1876 1877 1938 +f 1876 1938 1937 +f 1877 1939 1938 +f 1877 1878 1939 +f 1878 1940 1939 +f 1878 1941 1940 +f 1878 1879 1941 +f 1879 1880 1941 +f 1879 5630 1880 +f 1880 5630 1881 +f 1880 1881 1943 +f 1880 1942 1941 +f 1880 1943 1942 +f 1881 1882 1944 +f 1881 1944 1943 +f 1881 5107 1882 +f 1882 1883 5305 +f 1882 5107 1883 +f 1882 5305 1944 +f 1883 1884 5305 +f 1884 1885 1946 +f 1884 5919 1885 +f 1884 1946 5305 +f 1885 5919 5641 +f 1885 1886 5214 +f 1885 5214 1947 +f 1885 5641 1886 +f 1885 1947 1946 +f 1886 1887 5958 +f 1886 5641 5048 +f 1886 5048 1887 +f 1886 5958 5214 +f 1887 5361 5958 +f 1887 1888 5361 +f 1887 5048 1888 +f 1888 1889 5725 +f 1888 5725 5361 +f 1888 5052 1889 +f 1889 5944 5725 +f 1889 1890 5944 +f 1889 5052 5392 +f 1889 5392 1890 +f 1890 5392 1891 +f 1890 1891 5944 +f 1891 1953 5944 +f 1891 1892 5354 +f 1891 5354 1953 +f 1892 1893 5354 +f 1893 1955 5354 +f 1893 1894 1956 +f 1893 1956 1955 +f 1894 1895 1956 +f 1895 5872 1957 +f 1895 1896 5872 +f 1895 1957 1956 +f 1896 1897 5872 +f 1896 5301 1897 +f 1897 1959 5872 +f 1897 1898 5313 +f 1897 5313 1959 +f 1897 5301 1898 +f 1898 5596 5313 +f 1898 5301 5617 +f 1898 5617 5722 +f 1898 5722 5596 +f 1899 5596 5722 +f 1899 5722 5269 +f 1899 5304 5596 +f 1899 1900 5304 +f 1899 5269 1900 +f 1900 5417 1962 +f 1900 1962 5304 +f 1900 1901 5417 +f 1901 5395 1902 +f 1901 1902 5417 +f 1902 1903 5647 +f 1902 5647 5417 +f 1902 5395 1903 +f 1903 1966 1965 +f 1903 1904 1966 +f 1903 1965 5647 +f 1903 5395 5674 +f 1903 5674 1904 +f 1904 5911 1966 +f 1904 1905 5911 +f 1904 5674 5697 +f 1904 5697 1905 +f 1905 1906 5911 +f 1906 1968 5911 +f 1906 1907 1968 +f 1907 1969 1968 +f 1907 1908 5991 +f 1907 5991 1969 +f 1908 1909 5991 +f 1909 1971 5991 +f 1909 1910 1972 +f 1909 1972 1971 +f 1910 1973 1972 +f 1910 1911 1973 +f 1911 5925 1973 +f 1911 1912 5925 +f 1912 1913 5925 +f 1913 5843 5925 +f 1913 1914 5843 +f 1914 1976 5843 +f 1914 5491 1976 +f 1914 1915 5491 +f 1915 1977 5491 +f 1915 1916 1977 +f 1916 1978 1977 +f 1916 5152 1978 +f 1917 5388 1979 +f 1917 1979 5152 +f 1918 5987 5388 +f 1918 1980 5987 +f 1918 5818 1980 +f 1919 5916 5870 +f 1919 5870 1981 +f 1919 1981 5818 +f 1920 4672 4671 +f 1920 4671 1982 +f 1920 1982 5870 +f 1921 5577 4577 +f 1921 4577 5248 +f 1921 1983 4672 +f 1921 4672 4673 +f 1921 5248 1983 +f 1922 1984 5845 +f 1922 5845 4577 +f 1922 4577 5487 +f 1922 5134 4971 +f 1922 6006 1984 +f 1922 4971 6006 +f 1923 5430 4262 +f 1923 4805 4806 +f 1923 4262 1985 +f 1923 1985 4805 +f 1924 5335 5336 +f 1924 1986 5335 +f 1924 5336 5337 +f 1925 4818 1987 +f 1925 4814 4818 +f 1926 4726 4727 +f 1926 4727 1988 +f 1926 4075 4071 +f 1926 1988 4075 +f 1927 4514 4515 +f 1927 1989 4894 +f 1927 4515 1989 +f 1927 4370 5672 +f 1927 4894 4370 +f 1928 4870 1990 +f 1928 1990 5097 +f 1928 4869 4870 +f 1928 5097 5474 +f 1929 1992 1991 +f 1929 1930 1992 +f 1929 1991 4869 +f 1929 4869 4868 +f 1930 1993 1992 +f 1930 1931 1993 +f 1931 6082 1993 +f 1932 6058 6082 +f 1932 1994 6058 +f 1932 6082 5275 +f 1932 1995 1994 +f 1932 1933 1995 +f 1933 1934 1995 +f 1934 1935 1996 +f 1934 1996 1995 +f 1935 1936 1997 +f 1935 1997 1996 +f 1936 1937 1999 +f 1936 1999 1998 +f 1936 1998 1997 +f 1937 1938 1999 +f 1938 1939 2000 +f 1938 2000 1999 +f 1939 1940 2001 +f 1939 2001 2000 +f 1940 2002 2001 +f 1940 2003 2002 +f 1940 1941 2003 +f 1941 2004 2003 +f 1941 1942 2004 +f 1942 1943 2004 +f 1943 1944 2006 +f 1943 2005 2004 +f 1943 2006 2005 +f 1944 5305 1945 +f 1944 2007 2006 +f 1944 1945 2007 +f 1945 1946 2007 +f 1945 5305 1946 +f 1946 1947 5529 +f 1946 2008 2007 +f 1946 5529 2008 +f 1947 1948 5529 +f 1947 5214 1948 +f 1948 2010 5529 +f 1948 5990 2010 +f 1948 5958 1949 +f 1948 5214 5958 +f 1948 1949 5990 +f 1949 1950 5990 +f 1949 5958 5361 +f 1949 5361 1950 +f 1950 5725 1951 +f 1950 2012 5990 +f 1950 5361 5725 +f 1950 1951 2013 +f 1950 2013 2012 +f 1951 1952 5732 +f 1951 5732 2013 +f 1951 5725 5944 +f 1951 5944 1952 +f 1952 5728 5732 +f 1952 1953 5728 +f 1952 5944 1953 +f 1953 1954 5728 +f 1953 5354 1954 +f 1954 2016 5728 +f 1954 1955 5629 +f 1954 5354 1955 +f 1954 5629 2016 +f 1955 1956 5311 +f 1955 5311 5629 +f 1956 1957 5311 +f 1957 5872 1958 +f 1957 1958 5367 +f 1957 5367 5311 +f 1958 1959 2020 +f 1958 5872 1959 +f 1958 2020 5367 +f 1959 1960 5638 +f 1959 5638 2021 +f 1959 2021 2020 +f 1959 5313 1960 +f 1960 5825 5638 +f 1960 5313 5825 +f 1961 5588 5825 +f 1961 5304 1962 +f 1961 1962 5588 +f 1961 5825 5304 +f 1962 1963 5360 +f 1962 5360 5588 +f 1962 5417 1963 +f 1963 2025 5360 +f 1963 5417 1964 +f 1963 1964 2025 +f 1964 2027 2026 +f 1964 1965 2027 +f 1964 5647 1965 +f 1964 5417 5647 +f 1964 2026 2025 +f 1965 1966 2028 +f 1965 2028 2027 +f 1966 5911 1967 +f 1966 1967 2028 +f 1967 5911 1968 +f 1967 1968 2029 +f 1967 2029 2028 +f 1968 2030 2029 +f 1968 2031 2030 +f 1968 1969 2031 +f 1969 1970 2032 +f 1969 2032 2031 +f 1969 5991 1970 +f 1970 5991 1971 +f 1970 1971 2032 +f 1971 2033 2032 +f 1971 2034 2033 +f 1971 1972 2034 +f 1972 2035 2034 +f 1972 1973 2035 +f 1973 6053 2035 +f 1973 1974 6053 +f 1973 5925 1974 +f 1974 6003 6053 +f 1974 5925 6003 +f 1975 1976 5972 +f 1975 6003 5843 +f 1975 5972 6003 +f 1975 5843 1976 +f 1976 2038 5972 +f 1976 5491 2038 +f 1977 1978 2040 +f 1977 2039 5491 +f 1977 2040 2039 +f 1978 5152 1979 +f 1978 1979 2040 +f 1979 5987 5237 +f 1979 5388 5987 +f 1979 5237 2041 +f 1979 2041 2040 +f 1980 2042 5987 +f 1980 6055 2042 +f 1980 5818 6055 +f 1981 6055 5818 +f 1981 5870 1982 +f 1981 1982 2043 +f 1981 2043 6055 +f 1982 4671 2044 +f 1982 2044 2043 +f 1983 4671 4672 +f 1983 4670 4671 +f 1983 5248 2045 +f 1983 2045 4670 +f 1984 6006 5886 +f 1984 5886 5845 +f 1985 4265 2047 +f 1985 4804 4805 +f 1985 2047 4803 +f 1985 4262 4265 +f 1985 4803 4804 +f 1986 5334 5335 +f 1986 2048 5334 +f 1987 4818 2049 +f 1988 4727 2050 +f 1988 4082 4076 +f 1988 4076 4075 +f 1988 2050 4082 +f 1989 2051 4894 +f 1989 4515 2051 +f 1990 4515 5097 +f 1990 4516 4515 +f 1990 2052 4516 +f 1990 4870 2052 +f 1991 1992 2053 +f 1991 4871 4870 +f 1991 4870 4869 +f 1991 2053 4871 +f 1992 2054 2053 +f 1992 1993 2054 +f 1993 6082 6058 +f 1993 6058 2055 +f 1993 2055 2054 +f 1994 2056 6058 +f 1994 1995 2056 +f 1995 1996 2058 +f 1995 2058 2057 +f 1995 2057 2056 +f 1996 2059 2058 +f 1996 1997 2059 +f 1997 1998 2060 +f 1997 2060 2059 +f 1998 1999 2060 +f 1999 5200 2061 +f 1999 2000 5200 +f 1999 2061 2060 +f 2000 2001 5200 +f 2001 2002 2064 +f 2001 2063 5200 +f 2001 2064 2063 +f 2002 5505 2064 +f 2002 2003 5505 +f 2003 5184 5505 +f 2003 2004 5184 +f 2004 2005 5184 +f 2005 2067 5184 +f 2005 5613 2067 +f 2005 2006 5613 +f 2006 2007 5385 +f 2006 5385 5613 +f 2007 2008 5385 +f 2008 2070 5385 +f 2008 5529 2009 +f 2008 2009 2071 +f 2008 2071 2070 +f 2009 2010 2071 +f 2009 5529 2010 +f 2010 5990 2011 +f 2010 2011 5240 +f 2010 5240 2072 +f 2010 2072 2071 +f 2011 5498 5240 +f 2011 2012 5498 +f 2011 5990 2012 +f 2012 2013 6065 +f 2012 6065 5498 +f 2013 5732 2014 +f 2013 2014 6065 +f 2014 2076 6065 +f 2014 2015 2076 +f 2014 5728 2015 +f 2014 5732 5728 +f 2015 2077 2076 +f 2015 5728 2016 +f 2015 2016 2078 +f 2015 2078 2077 +f 2016 2017 2078 +f 2016 5629 2017 +f 2017 5311 2018 +f 2017 5629 5311 +f 2017 2079 2078 +f 2017 4933 2079 +f 2017 2018 4933 +f 2018 5311 2019 +f 2018 2019 4933 +f 2019 5311 5367 +f 2019 2020 2081 +f 2019 2081 4933 +f 2019 5367 2020 +f 2020 5463 2082 +f 2020 2021 5463 +f 2020 2082 2081 +f 2021 5638 2022 +f 2021 2022 5463 +f 2022 5638 2023 +f 2022 5055 5463 +f 2022 2023 5055 +f 2023 5638 5588 +f 2023 2024 5136 +f 2023 5136 5055 +f 2023 5588 2024 +f 2024 2086 5136 +f 2024 2025 2087 +f 2024 5588 5360 +f 2024 5360 2025 +f 2024 2087 2086 +f 2025 2026 2088 +f 2025 2088 2087 +f 2026 2027 2088 +f 2027 2028 2089 +f 2027 2089 2088 +f 2028 2091 2090 +f 2028 2029 2091 +f 2028 2090 2089 +f 2029 2030 2092 +f 2029 2092 2091 +f 2030 2031 2092 +f 2031 2032 2093 +f 2031 2093 2092 +f 2032 2033 2094 +f 2032 2094 2093 +f 2033 2095 2094 +f 2033 2034 2095 +f 2034 2096 2095 +f 2034 2035 2096 +f 2035 2097 2096 +f 2035 6039 2097 +f 2035 6053 2036 +f 2035 2036 6039 +f 2036 6077 6039 +f 2036 6020 6077 +f 2036 6096 6020 +f 2036 6053 6096 +f 2037 6020 6096 +f 2037 5972 2038 +f 2037 6096 5972 +f 2037 6007 6077 +f 2037 6077 6020 +f 2037 2038 6007 +f 2038 2100 6007 +f 2038 2039 2100 +f 2038 5491 2039 +f 2039 2101 2100 +f 2039 2040 2101 +f 2040 2041 2103 +f 2040 2103 2102 +f 2040 2102 2101 +f 2041 5237 2103 +f 2042 5237 5987 +f 2042 2104 5237 +f 2042 6055 2043 +f 2042 5705 2104 +f 2042 2043 5705 +f 2043 2105 5705 +f 2043 2044 5054 +f 2043 5054 2105 +f 2044 2106 5054 +f 2044 4671 4670 +f 2044 4670 4669 +f 2044 4669 2106 +f 2045 5248 5519 +f 2045 2107 4670 +f 2045 5519 2107 +f 2046 5886 5887 +f 2046 5887 2108 +f 2046 2108 5927 +f 2046 5927 5886 +f 2047 4265 4266 +f 2047 4273 2109 +f 2047 4266 4273 +f 2047 2109 4803 +f 2048 5362 5334 +f 2048 2110 5362 +f 2049 4818 4081 +f 2049 4081 4932 +f 2049 4932 2111 +f 2050 4727 4728 +f 2050 4729 2112 +f 2050 4728 4729 +f 2050 2112 4082 +f 2051 4516 4517 +f 2051 4517 2113 +f 2051 2113 5079 +f 2051 5079 4371 +f 2051 4371 4894 +f 2051 4515 4516 +f 2052 2114 4516 +f 2052 4870 4871 +f 2052 4872 2114 +f 2052 4871 4872 +f 2053 2116 2115 +f 2053 2054 2116 +f 2053 2115 4871 +f 2054 2055 2117 +f 2054 2117 2116 +f 2055 6058 2056 +f 2055 2056 2117 +f 2056 2118 2117 +f 2056 2057 2118 +f 2057 2119 2118 +f 2057 2058 2119 +f 2058 2120 2119 +f 2058 2059 2121 +f 2058 2121 2120 +f 2059 2060 2121 +f 2060 2122 2121 +f 2060 2123 2122 +f 2060 2061 2123 +f 2061 2062 5905 +f 2061 5200 2062 +f 2061 5905 2123 +f 2062 2063 5829 +f 2062 5200 2063 +f 2062 5829 5905 +f 2063 2064 5829 +f 2064 2126 5829 +f 2064 5505 2065 +f 2064 2065 2126 +f 2065 5505 5184 +f 2065 2127 2126 +f 2065 2128 2127 +f 2065 2066 2128 +f 2065 5184 2066 +f 2066 2067 2128 +f 2066 5184 2067 +f 2067 2068 2129 +f 2067 2129 2128 +f 2067 5613 2068 +f 2068 2130 2129 +f 2068 5613 5385 +f 2068 2069 2131 +f 2068 2131 2130 +f 2068 5385 2069 +f 2069 5385 2070 +f 2069 2132 2131 +f 2069 2070 2132 +f 2070 2133 2132 +f 2070 2071 2133 +f 2071 2134 2133 +f 2071 2072 2134 +f 2072 2073 2134 +f 2072 5240 2073 +f 2073 5240 5498 +f 2073 5498 2074 +f 2073 2074 2136 +f 2073 2135 2134 +f 2073 2136 2135 +f 2074 6065 2075 +f 2074 5498 6065 +f 2074 2075 2137 +f 2074 2137 2136 +f 2075 6065 2076 +f 2075 2076 6074 +f 2075 6074 2137 +f 2076 2077 6069 +f 2076 6069 6074 +f 2077 2078 6069 +f 2078 2079 2140 +f 2078 2140 6069 +f 2079 2141 2140 +f 2079 2142 2141 +f 2079 2080 2142 +f 2079 4933 2080 +f 2080 5830 2142 +f 2080 4933 2081 +f 2080 2081 5830 +f 2081 2082 5830 +f 2082 5463 2083 +f 2082 5141 5830 +f 2082 2083 5141 +f 2083 5463 2084 +f 2083 5167 5141 +f 2083 2084 5167 +f 2084 5463 5055 +f 2084 2085 2146 +f 2084 2146 5167 +f 2084 5055 2085 +f 2085 5055 5136 +f 2085 2086 2147 +f 2085 5136 2086 +f 2085 2147 2146 +f 2086 2087 5669 +f 2086 2148 2147 +f 2086 5669 2148 +f 2087 2088 5669 +f 2088 2150 5669 +f 2088 2089 2150 +f 2089 2090 2151 +f 2089 2151 2150 +f 2090 2152 2151 +f 2090 2091 2152 +f 2091 2154 2153 +f 2091 2092 2154 +f 2091 2153 2152 +f 2092 2155 2154 +f 2092 2093 2155 +f 2093 2094 2155 +f 2094 2157 2156 +f 2094 2095 2157 +f 2094 2156 2155 +f 2095 2096 2157 +f 2096 2158 2157 +f 2096 2097 2158 +f 2097 2098 2159 +f 2097 6039 2098 +f 2097 2159 2158 +f 2098 2160 2159 +f 2098 2099 2160 +f 2098 6039 6077 +f 2098 6077 2099 +f 2099 6007 2100 +f 2099 6077 6007 +f 2099 2100 2162 +f 2099 2162 2161 +f 2099 2161 2160 +f 2100 2101 2162 +f 2101 2102 2164 +f 2101 2163 2162 +f 2101 2164 2163 +f 2102 2103 2165 +f 2102 2165 2164 +f 2103 2166 2165 +f 2103 2104 2166 +f 2103 5237 2104 +f 2104 5705 2166 +f 2105 2167 5705 +f 2105 5054 2167 +f 2106 4668 2168 +f 2106 4669 4668 +f 2106 2168 5054 +f 2107 5519 4580 +f 2107 4580 2169 +f 2107 2169 4669 +f 2107 4669 4670 +f 2108 5887 4435 +f 2108 4435 2170 +f 2108 5942 5927 +f 2108 2170 5942 +f 2109 4273 2171 +f 2109 4802 4803 +f 2109 2171 4802 +f 2110 2172 5362 +f 2111 4932 4931 +f 2111 4931 2173 +f 2112 4083 4082 +f 2112 2174 4083 +f 2112 4729 4730 +f 2112 4730 2174 +f 2113 5626 2175 +f 2113 2175 5079 +f 2113 4517 5626 +f 2114 4517 4516 +f 2114 2176 4517 +f 2114 4872 4873 +f 2114 4873 2176 +f 2115 2116 2177 +f 2115 2177 4872 +f 2115 4872 4871 +f 2116 5794 2178 +f 2116 2117 5794 +f 2116 2178 2177 +f 2117 2118 5839 +f 2117 5839 2179 +f 2117 2179 5794 +f 2118 5980 2180 +f 2118 2180 5839 +f 2118 2119 5980 +f 2119 2120 5959 +f 2119 5959 2181 +f 2119 2181 5980 +f 2120 6114 2182 +f 2120 2182 5959 +f 2120 2121 6114 +f 2121 2183 6114 +f 2121 6098 2183 +f 2121 2122 6098 +f 2122 2123 2185 +f 2122 2185 2184 +f 2122 2184 6098 +f 2123 2124 2185 +f 2123 5905 2124 +f 2124 5905 5829 +f 2124 5829 2125 +f 2124 2125 2186 +f 2124 2186 2185 +f 2125 2187 2186 +f 2125 2126 2188 +f 2125 2188 2187 +f 2125 5829 2126 +f 2126 2189 2188 +f 2126 2127 2189 +f 2127 2128 2190 +f 2127 2190 2189 +f 2128 2129 2190 +f 2129 2130 2192 +f 2129 2192 2191 +f 2129 2191 2190 +f 2130 5368 2192 +f 2130 2131 5368 +f 2131 2132 5368 +f 2132 5121 2194 +f 2132 2133 5121 +f 2132 2194 5368 +f 2133 2134 5121 +f 2134 2196 5121 +f 2134 2135 5298 +f 2134 5298 2196 +f 2135 2136 5298 +f 2136 2137 2199 +f 2136 2199 2198 +f 2136 2198 5298 +f 2137 6074 2138 +f 2137 2138 2199 +f 2138 6074 6069 +f 2138 2200 2199 +f 2138 2139 5975 +f 2138 5975 2200 +f 2138 6069 2139 +f 2139 6069 2140 +f 2139 2140 5975 +f 2140 2141 5612 +f 2140 2202 5975 +f 2140 5612 2202 +f 2141 2142 5612 +f 2142 5830 2143 +f 2142 2143 2204 +f 2142 2204 5612 +f 2143 5830 2144 +f 2143 2144 2206 +f 2143 2206 2205 +f 2143 2205 2204 +f 2144 5830 5141 +f 2144 5141 2145 +f 2144 2145 2206 +f 2145 2207 2206 +f 2145 5141 5167 +f 2145 5167 2146 +f 2145 2208 2207 +f 2145 2146 2208 +f 2146 2147 2208 +f 2147 2210 2209 +f 2147 2148 2210 +f 2147 2209 2208 +f 2148 5669 2149 +f 2148 2149 2211 +f 2148 2211 2210 +f 2149 2150 2211 +f 2149 5669 2150 +f 2150 2212 2211 +f 2150 2151 5328 +f 2150 5328 2212 +f 2151 2152 5328 +f 2152 5415 2214 +f 2152 2153 5415 +f 2152 2214 5328 +f 2153 2154 5415 +f 2154 2155 5211 +f 2154 2216 5415 +f 2154 5211 2216 +f 2155 2156 5211 +f 2156 2218 5211 +f 2156 2157 2218 +f 2157 2219 2218 +f 2157 2158 5234 +f 2157 5234 2219 +f 2158 2159 5234 +f 2159 2160 2222 +f 2159 2222 2221 +f 2159 2221 5234 +f 2160 2161 2222 +f 2161 2223 2222 +f 2161 2162 2224 +f 2161 2224 2223 +f 2162 2163 2225 +f 2162 2225 2224 +f 2163 2164 2225 +f 2164 2165 2226 +f 2164 2226 2225 +f 2165 2166 2227 +f 2165 2227 2226 +f 2166 5705 2167 +f 2166 2229 2228 +f 2166 2167 2229 +f 2166 2228 2227 +f 2167 4946 2229 +f 2167 2168 4946 +f 2167 5054 2168 +f 2168 4668 4667 +f 2168 4667 2230 +f 2168 2230 4946 +f 2169 2231 4668 +f 2169 4580 4581 +f 2169 4668 4669 +f 2169 4581 2231 +f 2170 4435 4467 +f 2170 4581 5942 +f 2170 2232 4582 +f 2170 4467 4434 +f 2170 4434 2232 +f 2170 4582 4581 +f 2171 4273 4274 +f 2171 4274 2233 +f 2171 2233 4801 +f 2171 4801 4802 +f 2172 4935 4936 +f 2172 4936 5362 +f 2172 2234 4935 +f 2173 4931 2235 +f 2174 5216 4084 +f 2174 4084 4083 +f 2174 4730 4731 +f 2174 4731 2236 +f 2174 2236 5216 +f 2175 5626 2237 +f 2175 4372 5079 +f 2175 2237 4372 +f 2176 2238 4518 +f 2176 4873 4874 +f 2176 4874 2238 +f 2176 4518 4517 +f 2177 2239 4873 +f 2177 2178 5050 +f 2177 5050 2239 +f 2177 4873 4872 +f 2178 2240 5050 +f 2178 5794 5265 +f 2178 5265 2240 +f 2179 5780 5794 +f 2179 5839 5780 +f 2180 5980 5882 +f 2180 5882 5839 +f 2181 5935 5980 +f 2181 5959 5935 +f 2182 6032 2244 +f 2182 6114 6032 +f 2182 2244 5959 +f 2183 6098 6092 +f 2183 6092 6114 +f 2184 2246 6098 +f 2184 2185 2247 +f 2184 2247 2246 +f 2185 2186 2247 +f 2186 2248 2247 +f 2186 2249 2248 +f 2186 2187 2249 +f 2187 2188 2250 +f 2187 2250 2249 +f 2188 2189 5649 +f 2188 5649 2250 +f 2189 2190 5649 +f 2190 2252 5649 +f 2190 5296 2252 +f 2190 2191 5296 +f 2191 5180 5296 +f 2191 2192 5180 +f 2192 2193 5180 +f 2192 5368 2193 +f 2193 2255 5180 +f 2193 5368 2194 +f 2193 2256 2255 +f 2193 2194 2256 +f 2194 2195 2256 +f 2194 5121 2195 +f 2195 2257 2256 +f 2195 5121 2196 +f 2195 2196 5726 +f 2195 5726 2257 +f 2196 2197 5671 +f 2196 5671 5726 +f 2196 5298 2197 +f 2197 5298 2198 +f 2197 5633 5671 +f 2197 2198 5633 +f 2198 5929 5633 +f 2198 2199 5929 +f 2199 2200 5929 +f 2200 2201 2262 +f 2200 2262 5929 +f 2200 5975 2201 +f 2201 2263 2262 +f 2201 5975 2202 +f 2201 2202 2263 +f 2202 5612 2203 +f 2202 2203 5299 +f 2202 2264 2263 +f 2202 5299 2264 +f 2203 2204 5299 +f 2203 5612 2204 +f 2204 2205 2266 +f 2204 2266 5299 +f 2205 2206 2268 +f 2205 2268 2267 +f 2205 2267 2266 +f 2206 2207 2268 +f 2207 2269 2268 +f 2207 2208 4966 +f 2207 4966 2269 +f 2208 2209 4966 +f 2209 2271 4966 +f 2209 5144 2271 +f 2209 2210 5144 +f 2210 2211 5144 +f 2211 6162 5144 +f 2211 2212 6162 +f 2212 2274 6162 +f 2212 2275 2274 +f 2212 5328 2213 +f 2212 2213 2275 +f 2213 2214 2275 +f 2213 5328 2214 +f 2214 5268 2276 +f 2214 5415 2215 +f 2214 2215 5268 +f 2214 2276 2275 +f 2215 5415 2216 +f 2215 2216 5268 +f 2216 2278 5268 +f 2216 5211 2217 +f 2216 2217 2279 +f 2216 2279 2278 +f 2217 2218 2279 +f 2217 5211 2218 +f 2218 2280 2279 +f 2218 2219 2281 +f 2218 2281 2280 +f 2219 2220 2281 +f 2219 5234 2220 +f 2220 2282 2281 +f 2220 5234 2221 +f 2220 2221 2282 +f 2221 2222 2284 +f 2221 2283 2282 +f 2221 2284 2283 +f 2222 2223 2284 +f 2223 2224 2285 +f 2223 2285 2284 +f 2224 2286 2285 +f 2224 2225 2286 +f 2225 2226 2287 +f 2225 2287 2286 +f 2226 2288 2287 +f 2226 2227 5106 +f 2226 5106 2288 +f 2227 2289 5106 +f 2227 2290 2289 +f 2227 2228 2290 +f 2228 2291 2290 +f 2228 2229 2291 +f 2229 4946 2291 +f 2230 5076 4946 +f 2230 2292 5076 +f 2230 4667 2292 +f 2231 4667 4668 +f 2231 4581 4582 +f 2231 4582 2293 +f 2231 2293 4666 +f 2231 4666 4667 +f 2232 2294 4583 +f 2232 4583 4582 +f 2232 4434 6029 +f 2232 6029 2294 +f 2233 4275 2295 +f 2233 4800 4801 +f 2233 2295 4799 +f 2233 4799 4800 +f 2233 4274 4275 +f 2234 2296 4935 +f 2235 4931 4930 +f 2235 4930 4928 +f 2235 4928 2297 +f 2236 2298 5216 +f 2236 4731 2298 +f 2237 5626 4519 +f 2237 2299 4373 +f 2237 4520 2299 +f 2237 4519 4520 +f 2237 4373 4401 +f 2237 4401 4372 +f 2238 4519 4518 +f 2238 2300 5247 +f 2238 5247 4519 +f 2238 4874 2300 +f 2239 5050 2301 +f 2239 4875 4874 +f 2239 2301 4875 +f 2239 4874 4873 +f 2240 2302 5050 +f 2240 4961 2302 +f 2240 5265 4961 +f 2241 5037 2303 +f 2241 5780 5037 +f 2241 5265 5780 +f 2241 4961 5265 +f 2241 2303 4961 +f 2242 6099 2304 +f 2242 5936 6099 +f 2242 5037 5882 +f 2242 2304 5037 +f 2242 5882 5936 +f 2243 6079 6073 +f 2243 6073 5936 +f 2243 5936 5935 +f 2243 5935 6079 +f 2244 6079 5959 +f 2244 6032 6026 +f 2244 6026 6079 +f 2245 6031 6032 +f 2245 6060 6031 +f 2245 6032 6092 +f 2245 6092 6060 +f 2246 6060 6098 +f 2246 6048 6060 +f 2246 2247 6048 +f 2247 2248 2309 +f 2247 2309 6048 +f 2248 2310 2309 +f 2248 2249 2310 +f 2249 2311 2310 +f 2249 2250 2312 +f 2249 2312 2311 +f 2250 5649 2251 +f 2250 2251 2312 +f 2251 2252 2314 +f 2251 2314 2313 +f 2251 2313 2312 +f 2251 5649 2252 +f 2252 2253 2314 +f 2252 5296 2253 +f 2253 2315 2314 +f 2253 2254 2315 +f 2253 5296 5180 +f 2253 5180 2254 +f 2254 2316 2315 +f 2254 5180 2255 +f 2254 2255 2316 +f 2255 5032 2317 +f 2255 2317 2316 +f 2255 2256 5032 +f 2256 5535 5032 +f 2256 2257 5535 +f 2257 5163 5535 +f 2257 2258 5163 +f 2257 5726 2258 +f 2258 5671 2259 +f 2258 5726 5671 +f 2258 2259 5163 +f 2259 5671 5633 +f 2259 5633 2260 +f 2259 2321 5163 +f 2259 2260 2322 +f 2259 2322 2321 +f 2260 5929 2261 +f 2260 2261 2323 +f 2260 2323 2322 +f 2260 5633 5929 +f 2261 5929 2262 +f 2261 5475 2323 +f 2261 2262 5475 +f 2262 2263 5475 +f 2263 5523 2325 +f 2263 2325 5475 +f 2263 2264 5523 +f 2264 5358 5523 +f 2264 5299 2265 +f 2264 2265 5358 +f 2265 5299 2266 +f 2265 2266 5014 +f 2265 5014 5358 +f 2266 2267 5014 +f 2267 2268 5124 +f 2267 2329 5014 +f 2267 5124 2329 +f 2268 2269 5124 +f 2269 2270 6153 +f 2269 6153 5124 +f 2269 4966 2270 +f 2270 6157 6153 +f 2270 4966 2271 +f 2270 2271 6157 +f 2271 5144 2272 +f 2271 2272 6149 +f 2271 6149 6157 +f 2272 5144 6161 +f 2272 6161 6150 +f 2272 6150 6149 +f 2273 6161 6162 +f 2273 2274 5209 +f 2273 5209 6161 +f 2273 6162 2274 +f 2274 2336 5209 +f 2274 2337 2336 +f 2274 2275 2337 +f 2275 2276 2337 +f 2276 2339 2338 +f 2276 2277 2339 +f 2276 5268 2277 +f 2276 2338 2337 +f 2277 5268 2278 +f 2277 6129 2339 +f 2277 2278 6129 +f 2278 2279 6129 +f 2279 5123 6129 +f 2279 2280 5123 +f 2280 5902 2342 +f 2280 2281 5902 +f 2280 2342 5123 +f 2281 2282 5902 +f 2282 2344 5902 +f 2282 2345 2344 +f 2282 2283 2345 +f 2283 2284 2346 +f 2283 2346 2345 +f 2284 2285 2346 +f 2285 2347 2346 +f 2285 2286 2348 +f 2285 2348 2347 +f 2286 2287 2348 +f 2287 2350 2349 +f 2287 2288 2350 +f 2287 2349 2348 +f 2288 5106 2350 +f 2289 2351 5106 +f 2289 5007 2351 +f 2289 2290 5007 +f 2290 2291 2352 +f 2290 2352 5007 +f 2291 4946 5076 +f 2291 2353 2352 +f 2291 5076 2353 +f 2292 4665 2354 +f 2292 4667 4666 +f 2292 4666 4665 +f 2292 2354 5076 +f 2293 2355 4666 +f 2293 4583 2355 +f 2293 4582 4583 +f 2294 4584 4583 +f 2294 2356 4584 +f 2294 6029 2356 +f 2295 5743 2357 +f 2295 4275 4278 +f 2295 4278 5743 +f 2295 2357 4799 +f 2296 2358 4937 +f 2296 4937 4934 +f 2296 4934 4935 +f 2297 4928 5579 +f 2297 5579 2359 +f 2298 4733 2360 +f 2298 4732 4733 +f 2298 2360 4091 +f 2298 4731 4732 +f 2298 4091 5216 +f 2299 2361 4373 +f 2299 4520 4521 +f 2299 4521 2361 +f 2300 4874 4875 +f 2300 2362 5247 +f 2300 4876 2362 +f 2300 4875 4876 +f 2301 2363 4875 +f 2301 5050 6041 +f 2301 6041 2363 +f 2302 6041 5050 +f 2302 2364 6016 +f 2302 6016 6041 +f 2302 4961 2364 +f 2303 2365 4961 +f 2303 2366 2365 +f 2303 2304 2366 +f 2303 5037 2304 +f 2304 6115 2366 +f 2304 6099 6115 +f 2305 6013 2367 +f 2305 2367 6099 +f 2305 6073 6013 +f 2305 6099 6073 +f 2306 6031 2307 +f 2306 2368 6013 +f 2306 2369 2368 +f 2306 6026 6031 +f 2306 2307 2369 +f 2306 6013 6026 +f 2307 6048 2308 +f 2307 6031 6048 +f 2307 2370 2369 +f 2307 2308 2370 +f 2308 2371 2370 +f 2308 6048 2309 +f 2308 2309 2371 +f 2309 2310 2371 +f 2310 2372 2371 +f 2310 2311 2373 +f 2310 2373 2372 +f 2311 5387 2373 +f 2311 2312 5387 +f 2312 2313 5387 +f 2313 2376 2375 +f 2313 2314 2376 +f 2313 2375 5387 +f 2314 2377 2376 +f 2314 2315 2377 +f 2315 5068 2377 +f 2315 2316 5068 +f 2316 2317 5068 +f 2317 2379 5068 +f 2317 2318 2379 +f 2317 5032 2318 +f 2318 2380 2379 +f 2318 5535 2319 +f 2318 2319 2380 +f 2318 5032 5535 +f 2319 2320 2382 +f 2319 2382 2381 +f 2319 2381 2380 +f 2319 5535 5163 +f 2319 5163 2320 +f 2320 5163 2321 +f 2320 2321 2382 +f 2321 2322 2384 +f 2321 2383 2382 +f 2321 2384 2383 +f 2322 2323 2384 +f 2323 2324 5042 +f 2323 5042 2385 +f 2323 5475 2324 +f 2323 2385 2384 +f 2324 2325 5042 +f 2324 5475 2325 +f 2325 2387 5042 +f 2325 2326 2387 +f 2325 5523 2326 +f 2326 2388 2387 +f 2326 2389 2388 +f 2326 5358 2327 +f 2326 5523 5358 +f 2326 2327 2389 +f 2327 5358 5014 +f 2327 5014 2328 +f 2327 2328 2390 +f 2327 2390 2389 +f 2328 5014 2329 +f 2328 2329 2390 +f 2329 5124 2330 +f 2329 2391 2390 +f 2329 2330 5441 +f 2329 5441 2391 +f 2330 2331 5441 +f 2330 5124 2331 +f 2331 6153 2332 +f 2331 5124 6153 +f 2331 2393 5441 +f 2331 5563 2393 +f 2331 2332 5563 +f 2332 6153 6157 +f 2332 6157 2333 +f 2332 2333 5563 +f 2333 6157 6149 +f 2333 6152 6101 +f 2333 6148 6152 +f 2333 6149 6148 +f 2333 6101 5563 +f 2334 6150 6154 +f 2334 6151 6148 +f 2334 6148 6150 +f 2334 6154 6151 +f 2335 5209 2336 +f 2335 6066 6156 +f 2335 6156 6154 +f 2335 6154 5209 +f 2335 2336 6066 +f 2336 2337 6086 +f 2336 6086 6066 +f 2337 2338 5231 +f 2337 5231 6086 +f 2338 2339 6164 +f 2338 6164 5231 +f 2339 2401 6164 +f 2339 6129 2340 +f 2339 5044 2401 +f 2339 2340 5044 +f 2340 2341 5044 +f 2340 6129 2341 +f 2341 2342 5421 +f 2341 5123 2342 +f 2341 6129 5123 +f 2341 5421 5044 +f 2342 6134 5421 +f 2342 2343 6134 +f 2342 5902 2343 +f 2343 2344 2406 +f 2343 2406 2405 +f 2343 2405 6134 +f 2343 5902 2344 +f 2344 2345 2406 +f 2345 2407 2406 +f 2345 2346 2408 +f 2345 2408 2407 +f 2346 2347 2408 +f 2347 2348 2409 +f 2347 2409 2408 +f 2348 2410 2409 +f 2348 2349 2411 +f 2348 2411 2410 +f 2349 2412 2411 +f 2349 2350 2412 +f 2350 5106 2351 +f 2350 2351 5817 +f 2350 5817 2412 +f 2351 5007 5574 +f 2351 5574 2413 +f 2351 2413 5817 +f 2352 5574 5007 +f 2352 2353 2414 +f 2352 2414 5574 +f 2353 5070 2415 +f 2353 2415 2414 +f 2353 5076 2354 +f 2353 2354 5070 +f 2354 4665 4664 +f 2354 4664 2416 +f 2354 2416 5070 +f 2355 4665 4666 +f 2355 4584 5352 +f 2355 5352 2417 +f 2355 2417 4665 +f 2355 4583 4584 +f 2356 4433 5403 +f 2356 5403 2418 +f 2356 6029 4433 +f 2356 2418 4585 +f 2356 4585 4584 +f 2357 2419 4798 +f 2357 5743 2419 +f 2357 4798 4799 +f 2358 4938 4937 +f 2358 2420 4938 +f 2359 5579 2421 +f 2360 4733 4734 +f 2360 4734 2422 +f 2360 2422 4098 +f 2360 4098 4092 +f 2360 4092 4091 +f 2361 5753 4400 +f 2361 2423 5753 +f 2361 4522 2423 +f 2361 4400 4373 +f 2361 4521 4522 +f 2362 4521 5247 +f 2362 4877 2424 +f 2362 4876 4877 +f 2362 2424 4521 +f 2363 4876 4875 +f 2363 6041 6016 +f 2363 2425 4876 +f 2363 6016 2425 +f 2364 4961 2365 +f 2364 5164 2426 +f 2364 2365 5164 +f 2364 2426 5000 +f 2364 5000 6016 +f 2365 2366 2428 +f 2365 2428 2427 +f 2365 2427 5164 +f 2366 6115 2428 +f 2367 2368 2429 +f 2367 6115 6099 +f 2367 2429 6115 +f 2367 6013 2368 +f 2368 2430 2429 +f 2368 2369 2431 +f 2368 2431 2430 +f 2369 2432 2431 +f 2369 2370 2432 +f 2370 2433 2432 +f 2370 2371 2433 +f 2371 2372 2433 +f 2372 2373 2434 +f 2372 2434 2433 +f 2373 2435 2434 +f 2373 2436 2435 +f 2373 5387 2374 +f 2373 2374 2436 +f 2374 5387 2375 +f 2374 2375 2437 +f 2374 2437 2436 +f 2375 2376 2437 +f 2376 2377 2438 +f 2376 2438 2437 +f 2377 2439 2438 +f 2377 5068 2378 +f 2377 2440 2439 +f 2377 2378 2440 +f 2378 2441 2440 +f 2378 5068 2379 +f 2378 2379 2441 +f 2379 2380 2441 +f 2380 2442 2441 +f 2380 2381 5093 +f 2380 5093 2442 +f 2381 2382 5093 +f 2382 2444 5093 +f 2382 2383 2444 +f 2383 5251 2445 +f 2383 2384 5251 +f 2383 2445 2444 +f 2384 2385 5251 +f 2385 5042 2386 +f 2385 2448 2447 +f 2385 2386 2448 +f 2385 2447 5251 +f 2386 5687 2448 +f 2386 2387 5687 +f 2386 5042 2387 +f 2387 2388 5058 +f 2387 5058 5687 +f 2388 2389 5058 +f 2389 2390 2452 +f 2389 2452 2451 +f 2389 2451 5058 +f 2390 2453 2452 +f 2390 2391 2453 +f 2391 5441 2392 +f 2391 5735 2453 +f 2391 2392 5735 +f 2392 5441 2393 +f 2392 2393 5735 +f 2393 2394 5884 +f 2393 5884 2455 +f 2393 2455 5735 +f 2393 5563 2394 +f 2394 6132 5884 +f 2394 6112 6132 +f 2394 5563 6112 +f 2395 6102 6103 +f 2395 6103 6144 +f 2395 6144 6104 +f 2395 6104 6132 +f 2395 6132 6102 +f 2396 6141 6143 +f 2396 6140 6145 +f 2396 6145 6141 +f 2396 6143 6100 +f 2396 6100 6140 +f 2397 6136 6142 +f 2397 6139 6066 +f 2397 6137 6136 +f 2397 6066 6084 +f 2397 6084 6085 +f 2397 6142 6139 +f 2397 6085 6137 +f 2398 6086 6159 +f 2398 6159 6163 +f 2398 6163 6084 +f 2398 6084 6086 +f 2399 5231 2400 +f 2399 6159 5231 +f 2399 6160 6159 +f 2399 2400 6160 +f 2400 6164 2401 +f 2400 2462 6160 +f 2400 5231 6164 +f 2400 4987 2462 +f 2400 2401 4987 +f 2401 5044 2402 +f 2401 2402 4987 +f 2402 5044 2403 +f 2402 5384 2464 +f 2402 2464 4987 +f 2402 2403 5384 +f 2403 5044 5421 +f 2403 5421 2404 +f 2403 2404 5384 +f 2404 6080 5384 +f 2404 2405 6080 +f 2404 6134 2405 +f 2404 5421 6134 +f 2405 2467 6080 +f 2405 2406 2467 +f 2406 2407 2469 +f 2406 2469 2468 +f 2406 2468 2467 +f 2407 2470 2469 +f 2407 2408 2470 +f 2408 2409 2470 +f 2409 2471 2470 +f 2409 2410 2471 +f 2410 2411 2472 +f 2410 2472 2471 +f 2411 2474 2473 +f 2411 2412 2474 +f 2411 2473 2472 +f 2412 5817 5820 +f 2412 5820 2474 +f 2413 5574 5806 +f 2413 5806 5817 +f 2414 2476 5574 +f 2414 2415 2476 +f 2415 5070 2477 +f 2415 2477 2476 +f 2416 4663 2478 +f 2416 4664 4663 +f 2416 2478 5070 +f 2417 5490 4664 +f 2417 5352 5490 +f 2417 4664 4665 +f 2418 5403 2480 +f 2418 4586 4585 +f 2418 2480 4586 +f 2419 5743 4282 +f 2419 4797 4798 +f 2419 2481 4797 +f 2419 4289 2481 +f 2419 4282 4289 +f 2420 6165 4938 +f 2420 2482 6165 +f 2421 5579 5558 +f 2421 5570 2483 +f 2421 5558 5570 +f 2422 4734 4735 +f 2422 4735 2484 +f 2422 2484 4098 +f 2423 4522 4523 +f 2423 4523 2485 +f 2423 2485 5753 +f 2424 2486 4522 +f 2424 4878 2486 +f 2424 4877 4878 +f 2424 4522 4521 +f 2425 5000 5400 +f 2425 5400 2487 +f 2425 2487 4877 +f 2425 4877 4876 +f 2425 6016 5000 +f 2426 2488 5000 +f 2426 5164 2488 +f 2427 2428 2489 +f 2427 2489 5164 +f 2428 2490 2489 +f 2428 6115 2429 +f 2428 2491 2490 +f 2428 2429 2491 +f 2429 2492 2491 +f 2429 2430 2492 +f 2430 2431 2492 +f 2431 2432 2493 +f 2431 2493 2492 +f 2432 2494 2493 +f 2432 2433 2495 +f 2432 2495 2494 +f 2433 2434 2495 +f 2434 2435 2496 +f 2434 2496 2495 +f 2435 2497 2496 +f 2435 2436 2497 +f 2436 2498 2497 +f 2436 2437 2498 +f 2437 2438 5172 +f 2437 5172 2499 +f 2437 2499 2498 +f 2438 2439 5172 +f 2439 2440 2502 +f 2439 2502 2501 +f 2439 2501 5172 +f 2440 2441 2503 +f 2440 2503 2502 +f 2441 2504 2503 +f 2441 2442 2504 +f 2442 5093 2443 +f 2442 2443 2504 +f 2443 5093 2444 +f 2443 2505 2504 +f 2443 2444 2506 +f 2443 2506 2505 +f 2444 2445 2506 +f 2445 2446 2508 +f 2445 2508 2507 +f 2445 5251 2446 +f 2445 2507 2506 +f 2446 2447 5133 +f 2446 5251 2447 +f 2446 5133 2508 +f 2447 2448 5133 +f 2448 2510 5133 +f 2448 2511 2510 +f 2448 5687 2449 +f 2448 2449 2511 +f 2449 2450 2511 +f 2449 5687 5058 +f 2449 5058 2450 +f 2450 2451 2512 +f 2450 2512 2511 +f 2450 5058 2451 +f 2451 2513 2512 +f 2451 2452 4926 +f 2451 4926 2513 +f 2452 2453 4926 +f 2453 2454 5125 +f 2453 5735 2454 +f 2453 5125 4926 +f 2454 2516 5125 +f 2454 5966 2516 +f 2454 5735 2455 +f 2454 2455 5966 +f 2455 5884 2456 +f 2455 2456 5966 +f 2456 5884 2457 +f 2456 6125 5966 +f 2456 2457 6125 +f 2457 6104 6147 +f 2457 6147 2458 +f 2457 2458 6138 +f 2457 5884 6104 +f 2457 6138 6125 +f 2458 2520 6138 +f 2458 6147 2459 +f 2458 2459 2521 +f 2458 2521 2520 +f 2459 2460 5520 +f 2459 6085 2460 +f 2459 6147 6085 +f 2459 5520 2521 +f 2460 6085 5212 +f 2460 5212 2461 +f 2460 2461 5520 +f 2461 5212 6160 +f 2461 2524 2523 +f 2461 2523 5520 +f 2461 2462 2524 +f 2461 6160 2462 +f 2462 2463 2524 +f 2462 4987 2463 +f 2463 2525 2524 +f 2463 2464 2526 +f 2463 2526 2525 +f 2463 4987 2464 +f 2464 6121 2526 +f 2464 2465 6121 +f 2464 5384 2465 +f 2465 2466 6121 +f 2465 5384 2466 +f 2466 2467 5878 +f 2466 6080 2467 +f 2466 5384 6080 +f 2466 5878 6121 +f 2467 2529 5878 +f 2467 2468 2529 +f 2468 2530 2529 +f 2468 5585 2530 +f 2468 2469 5585 +f 2469 2470 5585 +f 2470 2532 5585 +f 2470 2471 2532 +f 2471 2472 2534 +f 2471 2534 2533 +f 2471 2533 2532 +f 2472 2473 2535 +f 2472 2535 2534 +f 2473 2474 2535 +f 2474 2536 2535 +f 2474 5820 2536 +f 2475 5806 2476 +f 2475 2538 2537 +f 2475 5820 5806 +f 2475 2476 2538 +f 2475 2537 5820 +f 2476 5806 5574 +f 2476 2477 2538 +f 2477 2478 2539 +f 2477 2539 2538 +f 2477 5070 2478 +f 2478 2540 2539 +f 2478 4663 2540 +f 2479 2541 4662 +f 2479 4662 4663 +f 2479 4663 5490 +f 2479 5490 5352 +f 2479 4586 2541 +f 2479 5352 4586 +f 2480 5403 4432 +f 2480 4432 2542 +f 2480 2542 4587 +f 2480 4587 4586 +f 2481 4795 4796 +f 2481 4289 2543 +f 2481 2543 4795 +f 2481 4796 4797 +f 2482 2544 6165 +f 2483 5570 2545 +f 2484 4735 2546 +f 2484 4099 4098 +f 2484 2546 4099 +f 2485 4523 4524 +f 2485 2547 5017 +f 2485 5017 4399 +f 2485 4524 2547 +f 2485 4399 5753 +f 2486 2548 5791 +f 2486 4523 4522 +f 2486 4878 2548 +f 2486 5791 4523 +f 2487 2549 4879 +f 2487 4879 4878 +f 2487 5400 2549 +f 2487 4878 4877 +f 2488 5922 5400 +f 2488 2550 5922 +f 2488 2489 2550 +f 2488 5164 2489 +f 2488 5400 5000 +f 2489 5910 2551 +f 2489 2490 5910 +f 2489 2551 2550 +f 2490 2491 2552 +f 2490 2552 5910 +f 2491 2553 2552 +f 2491 2492 2553 +f 2492 2555 2554 +f 2492 2554 2553 +f 2492 2493 2555 +f 2493 2494 2556 +f 2493 2556 2555 +f 2494 2495 2556 +f 2495 2558 2557 +f 2495 2496 2558 +f 2495 2557 2556 +f 2496 5294 2558 +f 2496 2497 5294 +f 2497 2498 5294 +f 2498 2499 4887 +f 2498 2560 5294 +f 2498 4887 2560 +f 2499 5172 2500 +f 2499 2500 4887 +f 2500 5172 2501 +f 2500 4918 2562 +f 2500 2501 4918 +f 2500 2562 4887 +f 2501 2502 4918 +f 2502 2503 2564 +f 2502 2564 4918 +f 2503 2565 2564 +f 2503 2504 2565 +f 2504 2567 2566 +f 2504 2505 2567 +f 2504 2566 2565 +f 2505 2506 2567 +f 2506 2568 2567 +f 2506 2507 2569 +f 2506 2569 2568 +f 2507 5288 2569 +f 2507 2508 5288 +f 2508 5133 2509 +f 2508 2509 5710 +f 2508 5710 5288 +f 2509 5133 2510 +f 2509 2510 5710 +f 2510 5541 2572 +f 2510 2511 5541 +f 2510 2572 5710 +f 2511 2512 5541 +f 2512 5019 2574 +f 2512 2574 5541 +f 2512 2513 5019 +f 2513 4926 2514 +f 2513 2514 5019 +f 2514 4926 2515 +f 2514 2515 2576 +f 2514 2576 5019 +f 2515 5125 2516 +f 2515 6107 2577 +f 2515 2516 6107 +f 2515 4926 5125 +f 2515 2577 2576 +f 2516 2517 6107 +f 2516 5966 2517 +f 2517 5966 2518 +f 2517 2518 5981 +f 2517 5981 6107 +f 2518 5966 6125 +f 2518 6125 2519 +f 2518 2580 5981 +f 2518 2519 5816 +f 2518 5816 2580 +f 2519 6125 6138 +f 2519 2520 5816 +f 2519 6138 2520 +f 2520 2582 5816 +f 2520 5181 2582 +f 2520 2521 5181 +f 2521 5520 2522 +f 2521 2522 5181 +f 2522 2584 5181 +f 2522 2523 2584 +f 2522 5520 2523 +f 2523 2585 2584 +f 2523 2524 2585 +f 2524 2587 2586 +f 2524 2525 2587 +f 2524 2586 2585 +f 2525 2526 2587 +f 2526 2588 2587 +f 2526 2527 2588 +f 2526 6121 2527 +f 2527 6121 2528 +f 2527 2590 2589 +f 2527 2528 2590 +f 2527 2589 2588 +f 2528 6121 5878 +f 2528 5878 2529 +f 2528 2529 2590 +f 2529 2530 2592 +f 2529 2592 2591 +f 2529 2591 2590 +f 2530 5585 2531 +f 2530 2531 2592 +f 2531 5585 2532 +f 2531 2594 2593 +f 2531 2593 2592 +f 2531 2532 2594 +f 2532 2595 2594 +f 2532 2533 2595 +f 2533 2534 2595 +f 2534 2535 2596 +f 2534 2596 2595 +f 2535 2597 2596 +f 2535 2598 2597 +f 2535 2536 2598 +f 2536 5820 2537 +f 2536 2599 2598 +f 2536 2537 2599 +f 2537 2538 2599 +f 2538 2601 2600 +f 2538 2600 2599 +f 2538 2539 2601 +f 2539 2602 2601 +f 2539 2540 2602 +f 2540 4662 4661 +f 2540 4661 2602 +f 2540 4663 4662 +f 2541 2603 4662 +f 2541 4587 5921 +f 2541 5921 2603 +f 2541 4586 4587 +f 2542 5752 2604 +f 2542 2604 4588 +f 2542 4588 4587 +f 2542 4470 5752 +f 2542 4432 4470 +f 2543 2605 4795 +f 2543 4290 2605 +f 2543 4289 4290 +f 2544 2606 4891 +f 2544 4891 4892 +f 2544 4892 6165 +f 2545 5570 5345 +f 2545 5345 5346 +f 2545 5346 2607 +f 2546 4735 4736 +f 2546 4737 2608 +f 2546 4100 4099 +f 2546 4736 4737 +f 2546 2608 4100 +f 2547 4525 2609 +f 2547 4524 4525 +f 2547 2609 5017 +f 2548 2610 5791 +f 2548 4878 4879 +f 2548 4879 4880 +f 2548 4880 2610 +f 2549 5922 5137 +f 2549 2611 4879 +f 2549 5137 2611 +f 2549 5400 5922 +f 2550 5757 2612 +f 2550 2551 5757 +f 2550 2612 5922 +f 2551 2613 5757 +f 2551 5910 2613 +f 2552 2614 5910 +f 2552 2553 2615 +f 2552 2615 2614 +f 2553 2554 2616 +f 2553 2616 2615 +f 2554 2555 2616 +f 2555 2556 2617 +f 2555 2617 2616 +f 2556 2557 2618 +f 2556 2618 2617 +f 2557 2620 2619 +f 2557 2558 2620 +f 2557 2619 2618 +f 2558 2621 2620 +f 2558 2559 2621 +f 2558 5294 2559 +f 2559 2560 2621 +f 2559 5294 2560 +f 2560 2622 2621 +f 2560 2561 2622 +f 2560 4887 2561 +f 2561 2623 2622 +f 2561 2562 2623 +f 2561 4887 2562 +f 2562 2624 2623 +f 2562 4918 2563 +f 2562 2625 2624 +f 2562 2563 2625 +f 2563 4918 2564 +f 2563 2564 2625 +f 2564 2565 2626 +f 2564 2626 2625 +f 2565 2628 2627 +f 2565 2627 2626 +f 2565 2566 2628 +f 2566 2567 2629 +f 2566 2629 2628 +f 2567 4919 2629 +f 2567 2568 4919 +f 2568 2569 4919 +f 2569 5288 2570 +f 2569 4679 2631 +f 2569 2631 4919 +f 2569 2570 4679 +f 2570 2571 4679 +f 2570 5710 2571 +f 2570 5288 5710 +f 2571 2633 4679 +f 2571 2634 2633 +f 2571 2572 2634 +f 2571 5710 2572 +f 2572 5541 2573 +f 2572 2573 2634 +f 2573 2574 5150 +f 2573 2635 2634 +f 2573 5541 2574 +f 2573 5150 2635 +f 2574 5019 2575 +f 2574 2575 5150 +f 2575 2576 5201 +f 2575 5019 2576 +f 2575 5201 2637 +f 2575 2637 5150 +f 2576 2577 5201 +f 2577 2578 5923 +f 2577 5923 5201 +f 2577 6107 2578 +f 2578 6107 2579 +f 2578 2640 5923 +f 2578 2641 2640 +f 2578 2579 2641 +f 2579 5981 2580 +f 2579 2580 6004 +f 2579 6004 2641 +f 2579 6107 5981 +f 2580 2581 6004 +f 2580 5816 2581 +f 2581 5816 2582 +f 2581 2582 5351 +f 2581 5351 6004 +f 2582 5849 5351 +f 2582 2583 5849 +f 2582 5181 2583 +f 2583 2584 5464 +f 2583 5464 5849 +f 2583 5181 2584 +f 2584 2646 5464 +f 2584 5964 2646 +f 2584 2585 5964 +f 2585 2586 5964 +f 2586 2587 5102 +f 2586 5102 2648 +f 2586 2648 5964 +f 2587 2588 5102 +f 2588 2589 2650 +f 2588 2650 5102 +f 2589 2590 2651 +f 2589 2651 2650 +f 2590 2591 2653 +f 2590 2652 2651 +f 2590 2653 2652 +f 2591 2654 2653 +f 2591 2592 2654 +f 2592 2593 2654 +f 2593 2655 2654 +f 2593 2594 2655 +f 2594 2595 2657 +f 2594 2657 2656 +f 2594 2656 2655 +f 2595 2658 2657 +f 2595 2596 2658 +f 2596 2597 2659 +f 2596 2659 2658 +f 2597 2598 2659 +f 2598 2660 2659 +f 2598 2599 2660 +f 2599 2662 2661 +f 2599 2600 2662 +f 2599 2661 2660 +f 2600 2663 2662 +f 2600 2601 2663 +f 2601 5960 2663 +f 2601 2602 5960 +f 2602 4661 4660 +f 2602 4660 2664 +f 2602 2664 5960 +f 2603 4661 4662 +f 2603 2665 4661 +f 2603 5921 2665 +f 2604 4589 4588 +f 2604 5752 2666 +f 2604 2666 4589 +f 2605 2667 4794 +f 2605 4794 4795 +f 2605 4291 2667 +f 2605 4290 4291 +f 2606 2668 4891 +f 2607 5346 2669 +f 2608 2670 5377 +f 2608 5377 4103 +f 2608 4103 4100 +f 2608 4737 4738 +f 2608 4738 2670 +f 2609 4398 5017 +f 2609 5716 4398 +f 2609 2671 5716 +f 2609 4526 2671 +f 2609 4525 4526 +f 2610 4525 5791 +f 2610 2672 4525 +f 2610 4880 4881 +f 2610 4881 2672 +f 2611 5895 4880 +f 2611 2673 5895 +f 2611 4880 4879 +f 2611 5137 5901 +f 2611 5901 2673 +f 2612 5757 5896 +f 2612 5896 5137 +f 2612 5137 5922 +f 2613 2675 5897 +f 2613 5897 5757 +f 2613 5910 2614 +f 2613 2614 2675 +f 2614 2615 2676 +f 2614 2676 2675 +f 2615 2678 2677 +f 2615 2677 2676 +f 2615 2616 2678 +f 2616 2617 2679 +f 2616 2679 2678 +f 2617 2618 2679 +f 2618 2680 2679 +f 2618 2619 2680 +f 2619 2681 2680 +f 2619 2682 2681 +f 2619 2620 2682 +f 2620 2621 2682 +f 2621 2683 2682 +f 2621 2684 2683 +f 2621 2622 2684 +f 2622 2623 5885 +f 2622 5885 2684 +f 2623 2624 5885 +f 2624 2686 5885 +f 2624 2625 2686 +f 2625 2626 2687 +f 2625 2687 2686 +f 2626 2627 5676 +f 2626 5676 2688 +f 2626 2688 2687 +f 2627 2628 5029 +f 2627 5029 5676 +f 2628 2629 5029 +f 2629 2691 5029 +f 2629 4919 2630 +f 2629 2630 2691 +f 2630 4919 2631 +f 2630 2631 4977 +f 2630 4977 2692 +f 2630 2692 2691 +f 2631 2632 5660 +f 2631 4679 2632 +f 2631 5660 4977 +f 2632 5857 5660 +f 2632 2633 5857 +f 2632 4679 2633 +f 2633 2634 5857 +f 2634 2696 5857 +f 2634 2635 2697 +f 2634 2697 2696 +f 2635 2698 2697 +f 2635 2636 2698 +f 2635 5150 2636 +f 2636 2637 2698 +f 2636 5150 2637 +f 2637 5201 2638 +f 2637 2638 2700 +f 2637 2700 2699 +f 2637 2699 2698 +f 2638 2639 5308 +f 2638 5201 2639 +f 2638 5308 2700 +f 2639 5923 2640 +f 2639 2640 5308 +f 2639 5201 5923 +f 2640 2702 5308 +f 2640 2641 2702 +f 2641 2642 4972 +f 2641 4972 2703 +f 2641 6004 2642 +f 2641 2703 2702 +f 2642 2643 4972 +f 2642 6004 2643 +f 2643 5747 4972 +f 2643 6004 5351 +f 2643 5351 2644 +f 2643 2644 5747 +f 2644 5351 5849 +f 2644 5517 5747 +f 2644 2645 5517 +f 2644 5849 2645 +f 2645 2646 5640 +f 2645 5640 5517 +f 2645 5464 2646 +f 2645 5849 5464 +f 2646 2708 5640 +f 2646 5964 2647 +f 2646 2647 2708 +f 2647 5964 2648 +f 2647 2648 2710 +f 2647 2710 2709 +f 2647 2709 2708 +f 2648 5102 2649 +f 2648 2711 2710 +f 2648 2649 2711 +f 2649 5102 2650 +f 2649 5158 2711 +f 2649 2650 5158 +f 2650 2651 5158 +f 2651 2713 5158 +f 2651 2714 2713 +f 2651 2652 2714 +f 2652 2653 2714 +f 2653 2654 2715 +f 2653 2715 2714 +f 2654 2655 2717 +f 2654 2717 2716 +f 2654 2716 2715 +f 2655 2656 2717 +f 2656 2718 2717 +f 2656 2719 2718 +f 2656 2657 2719 +f 2657 2658 2719 +f 2658 2659 2720 +f 2658 2720 2719 +f 2659 2660 2721 +f 2659 2721 2720 +f 2660 2661 2723 +f 2660 2723 2722 +f 2660 2722 2721 +f 2661 2662 5267 +f 2661 5267 2723 +f 2662 2724 5267 +f 2662 2663 2725 +f 2662 2725 2724 +f 2663 5960 2725 +f 2664 4660 4659 +f 2664 4659 2726 +f 2664 2726 5960 +f 2665 5921 4589 +f 2665 4660 4661 +f 2665 2727 4660 +f 2665 4589 2727 +f 2666 5752 4471 +f 2666 2728 4590 +f 2666 4590 4589 +f 2666 4430 2728 +f 2666 4471 4430 +f 2667 4793 4794 +f 2667 4291 4294 +f 2667 4294 2729 +f 2667 2729 4793 +f 2668 4890 4891 +f 2668 2730 4889 +f 2668 4889 4890 +f 2669 5347 5348 +f 2669 5348 2731 +f 2669 5346 5347 +f 2670 2732 5377 +f 2670 4738 4739 +f 2670 4739 2732 +f 2671 2733 5716 +f 2671 4526 2733 +f 2672 4881 4882 +f 2672 4526 4525 +f 2672 4527 4526 +f 2672 4882 2734 +f 2672 2734 4527 +f 2673 2735 5895 +f 2673 5901 2735 +f 2674 5897 2736 +f 2674 5896 5897 +f 2674 5901 5896 +f 2674 2736 5901 +f 2675 2676 2738 +f 2675 2737 5897 +f 2675 2738 2737 +f 2676 2677 2738 +f 2677 2678 2740 +f 2677 2740 2739 +f 2677 2739 2738 +f 2678 2679 2740 +f 2679 2741 2740 +f 2679 2742 2741 +f 2679 2680 2742 +f 2680 2681 5810 +f 2680 5810 2742 +f 2681 2743 5810 +f 2681 2744 2743 +f 2681 2682 2744 +f 2682 2745 2744 +f 2682 2683 2745 +f 2683 2684 2745 +f 2684 5885 2685 +f 2684 2747 2746 +f 2684 2685 2747 +f 2684 2746 2745 +f 2685 2686 2747 +f 2685 5885 2686 +f 2686 2749 2748 +f 2686 2687 2749 +f 2686 2748 2747 +f 2687 2750 2749 +f 2687 2688 2750 +f 2688 2689 2751 +f 2688 2751 2750 +f 2688 5676 2689 +f 2689 6033 2751 +f 2689 2690 6033 +f 2689 5029 2690 +f 2689 5676 5029 +f 2690 5999 6033 +f 2690 2691 5999 +f 2690 5029 2691 +f 2691 2692 5999 +f 2692 2693 5893 +f 2692 4977 2693 +f 2692 5893 2754 +f 2692 2754 5999 +f 2693 4977 5660 +f 2693 2694 5608 +f 2693 5660 2694 +f 2693 5608 5893 +f 2694 5857 2695 +f 2694 5660 5857 +f 2694 5246 5608 +f 2694 2695 5246 +f 2695 2696 5784 +f 2695 5784 5246 +f 2695 5857 2696 +f 2696 2697 5429 +f 2696 5429 5784 +f 2697 2698 5429 +f 2698 2699 5302 +f 2698 2760 5429 +f 2698 5302 2760 +f 2699 4967 5302 +f 2699 2700 4967 +f 2700 5308 2701 +f 2700 2701 4967 +f 2701 5308 2702 +f 2701 2702 5225 +f 2701 2763 4967 +f 2701 5225 2763 +f 2702 2703 5225 +f 2703 4972 2704 +f 2703 2765 5225 +f 2703 2704 2765 +f 2704 4972 2705 +f 2704 2767 2766 +f 2704 2766 2765 +f 2704 2705 2767 +f 2705 4972 5747 +f 2705 5747 2706 +f 2705 2706 5555 +f 2705 5555 2767 +f 2706 5747 5517 +f 2706 5826 5555 +f 2706 5517 5826 +f 2707 5903 5826 +f 2707 5826 5640 +f 2707 2708 5903 +f 2707 5640 2708 +f 2708 2770 5903 +f 2708 2709 2770 +f 2709 2771 2770 +f 2709 5480 2771 +f 2709 2710 5480 +f 2710 2711 5480 +f 2711 2712 2774 +f 2711 2774 2773 +f 2711 2773 5480 +f 2711 5158 2712 +f 2712 2713 2774 +f 2712 5158 2713 +f 2713 2775 2774 +f 2713 2776 2775 +f 2713 2714 2776 +f 2714 2715 2776 +f 2715 2777 2776 +f 2715 2716 2778 +f 2715 2778 2777 +f 2716 2717 2778 +f 2717 2718 2779 +f 2717 2779 2778 +f 2718 2780 2779 +f 2718 2781 2780 +f 2718 2719 2781 +f 2719 2720 2781 +f 2720 2782 2781 +f 2720 2783 2782 +f 2720 2721 2783 +f 2721 2722 2783 +f 2722 2723 2785 +f 2722 2785 2784 +f 2722 2784 2783 +f 2723 5267 2785 +f 2724 2786 5267 +f 2724 2787 2786 +f 2724 2725 2787 +f 2725 2788 2787 +f 2725 2726 2788 +f 2725 5960 2726 +f 2726 4659 2788 +f 2727 4659 4660 +f 2727 2789 4658 +f 2727 4658 4659 +f 2727 4590 4591 +f 2727 4591 2789 +f 2727 4589 4590 +f 2728 4430 2790 +f 2728 2790 4590 +f 2729 4297 2791 +f 2729 2791 4791 +f 2729 4791 4792 +f 2729 4294 4297 +f 2729 4792 4793 +f 2730 2792 4888 +f 2730 4888 4889 +f 2731 5348 2793 +f 2732 4114 4108 +f 2732 2794 4114 +f 2732 4108 5377 +f 2732 4739 2794 +f 2733 2795 6037 +f 2733 6037 6035 +f 2733 4528 2795 +f 2733 4527 4528 +f 2733 6035 5716 +f 2733 4526 4527 +f 2734 4882 2796 +f 2734 2796 4527 +f 2735 4882 5895 +f 2735 5901 2736 +f 2735 4819 4882 +f 2735 2797 4819 +f 2735 2736 5804 +f 2735 5804 2797 +f 2736 2798 5804 +f 2736 2799 2798 +f 2736 2737 2799 +f 2736 5897 2737 +f 2737 2738 2800 +f 2737 2800 2799 +f 2738 2739 2800 +f 2739 2740 2801 +f 2739 2801 2800 +f 2740 2741 2802 +f 2740 2802 2801 +f 2741 2742 5854 +f 2741 5854 2803 +f 2741 2803 2802 +f 2742 5810 5854 +f 2743 2744 5799 +f 2743 5799 5810 +f 2744 2745 5933 +f 2744 5933 2806 +f 2744 2806 5799 +f 2745 2746 5933 +f 2746 2809 2808 +f 2746 2747 2809 +f 2746 2808 5933 +f 2747 5327 2809 +f 2747 2748 5327 +f 2748 2749 5327 +f 2749 2750 2811 +f 2749 2811 5327 +f 2750 2812 2811 +f 2750 2751 2813 +f 2750 2813 2812 +f 2751 6033 2752 +f 2751 2752 2813 +f 2752 6033 5999 +f 2752 5999 2753 +f 2752 2815 2814 +f 2752 2753 2815 +f 2752 2814 2813 +f 2753 2754 2815 +f 2753 5999 2754 +f 2754 5893 2755 +f 2754 2816 2815 +f 2754 2755 2817 +f 2754 2817 2816 +f 2755 5608 2756 +f 2755 2756 2817 +f 2755 5893 5608 +f 2756 2818 2817 +f 2756 5246 2757 +f 2756 5608 5246 +f 2756 2819 2818 +f 2756 2757 2819 +f 2757 5784 2758 +f 2757 5868 2819 +f 2757 2758 5868 +f 2757 5246 5784 +f 2758 5784 5429 +f 2758 5429 2759 +f 2758 5856 5868 +f 2758 2759 5856 +f 2759 5429 2760 +f 2759 2760 5856 +f 2760 2761 6014 +f 2760 6014 2822 +f 2760 5302 2761 +f 2760 2822 5856 +f 2761 2762 6014 +f 2761 5302 4967 +f 2761 4967 2762 +f 2762 2824 6014 +f 2762 4967 2763 +f 2762 2825 2824 +f 2762 2763 2825 +f 2763 5225 2764 +f 2763 2764 2825 +f 2764 2826 2825 +f 2764 5225 2765 +f 2764 2765 2827 +f 2764 2827 2826 +f 2765 5283 2827 +f 2765 2766 5283 +f 2766 2767 5283 +f 2767 5555 2768 +f 2767 2768 5081 +f 2767 2829 5283 +f 2767 5081 2829 +f 2768 2769 5081 +f 2768 5555 2769 +f 2769 5555 5903 +f 2769 2831 5081 +f 2769 2770 2832 +f 2769 2832 2831 +f 2769 5903 2770 +f 2770 5364 2832 +f 2770 2771 5364 +f 2771 5480 2772 +f 2771 2772 5364 +f 2772 2834 5364 +f 2772 2773 5005 +f 2772 5480 2773 +f 2772 5005 2834 +f 2773 2774 5005 +f 2774 2775 2836 +f 2774 2836 5005 +f 2775 2776 2838 +f 2775 2838 2837 +f 2775 2837 2836 +f 2776 2777 2838 +f 2777 2778 2839 +f 2777 2839 2838 +f 2778 2840 2839 +f 2778 2779 2841 +f 2778 2841 2840 +f 2779 2780 2841 +f 2780 2842 2841 +f 2780 2843 2842 +f 2780 2781 2843 +f 2781 2844 2843 +f 2781 2782 2844 +f 2782 2845 2844 +f 2782 2783 2845 +f 2783 2784 2846 +f 2783 2846 2845 +f 2784 2785 2846 +f 2785 5267 2786 +f 2785 2847 2846 +f 2785 2786 2847 +f 2786 2787 2848 +f 2786 2848 2847 +f 2787 2849 2848 +f 2787 2850 2849 +f 2787 2788 2850 +f 2788 4659 4658 +f 2788 4657 2850 +f 2788 4658 4657 +f 2789 2851 4658 +f 2789 4591 2851 +f 2790 4592 4591 +f 2790 2852 4592 +f 2790 4430 4472 +f 2790 4429 2852 +f 2790 4472 4429 +f 2790 4591 4590 +f 2791 2853 4791 +f 2791 4297 4298 +f 2791 4298 5962 +f 2791 5962 2853 +f 2792 2854 5844 +f 2792 5844 4888 +f 2793 4113 4902 +f 2793 5348 4113 +f 2793 4902 2855 +f 2794 2856 6061 +f 2794 4740 4741 +f 2794 4741 2856 +f 2794 4739 4740 +f 2794 6061 4114 +f 2795 2857 6037 +f 2795 4528 4529 +f 2795 4529 2857 +f 2796 4882 4819 +f 2796 4819 4820 +f 2796 2858 4528 +f 2796 4820 2858 +f 2796 4528 4527 +f 2797 5804 2859 +f 2797 2859 4819 +f 2798 2860 5804 +f 2798 2861 2860 +f 2798 2799 2861 +f 2799 5166 2861 +f 2799 2800 5166 +f 2800 5584 2862 +f 2800 2862 5166 +f 2800 2801 5584 +f 2801 2802 2863 +f 2801 2863 5584 +f 2802 2865 2864 +f 2802 2864 2863 +f 2802 2803 2865 +f 2803 2804 5965 +f 2803 5965 2865 +f 2803 5854 2804 +f 2804 5854 5947 +f 2804 5947 5965 +f 2805 5799 2806 +f 2805 2806 2868 +f 2805 2868 2867 +f 2805 2867 5947 +f 2805 5947 5799 +f 2806 5933 2807 +f 2806 2807 2869 +f 2806 2869 2868 +f 2807 2808 2869 +f 2807 5933 2808 +f 2808 2870 2869 +f 2808 2809 2870 +f 2809 5327 2810 +f 2809 2871 2870 +f 2809 2810 2871 +f 2810 5327 2811 +f 2810 2811 2873 +f 2810 2872 2871 +f 2810 2873 2872 +f 2811 2812 2873 +f 2812 2813 6002 +f 2812 6002 2874 +f 2812 2874 2873 +f 2813 5793 6002 +f 2813 2814 5793 +f 2814 2815 5074 +f 2814 5074 5793 +f 2815 2816 5074 +f 2816 2817 2879 +f 2816 2879 2878 +f 2816 2878 5074 +f 2817 2818 2880 +f 2817 2880 2879 +f 2818 2881 2880 +f 2818 2819 2881 +f 2819 5815 2881 +f 2819 2820 5815 +f 2819 5868 2820 +f 2820 5560 5815 +f 2820 2821 5560 +f 2820 5856 2821 +f 2820 5868 5856 +f 2821 5856 2822 +f 2821 2822 5560 +f 2822 6014 2823 +f 2822 2884 5560 +f 2822 2823 2884 +f 2823 2885 2884 +f 2823 2824 2886 +f 2823 6014 2824 +f 2823 2886 2885 +f 2824 2825 2887 +f 2824 2887 2886 +f 2825 2826 2887 +f 2826 2827 2889 +f 2826 2889 2888 +f 2826 2888 2887 +f 2827 5283 2828 +f 2827 2828 2889 +f 2828 5373 2890 +f 2828 2829 5373 +f 2828 5283 2829 +f 2828 2890 2889 +f 2829 5081 2830 +f 2829 2830 5373 +f 2830 2892 5373 +f 2830 5081 2831 +f 2830 2831 5273 +f 2830 5273 2892 +f 2831 2832 5273 +f 2832 5364 2833 +f 2832 2894 5273 +f 2832 2895 2894 +f 2832 2833 2895 +f 2833 5132 2895 +f 2833 2834 5132 +f 2833 5364 2834 +f 2834 2835 5132 +f 2834 5005 2835 +f 2835 2836 2898 +f 2835 2898 2897 +f 2835 2897 5132 +f 2835 5005 2836 +f 2836 5785 2898 +f 2836 2837 5785 +f 2837 2838 5785 +f 2838 5623 5785 +f 2838 2839 5623 +f 2839 2901 5623 +f 2839 2840 2902 +f 2839 2902 2901 +f 2840 2841 2902 +f 2841 2903 2902 +f 2841 2842 2903 +f 2842 2904 2903 +f 2842 2905 2904 +f 2842 2843 2905 +f 2843 2844 2905 +f 2844 2845 2906 +f 2844 2906 2905 +f 2845 2846 2908 +f 2845 2908 2907 +f 2845 2907 2906 +f 2846 2909 2908 +f 2846 2847 2909 +f 2847 2848 2910 +f 2847 2910 2909 +f 2848 2849 2910 +f 2849 2850 2911 +f 2849 2911 2910 +f 2850 4657 4656 +f 2850 4656 2912 +f 2850 2912 2911 +f 2851 4592 2913 +f 2851 2913 4657 +f 2851 4591 4592 +f 2851 4657 4658 +f 2852 2914 4593 +f 2852 4593 4592 +f 2852 4429 2914 +f 2853 2915 4790 +f 2853 4790 4791 +f 2853 5962 2915 +f 2854 2916 5844 +f 2855 4902 2917 +f 2856 2918 6061 +f 2856 4741 4742 +f 2856 4742 2918 +f 2857 5002 6037 +f 2857 2919 5046 +f 2857 5046 5002 +f 2857 4529 4530 +f 2857 4530 2919 +f 2858 4529 4528 +f 2858 2920 4529 +f 2858 4820 4821 +f 2858 4821 2920 +f 2859 2921 4820 +f 2859 4820 4819 +f 2859 5904 2921 +f 2859 2860 5904 +f 2859 5804 2860 +f 2860 2861 2922 +f 2860 2922 5904 +f 2861 5166 5842 +f 2861 2923 2922 +f 2861 5842 2923 +f 2862 2924 5166 +f 2862 5584 2924 +f 2863 2864 2926 +f 2863 2925 5584 +f 2863 2926 2925 +f 2864 2865 2926 +f 2865 5965 2866 +f 2865 2866 2928 +f 2865 2928 2927 +f 2865 2927 2926 +f 2866 5965 2867 +f 2866 2867 2928 +f 2867 5965 5947 +f 2867 2868 5680 +f 2867 2929 2928 +f 2867 5680 2929 +f 2868 2869 5680 +f 2869 2931 5680 +f 2869 5100 2931 +f 2869 2870 5100 +f 2870 2871 5100 +f 2871 2933 5100 +f 2871 2872 2933 +f 2872 2935 2934 +f 2872 2934 2933 +f 2872 2873 2935 +f 2873 2874 5434 +f 2873 5434 2935 +f 2874 6002 2875 +f 2874 2875 5434 +f 2875 5793 2876 +f 2875 2876 2937 +f 2875 2937 5434 +f 2875 6002 5793 +f 2876 2938 2937 +f 2876 5074 2877 +f 2876 5793 5074 +f 2876 2877 2938 +f 2877 2878 5940 +f 2877 5940 2939 +f 2877 2939 2938 +f 2877 5074 2878 +f 2878 2879 5928 +f 2878 5928 5940 +f 2879 2880 5928 +f 2880 2942 5928 +f 2880 2881 2942 +f 2881 5815 2882 +f 2881 5551 2943 +f 2881 2943 2942 +f 2881 2882 5551 +f 2882 5815 5560 +f 2882 2883 5115 +f 2882 5560 2883 +f 2882 5115 5551 +f 2883 5560 2884 +f 2883 2884 5115 +f 2884 2885 2946 +f 2884 2946 5115 +f 2885 2886 5222 +f 2885 5222 2947 +f 2885 2947 2946 +f 2886 2887 5222 +f 2887 5186 5222 +f 2887 2888 5186 +f 2888 2951 2950 +f 2888 2889 2951 +f 2888 2950 5186 +f 2889 2952 2951 +f 2889 2890 2952 +f 2890 5373 2891 +f 2890 2891 2953 +f 2890 2953 2952 +f 2891 5373 2892 +f 2891 2954 2953 +f 2891 2892 2954 +f 2892 2893 2954 +f 2892 5273 2893 +f 2893 5273 2894 +f 2893 2955 2954 +f 2893 2894 2956 +f 2893 2956 2955 +f 2894 2895 2956 +f 2895 5132 2896 +f 2895 2957 2956 +f 2895 2896 2958 +f 2895 2958 2957 +f 2896 2897 2959 +f 2896 2959 2958 +f 2896 5132 2897 +f 2897 2898 5809 +f 2897 5809 2959 +f 2898 5785 2899 +f 2898 2899 5809 +f 2899 5785 2900 +f 2899 2900 5914 +f 2899 5914 5809 +f 2900 5785 5623 +f 2900 2962 5914 +f 2900 5623 2901 +f 2900 2901 2962 +f 2901 2964 2963 +f 2901 2902 2964 +f 2901 2963 2962 +f 2902 2965 2964 +f 2902 2903 2965 +f 2903 2904 2966 +f 2903 2966 2965 +f 2904 2967 2966 +f 2904 2905 2967 +f 2905 2906 2967 +f 2906 2907 2968 +f 2906 2968 2967 +f 2907 2908 2969 +f 2907 2969 2968 +f 2908 2970 2969 +f 2908 2909 2970 +f 2909 2971 2970 +f 2909 2910 2971 +f 2910 2973 2972 +f 2910 2911 2973 +f 2910 2972 2971 +f 2911 2912 6022 +f 2911 6022 2973 +f 2912 2974 6022 +f 2912 4655 2974 +f 2912 4656 4655 +f 2913 4593 2975 +f 2913 2975 4656 +f 2913 4592 4593 +f 2913 4656 4657 +f 2914 4429 4473 +f 2914 4428 2976 +f 2914 4473 4428 +f 2914 4594 4593 +f 2914 2976 4594 +f 2915 4789 4790 +f 2915 4306 5620 +f 2915 5962 4306 +f 2915 5620 2977 +f 2915 2977 4789 +f 2916 4899 5844 +f 2916 4898 4899 +f 2916 2978 4898 +f 2917 4903 2979 +f 2917 4901 4903 +f 2917 4902 4901 +f 2918 4743 2980 +f 2918 4742 4743 +f 2918 4116 6061 +f 2918 2980 4116 +f 2919 4530 4531 +f 2919 4531 2981 +f 2919 2981 5046 +f 2920 5970 4530 +f 2920 2982 5970 +f 2920 4530 4529 +f 2920 4821 4822 +f 2920 4822 2982 +f 2921 4821 4820 +f 2921 5904 2983 +f 2921 2983 4821 +f 2922 2923 2985 +f 2922 2985 2984 +f 2922 2984 5904 +f 2923 5842 2985 +f 2924 5284 5842 +f 2924 2986 5284 +f 2924 2987 2986 +f 2924 2925 2987 +f 2924 5584 2925 +f 2924 5842 5166 +f 2925 2926 2987 +f 2926 2988 2987 +f 2926 2927 2988 +f 2927 2989 2988 +f 2927 2928 2990 +f 2927 2990 2989 +f 2928 5546 2990 +f 2928 2929 5546 +f 2929 2930 5546 +f 2929 5680 2930 +f 2930 2992 5546 +f 2930 5680 2931 +f 2930 2931 2992 +f 2931 5939 2993 +f 2931 2932 5939 +f 2931 5100 2932 +f 2931 2993 2992 +f 2932 5004 5939 +f 2932 2933 5004 +f 2932 5100 2933 +f 2933 2934 5004 +f 2934 5096 2996 +f 2934 2935 5096 +f 2934 2996 5004 +f 2935 5434 2936 +f 2935 2936 5096 +f 2936 5434 2937 +f 2936 2998 5096 +f 2936 2937 2999 +f 2936 2999 2998 +f 2937 2938 2999 +f 2938 2939 3000 +f 2938 3000 2999 +f 2939 5940 2940 +f 2939 3001 3000 +f 2939 2940 3001 +f 2940 2941 5316 +f 2940 5940 5928 +f 2940 5928 2941 +f 2940 3002 3001 +f 2940 5316 3002 +f 2941 5928 2942 +f 2941 5028 5316 +f 2941 2942 5028 +f 2942 2943 5028 +f 2943 3005 5028 +f 2943 2944 5094 +f 2943 5094 3005 +f 2943 5551 2944 +f 2944 2945 4893 +f 2944 4893 5094 +f 2944 5551 5115 +f 2944 5115 2945 +f 2945 2946 4893 +f 2945 5115 2946 +f 2946 2947 5238 +f 2946 5238 3008 +f 2946 3008 4893 +f 2947 2948 5238 +f 2947 5222 2948 +f 2948 5222 2949 +f 2948 2949 3010 +f 2948 3010 5238 +f 2949 5186 2950 +f 2949 5222 5186 +f 2949 2950 3012 +f 2949 3012 3011 +f 2949 3011 3010 +f 2950 2951 3012 +f 2951 3013 3012 +f 2951 2952 3013 +f 2952 2953 3014 +f 2952 3014 3013 +f 2953 3015 3014 +f 2953 2954 3015 +f 2954 2955 3016 +f 2954 3016 3015 +f 2955 2956 3017 +f 2955 3017 3016 +f 2956 3019 3018 +f 2956 2957 3019 +f 2956 3018 3017 +f 2957 5926 3019 +f 2957 2958 5926 +f 2958 2959 5926 +f 2959 5113 5926 +f 2959 5809 2960 +f 2959 2960 5113 +f 2960 2961 6124 +f 2960 5809 2961 +f 2960 6124 5113 +f 2961 5809 5914 +f 2961 5914 2962 +f 2961 2962 3024 +f 2961 3024 3023 +f 2961 3023 6124 +f 2962 2963 3024 +f 2963 3026 3025 +f 2963 2964 3026 +f 2963 3025 3024 +f 2964 2965 3027 +f 2964 3027 3026 +f 2965 2966 3027 +f 2966 3028 3027 +f 2966 2967 3028 +f 2967 2968 3030 +f 2967 3030 3029 +f 2967 3029 3028 +f 2968 2969 3030 +f 2969 3031 3030 +f 2969 3032 3031 +f 2969 2970 3032 +f 2970 2971 3032 +f 2971 5978 3033 +f 2971 2972 5978 +f 2971 3033 3032 +f 2972 3034 5978 +f 2972 2973 3034 +f 2973 6022 3035 +f 2973 3035 3034 +f 2974 5957 6022 +f 2974 3036 5957 +f 2974 4655 3036 +f 2975 4593 4594 +f 2975 4655 4656 +f 2975 4594 3037 +f 2975 4654 4655 +f 2975 3037 4654 +f 2976 4428 3038 +f 2976 4595 4594 +f 2976 3038 4595 +f 2977 5620 3039 +f 2977 4788 4789 +f 2977 3039 4787 +f 2977 4787 4788 +f 2978 3040 4898 +f 2979 4904 3041 +f 2979 4903 4904 +f 2980 4119 4116 +f 2980 3042 4119 +f 2980 4743 3042 +f 2981 4531 4532 +f 2981 4532 3043 +f 2981 3043 4379 +f 2981 4379 4395 +f 2981 4395 5046 +f 2982 4822 3044 +f 2982 3044 5970 +f 2983 5904 2984 +f 2983 4823 4822 +f 2983 4822 4821 +f 2983 3045 4823 +f 2983 2984 3045 +f 2984 2985 5112 +f 2984 5112 3046 +f 2984 3046 3045 +f 2985 5284 3047 +f 2985 5842 5284 +f 2985 3047 5112 +f 2986 3048 5284 +f 2986 2987 3048 +f 2987 2988 3050 +f 2987 3050 3049 +f 2987 3049 3048 +f 2988 3051 3050 +f 2988 2989 3051 +f 2989 2990 3052 +f 2989 3052 3051 +f 2990 5546 2991 +f 2990 2991 3052 +f 2991 2992 5748 +f 2991 5546 2992 +f 2991 3053 3052 +f 2991 5748 3053 +f 2992 2993 5748 +f 2993 2994 5760 +f 2993 5939 2994 +f 2993 3055 5748 +f 2993 5760 3055 +f 2994 5939 5004 +f 2994 5004 2995 +f 2994 2995 5760 +f 2995 3057 5760 +f 2995 2996 3057 +f 2995 5004 2996 +f 2996 2997 3059 +f 2996 3059 3058 +f 2996 5096 2997 +f 2996 3058 3057 +f 2997 2998 5171 +f 2997 5096 2998 +f 2997 5171 3059 +f 2998 2999 5171 +f 2999 3000 3061 +f 2999 3061 5171 +f 3000 3063 3062 +f 3000 3001 3063 +f 3000 3062 3061 +f 3001 5314 3063 +f 3001 3002 5314 +f 3002 5316 3003 +f 3002 3003 5314 +f 3003 5486 3065 +f 3003 5028 3004 +f 3003 5316 5028 +f 3003 3004 5486 +f 3003 3065 5314 +f 3004 3005 5486 +f 3004 5028 3005 +f 3005 5266 3067 +f 3005 5094 3006 +f 3005 3006 5266 +f 3005 3067 5486 +f 3006 3007 6017 +f 3006 4893 3007 +f 3006 5094 4893 +f 3006 6017 5266 +f 3007 3008 6017 +f 3007 4893 3008 +f 3008 3070 6017 +f 3008 5238 3009 +f 3008 4883 3070 +f 3008 3009 4883 +f 3009 3010 4883 +f 3009 5238 3010 +f 3010 3072 4883 +f 3010 3073 3072 +f 3010 3011 3073 +f 3011 3012 3073 +f 3012 3075 3074 +f 3012 3013 3075 +f 3012 3074 3073 +f 3013 5143 3075 +f 3013 3014 5143 +f 3014 3015 5143 +f 3015 5177 5143 +f 3015 3016 5177 +f 3016 3078 5177 +f 3016 3017 3079 +f 3016 3079 3078 +f 3017 3018 5253 +f 3017 5253 3079 +f 3018 3019 5253 +f 3019 6128 3081 +f 3019 3020 6128 +f 3019 3081 5253 +f 3019 5926 3020 +f 3020 3021 6128 +f 3020 5926 3021 +f 3021 6122 6123 +f 3021 6123 6128 +f 3021 5926 5113 +f 3021 5113 6122 +f 3022 6122 6124 +f 3022 5543 6122 +f 3022 3023 5543 +f 3022 6124 3023 +f 3023 4960 5543 +f 3023 3024 4960 +f 3024 3025 5908 +f 3024 5908 4960 +f 3025 3026 5955 +f 3025 5955 5908 +f 3026 3088 5955 +f 3026 3027 3088 +f 3027 3089 3088 +f 3027 3028 3089 +f 3028 3091 3090 +f 3028 3029 3091 +f 3028 3090 3089 +f 3029 3030 3091 +f 3030 3092 3091 +f 3030 3031 3092 +f 3031 3093 3092 +f 3031 3032 3093 +f 3032 3033 3095 +f 3032 3095 3094 +f 3032 3094 3093 +f 3033 5978 3095 +f 3034 3096 5978 +f 3034 3097 3096 +f 3034 3035 3097 +f 3035 5937 3097 +f 3035 6022 5957 +f 3035 5957 5937 +f 3036 3098 5957 +f 3036 4655 4654 +f 3036 4654 4653 +f 3036 4653 3098 +f 3037 3099 4654 +f 3037 4594 4595 +f 3037 4595 3099 +f 3038 3100 5778 +f 3038 5778 4595 +f 3038 5252 3100 +f 3038 4474 5252 +f 3038 4428 4474 +f 3039 5620 5846 +f 3039 3101 4787 +f 3039 5846 3101 +f 3040 4896 4897 +f 3040 4897 4898 +f 3040 3102 4896 +f 3041 4904 3103 +f 3042 3104 4123 +f 3042 4681 3104 +f 3042 4743 4680 +f 3042 4680 4681 +f 3042 4123 4119 +f 3043 4533 3105 +f 3043 4532 4533 +f 3043 5938 4379 +f 3043 3105 5938 +f 3044 3106 4532 +f 3044 4822 4823 +f 3044 4823 4824 +f 3044 4532 5970 +f 3044 4824 3106 +f 3045 3046 3108 +f 3045 3108 3107 +f 3045 3107 4823 +f 3046 5112 3108 +f 3047 5284 3048 +f 3047 3109 5112 +f 3047 5153 3109 +f 3047 3048 5153 +f 3048 3049 3110 +f 3048 3110 5153 +f 3049 3050 3111 +f 3049 3111 3110 +f 3050 3112 3111 +f 3050 3051 3112 +f 3051 3052 3113 +f 3051 3113 3112 +f 3052 3053 3115 +f 3052 3114 3113 +f 3052 3115 3114 +f 3053 5756 3115 +f 3053 5677 5756 +f 3053 3054 5677 +f 3053 5748 3054 +f 3054 3055 5677 +f 3054 5748 3055 +f 3055 5760 3056 +f 3055 5607 3117 +f 3055 3056 5607 +f 3055 3117 5677 +f 3056 3057 5607 +f 3056 5760 3057 +f 3057 3119 5607 +f 3057 3120 3119 +f 3057 3058 3120 +f 3058 3059 3120 +f 3059 5171 3060 +f 3059 3121 3120 +f 3059 3060 3121 +f 3060 5171 3061 +f 3060 5098 3122 +f 3060 3061 5098 +f 3060 3122 3121 +f 3061 3062 5098 +f 3062 3063 5494 +f 3062 5494 3124 +f 3062 3124 5098 +f 3063 3064 5494 +f 3063 5314 3064 +f 3064 3065 5900 +f 3064 3126 5494 +f 3064 5900 3126 +f 3064 5314 3065 +f 3065 5486 3066 +f 3065 3066 5900 +f 3066 5486 3067 +f 3066 3128 5900 +f 3066 3067 3128 +f 3067 5266 3068 +f 3067 3068 3130 +f 3067 3129 3128 +f 3067 3130 3129 +f 3068 5266 6017 +f 3068 6017 3069 +f 3068 3069 3131 +f 3068 3131 3130 +f 3069 3070 5984 +f 3069 6017 3070 +f 3069 5984 3131 +f 3070 3071 4963 +f 3070 4963 5984 +f 3070 4883 3071 +f 3071 5427 4963 +f 3071 3072 5427 +f 3071 4883 3072 +f 3072 3073 5427 +f 3073 3135 5427 +f 3073 5086 3135 +f 3073 3074 5086 +f 3074 3075 5086 +f 3075 3076 5064 +f 3075 5064 5086 +f 3075 5143 3076 +f 3076 5287 3138 +f 3076 5143 3077 +f 3076 3077 5287 +f 3076 3138 5064 +f 3077 3078 5287 +f 3077 5143 5177 +f 3077 5177 3078 +f 3078 3140 5287 +f 3078 5027 3140 +f 3078 3079 5027 +f 3079 3080 5027 +f 3079 5253 3080 +f 3080 5253 3081 +f 3080 3081 5622 +f 3080 5622 5027 +f 3081 6128 3082 +f 3081 3082 5514 +f 3081 5514 3143 +f 3081 3143 5622 +f 3082 3083 5514 +f 3082 6128 3083 +f 3083 6123 3084 +f 3083 6128 6123 +f 3083 5424 3145 +f 3083 3145 5514 +f 3083 3084 5424 +f 3084 6123 5543 +f 3084 3085 5424 +f 3084 5543 3085 +f 3085 4960 3086 +f 3085 3147 5424 +f 3085 3086 5281 +f 3085 5281 3147 +f 3085 5543 4960 +f 3086 5908 5907 +f 3086 5907 5281 +f 3086 4960 5908 +f 3087 5767 5907 +f 3087 5907 5955 +f 3087 3088 5767 +f 3087 5955 3088 +f 3088 3089 3150 +f 3088 3150 5767 +f 3089 3090 3151 +f 3089 3151 3150 +f 3090 3091 3152 +f 3090 3152 3151 +f 3091 3154 3153 +f 3091 3092 3154 +f 3091 3153 3152 +f 3092 3093 3154 +f 3093 3155 3154 +f 3093 3094 3156 +f 3093 3156 3155 +f 3094 3157 3156 +f 3094 3095 3157 +f 3095 5978 3096 +f 3095 5408 3157 +f 3095 3096 5408 +f 3096 3158 5408 +f 3096 3097 3158 +f 3097 5937 3159 +f 3097 3159 3158 +f 3098 5937 5957 +f 3098 3160 5937 +f 3098 4653 5931 +f 3098 5931 3160 +f 3099 4653 4654 +f 3099 4595 4596 +f 3099 4596 3161 +f 3099 3161 4653 +f 3100 5252 3162 +f 3100 3162 5778 +f 3101 4786 4787 +f 3101 3163 4786 +f 3101 4313 3163 +f 3101 5846 4313 +f 3102 3164 4900 +f 3102 4900 4896 +f 3103 4905 5450 +f 3103 5450 3165 +f 3103 4904 4905 +f 3104 4124 4123 +f 3104 4130 4124 +f 3104 3166 4130 +f 3104 4682 3166 +f 3104 4681 4682 +f 3105 4533 4534 +f 3105 4534 3167 +f 3105 5934 5938 +f 3105 3167 5934 +f 3106 4533 4532 +f 3106 3168 4533 +f 3106 4825 3168 +f 3106 4824 4825 +f 3107 3169 4824 +f 3107 4824 4823 +f 3107 3108 3169 +f 3108 5112 3109 +f 3108 3109 3170 +f 3108 3170 3169 +f 3109 5451 3171 +f 3109 5153 5451 +f 3109 3171 3170 +f 3110 3172 5153 +f 3110 3111 3173 +f 3110 3173 3172 +f 3111 3112 3174 +f 3111 3174 3173 +f 3112 3113 3174 +f 3113 3175 3174 +f 3113 3114 3175 +f 3114 3115 5805 +f 3114 3176 3175 +f 3114 5805 3176 +f 3115 5756 5805 +f 3116 3117 5773 +f 3116 5677 3117 +f 3116 5773 5756 +f 3116 5756 5677 +f 3117 5607 3118 +f 3117 3179 5773 +f 3117 3118 5807 +f 3117 5807 3179 +f 3118 3119 5807 +f 3118 5607 3119 +f 3119 3181 5807 +f 3119 5750 3181 +f 3119 3120 5750 +f 3120 3121 5750 +f 3121 3183 5750 +f 3121 3122 5508 +f 3121 5508 3183 +f 3122 3123 5508 +f 3122 5098 3123 +f 3123 3124 3186 +f 3123 3186 3185 +f 3123 5098 3124 +f 3123 3185 5508 +f 3124 5494 3125 +f 3124 3125 3187 +f 3124 3187 3186 +f 3125 5494 3126 +f 3125 3188 3187 +f 3125 3126 3188 +f 3126 5900 3127 +f 3126 3127 5333 +f 3126 5333 3188 +f 3127 5833 5333 +f 3127 3128 5833 +f 3127 5900 3128 +f 3128 3129 5033 +f 3128 5033 5833 +f 3129 3130 5033 +f 3130 5051 3192 +f 3130 3192 5033 +f 3130 3131 5051 +f 3131 5950 5051 +f 3131 5984 3132 +f 3131 3132 5950 +f 3132 5984 4963 +f 3132 3133 5091 +f 3132 5091 5950 +f 3132 4963 3133 +f 3133 4921 5091 +f 3133 4963 5427 +f 3133 3134 4921 +f 3133 5427 3134 +f 3134 5427 3135 +f 3134 3135 4921 +f 3135 3136 5092 +f 3135 5086 3136 +f 3135 5092 4921 +f 3136 5559 3198 +f 3136 3198 5092 +f 3136 3137 5559 +f 3136 5086 3137 +f 3137 5064 3138 +f 3137 5086 5064 +f 3137 3138 5559 +f 3138 5287 3139 +f 3138 5155 3200 +f 3138 3139 5155 +f 3138 3200 5559 +f 3139 3140 5155 +f 3139 5287 3140 +f 3140 5027 3141 +f 3140 5418 3202 +f 3140 3202 5155 +f 3140 3141 5418 +f 3141 3142 5418 +f 3141 5027 3142 +f 3142 3143 5534 +f 3142 5622 3143 +f 3142 5534 3204 +f 3142 5027 5622 +f 3142 3204 5418 +f 3143 5514 3144 +f 3143 3144 5534 +f 3144 5658 3206 +f 3144 5514 3145 +f 3144 3206 5534 +f 3144 3145 5658 +f 3145 3146 5658 +f 3145 5424 3146 +f 3146 3147 3209 +f 3146 3209 3208 +f 3146 3208 5658 +f 3146 5424 3147 +f 3147 5281 3148 +f 3147 3148 3209 +f 3148 5281 3149 +f 3148 3210 3209 +f 3148 3149 3210 +f 3149 5281 5767 +f 3149 5767 3150 +f 3149 3150 3211 +f 3149 3211 3210 +f 3150 3213 3212 +f 3150 3151 3213 +f 3150 3212 3211 +f 3151 3152 3214 +f 3151 3214 3213 +f 3152 3153 3215 +f 3152 3215 3214 +f 3153 3154 3216 +f 3153 3216 3215 +f 3154 3155 3217 +f 3154 3217 3216 +f 3155 3156 3217 +f 3156 3157 3218 +f 3156 3218 3217 +f 3157 3219 3218 +f 3157 5408 3219 +f 3158 3220 5408 +f 3158 3159 3221 +f 3158 3221 3220 +f 3159 3160 5003 +f 3159 5937 3160 +f 3159 5003 3221 +f 3160 5931 3222 +f 3160 3222 5003 +f 3161 4597 3223 +f 3161 4596 4597 +f 3161 4652 4653 +f 3161 3223 4652 +f 3162 4597 5778 +f 3162 5252 4955 +f 3162 4955 3224 +f 3162 3224 4598 +f 3162 4598 4597 +f 3163 3225 5709 +f 3163 5709 4785 +f 3163 4785 4786 +f 3163 4313 4314 +f 3163 4321 3225 +f 3163 4314 4321 +f 3164 3226 4900 +f 3165 5562 3227 +f 3165 5450 5562 +f 3166 4682 4683 +f 3166 3228 4130 +f 3166 4683 3228 +f 3167 3229 5934 +f 3167 4535 3229 +f 3167 4534 4535 +f 3168 4534 4533 +f 3168 4825 4826 +f 3168 3230 4534 +f 3168 4826 3230 +f 3169 3170 3232 +f 3169 3232 3231 +f 3169 3231 4825 +f 3169 4825 4824 +f 3170 3171 3232 +f 3171 5451 3233 +f 3171 3233 3232 +f 3172 3234 5451 +f 3172 5451 5153 +f 3172 3173 3234 +f 3173 3235 3234 +f 3173 3236 3235 +f 3173 3174 3236 +f 3174 3237 3236 +f 3174 3175 3237 +f 3175 3176 3238 +f 3175 3238 3237 +f 3176 5805 3177 +f 3176 3177 3239 +f 3176 3239 3238 +f 3177 5805 5773 +f 3177 3178 3239 +f 3177 5773 3178 +f 3178 3240 3239 +f 3178 5773 3179 +f 3178 3179 3241 +f 3178 3241 3240 +f 3179 5807 3180 +f 3179 3242 3241 +f 3179 3180 3242 +f 3180 5807 3181 +f 3180 3181 3242 +f 3181 3244 3243 +f 3181 3243 3242 +f 3181 5750 3182 +f 3181 3182 3244 +f 3182 5750 3183 +f 3182 3183 3244 +f 3183 3245 3244 +f 3183 5508 3184 +f 3183 3184 3245 +f 3184 5084 3246 +f 3184 3246 3245 +f 3184 5508 3185 +f 3184 3185 5084 +f 3185 3186 5084 +f 3186 3248 5084 +f 3186 3187 3248 +f 3187 3249 3248 +f 3187 3188 3250 +f 3187 3250 3249 +f 3188 5333 3189 +f 3188 3189 3250 +f 3189 5833 3190 +f 3189 5333 5833 +f 3189 5053 3251 +f 3189 3190 5053 +f 3189 3251 3250 +f 3190 3191 5915 +f 3190 5915 5053 +f 3190 5833 5033 +f 3190 5033 3191 +f 3191 5033 3192 +f 3191 3192 5915 +f 3192 3254 5915 +f 3192 5051 3193 +f 3192 3193 5803 +f 3192 5803 3254 +f 3193 3194 5803 +f 3193 5051 5950 +f 3193 5950 3194 +f 3194 5797 3256 +f 3194 3256 5803 +f 3194 5950 5091 +f 3194 3195 5797 +f 3194 5091 3195 +f 3195 4921 3196 +f 3195 3196 5797 +f 3195 5091 4921 +f 3196 3258 5797 +f 3196 3197 5020 +f 3196 4921 3197 +f 3196 5020 3258 +f 3197 3198 5020 +f 3197 5092 3198 +f 3197 4921 5092 +f 3198 3260 5020 +f 3198 3199 3260 +f 3198 5559 3199 +f 3199 3261 3260 +f 3199 3200 5462 +f 3199 5462 3261 +f 3199 5559 3200 +f 3200 3201 5462 +f 3200 5155 3201 +f 3201 3202 3263 +f 3201 3263 5462 +f 3201 5155 3202 +f 3202 5418 3203 +f 3202 5698 3264 +f 3202 3203 5698 +f 3202 3264 3263 +f 3203 3204 5698 +f 3203 5418 3204 +f 3204 3267 3266 +f 3204 5534 3205 +f 3204 3205 3267 +f 3204 3266 5698 +f 3205 3206 3267 +f 3205 5534 3206 +f 3206 5684 3268 +f 3206 3207 5684 +f 3206 3268 3267 +f 3206 5658 3207 +f 3207 3208 5684 +f 3207 5658 3208 +f 3208 3209 5458 +f 3208 5458 5684 +f 3209 3210 3271 +f 3209 3271 5458 +f 3210 3272 3271 +f 3210 3273 3272 +f 3210 3211 3273 +f 3211 3212 3273 +f 3212 3213 3274 +f 3212 3274 3273 +f 3213 3276 3275 +f 3213 3275 3274 +f 3213 3214 3276 +f 3214 3215 3276 +f 3215 3277 3276 +f 3215 3216 3277 +f 3216 3278 3277 +f 3216 3217 3278 +f 3217 3218 3279 +f 3217 3279 3278 +f 3218 3280 3279 +f 3218 3219 3280 +f 3219 3281 3280 +f 3219 3220 3282 +f 3219 3282 3281 +f 3219 5408 3220 +f 3220 3283 3282 +f 3220 3221 3283 +f 3221 5003 3283 +f 3222 5931 4651 +f 3222 4651 3284 +f 3222 5310 5003 +f 3222 3284 5310 +f 3223 4651 4652 +f 3223 4598 3285 +f 3223 4597 4598 +f 3223 3285 4650 +f 3223 4650 4651 +f 3224 3286 4599 +f 3224 4599 4598 +f 3224 4426 3286 +f 3224 4955 4426 +f 3225 3287 5709 +f 3225 4321 3287 +f 3226 3288 5116 +f 3226 5116 4324 +f 3226 4324 4900 +f 3227 5562 3289 +f 3228 4683 3290 +f 3228 3290 5547 +f 3228 4131 4130 +f 3228 5547 4131 +f 3229 5831 5934 +f 3229 4535 4536 +f 3229 3291 5813 +f 3229 4536 3291 +f 3229 5813 5831 +f 3230 4826 3292 +f 3230 3292 4535 +f 3230 4535 4534 +f 3231 4827 4826 +f 3231 3293 4827 +f 3231 4826 4825 +f 3231 3232 3294 +f 3231 3294 3293 +f 3232 3233 3295 +f 3232 3295 3294 +f 3233 3234 3295 +f 3233 5451 3234 +f 3234 3296 3295 +f 3234 3297 3296 +f 3234 3235 3297 +f 3235 3236 3297 +f 3236 3298 3297 +f 3236 3237 3299 +f 3236 3299 3298 +f 3237 3238 3299 +f 3238 3239 3301 +f 3238 3301 3300 +f 3238 3300 3299 +f 3239 3240 3301 +f 3240 3302 3301 +f 3240 3241 3302 +f 3241 3242 5235 +f 3241 5235 3303 +f 3241 3303 3302 +f 3242 3243 5235 +f 3243 3305 5235 +f 3243 3306 3305 +f 3243 3244 3306 +f 3244 3245 3306 +f 3245 3307 3306 +f 3245 3246 3307 +f 3246 5084 3247 +f 3246 3247 3308 +f 3246 3308 3307 +f 3247 5084 3248 +f 3247 3310 3309 +f 3247 3309 3308 +f 3247 3248 3310 +f 3248 3249 3310 +f 3249 3311 3310 +f 3249 3250 3311 +f 3250 3312 3311 +f 3250 3251 3312 +f 3251 5053 3252 +f 3251 3313 3312 +f 3251 3252 3314 +f 3251 3314 3313 +f 3252 5053 5915 +f 3252 5915 3253 +f 3252 6015 3314 +f 3252 3253 6015 +f 3253 5915 3254 +f 3253 3254 6015 +f 3254 3255 5739 +f 3254 5739 3316 +f 3254 3316 6015 +f 3254 5803 3255 +f 3255 3256 5865 +f 3255 5803 3256 +f 3255 5865 5739 +f 3256 3257 5865 +f 3256 5797 3257 +f 3257 3319 5865 +f 3257 5797 3258 +f 3257 3258 3320 +f 3257 3320 3319 +f 3258 3259 5039 +f 3258 5039 3320 +f 3258 5020 3259 +f 3259 5020 3260 +f 3259 3260 5039 +f 3260 5282 5039 +f 3260 3261 5282 +f 3261 3262 5595 +f 3261 5462 3262 +f 3261 5595 5282 +f 3262 3263 5667 +f 3262 5667 5595 +f 3262 5462 3263 +f 3263 3325 5667 +f 3263 3264 3325 +f 3264 5698 3265 +f 3264 3265 3326 +f 3264 3326 3325 +f 3265 5770 3327 +f 3265 5698 3266 +f 3265 3327 3326 +f 3265 3266 5770 +f 3266 3267 5770 +f 3267 3268 5706 +f 3267 5706 5770 +f 3268 5684 3269 +f 3268 3269 5533 +f 3268 5533 3330 +f 3268 3330 5706 +f 3269 5684 3270 +f 3269 3270 5533 +f 3270 5684 5458 +f 3270 5761 5533 +f 3270 3271 5761 +f 3270 5458 3271 +f 3271 3272 3333 +f 3271 3333 5761 +f 3272 3334 3333 +f 3272 3273 5390 +f 3272 5390 3334 +f 3273 3274 5390 +f 3274 3336 5390 +f 3274 3275 3337 +f 3274 3337 3336 +f 3275 3338 3337 +f 3275 3276 3338 +f 3276 3277 3338 +f 3277 3278 3340 +f 3277 3340 3339 +f 3277 3339 3338 +f 3278 3279 3340 +f 3279 3341 3340 +f 3279 3280 3342 +f 3279 3342 3341 +f 3280 3281 3342 +f 3281 3343 3342 +f 3281 5030 3343 +f 3281 3282 5030 +f 3282 3344 5030 +f 3282 3283 3344 +f 3283 5310 3345 +f 3283 5003 5310 +f 3283 3345 3344 +f 3284 4650 4649 +f 3284 4649 3346 +f 3284 4651 4650 +f 3284 3346 5459 +f 3284 5459 5310 +f 3285 4598 4599 +f 3285 3347 4650 +f 3285 4599 3347 +f 3286 4600 4599 +f 3286 3348 4600 +f 3286 4426 4476 +f 3286 4425 3348 +f 3286 4476 4425 +f 3287 3349 4783 +f 3287 4322 3349 +f 3287 4321 4322 +f 3287 4783 5709 +f 3288 3350 5117 +f 3288 5117 5116 +f 3289 5562 4950 +f 3289 4950 3351 +f 3290 4683 4684 +f 3290 4684 4685 +f 3290 4685 3352 +f 3290 3352 5547 +f 3291 3353 5813 +f 3291 4537 3353 +f 3291 4536 4537 +f 3292 4828 3354 +f 3292 4827 4828 +f 3292 4826 4827 +f 3292 3354 4536 +f 3292 4536 4535 +f 3293 3355 4827 +f 3293 3294 3355 +f 3294 3356 3355 +f 3294 3295 3357 +f 3294 3357 3356 +f 3295 3358 3357 +f 3295 3296 3358 +f 3296 3297 3359 +f 3296 3359 3358 +f 3297 3298 3359 +f 3298 3360 3359 +f 3298 3299 3360 +f 3299 3300 3362 +f 3299 3362 3361 +f 3299 3361 3360 +f 3300 3301 3363 +f 3300 3363 3362 +f 3301 3302 3363 +f 3302 3364 3363 +f 3302 3303 3365 +f 3302 3365 3364 +f 3303 3304 3365 +f 3303 5235 3304 +f 3304 3366 3365 +f 3304 5235 3305 +f 3304 3305 3366 +f 3305 3367 3366 +f 3305 3368 3367 +f 3305 3306 3368 +f 3306 3307 3369 +f 3306 3369 3368 +f 3307 3370 3369 +f 3307 3308 3370 +f 3308 5090 3370 +f 3308 3309 5090 +f 3309 3310 5090 +f 3310 3372 5090 +f 3310 3311 3372 +f 3311 3373 3372 +f 3311 3374 3373 +f 3311 3312 3374 +f 3312 3313 3374 +f 3313 3314 3376 +f 3313 3376 3375 +f 3313 3375 3374 +f 3314 6015 3315 +f 3314 3315 3376 +f 3315 6015 3316 +f 3315 3377 3376 +f 3315 5018 3377 +f 3315 3316 5018 +f 3316 5739 3317 +f 3316 3317 5306 +f 3316 5306 5018 +f 3317 3318 5306 +f 3317 5865 3318 +f 3317 5739 5865 +f 3318 5865 3319 +f 3318 3380 5306 +f 3318 3319 3381 +f 3318 3381 3380 +f 3319 3320 3382 +f 3319 3382 3381 +f 3320 3321 3382 +f 3320 5039 3321 +f 3321 5039 3322 +f 3321 3322 5206 +f 3321 3383 3382 +f 3321 5206 3383 +f 3322 3323 5206 +f 3322 5282 3323 +f 3322 5039 5282 +f 3323 5282 5595 +f 3323 5595 3324 +f 3323 3324 3386 +f 3323 3386 3385 +f 3323 3385 5206 +f 3324 5792 3386 +f 3324 5667 3325 +f 3324 3325 5792 +f 3324 5595 5667 +f 3325 3326 5792 +f 3326 3388 5792 +f 3326 3327 3389 +f 3326 3389 3388 +f 3327 5770 3328 +f 3327 3328 3389 +f 3328 5770 3329 +f 3328 3329 6127 +f 3328 6127 3390 +f 3328 3390 3389 +f 3329 5770 5706 +f 3329 3330 6127 +f 3329 5706 3330 +f 3330 3392 6127 +f 3330 5533 3331 +f 3330 3331 3392 +f 3331 5501 3393 +f 3331 3332 5501 +f 3331 5533 3332 +f 3331 3393 3392 +f 3332 5761 3333 +f 3332 3333 5501 +f 3332 5533 5761 +f 3333 3334 5611 +f 3333 3395 5501 +f 3333 5611 3395 +f 3334 3335 5611 +f 3334 5390 3335 +f 3335 3398 3397 +f 3335 3336 3398 +f 3335 5390 3336 +f 3335 3397 5611 +f 3336 3337 3398 +f 3337 3399 3398 +f 3337 3400 3399 +f 3337 3338 3400 +f 3338 3339 3400 +f 3339 3340 3401 +f 3339 3401 3400 +f 3340 3403 3402 +f 3340 3341 3403 +f 3340 3402 3401 +f 3341 3342 3404 +f 3341 3404 3403 +f 3342 3343 3404 +f 3343 5703 3405 +f 3343 3405 3404 +f 3343 5030 5703 +f 3344 5703 5030 +f 3344 3345 5941 +f 3344 5941 3406 +f 3344 3406 5703 +f 3345 5310 5459 +f 3345 5654 3407 +f 3345 3407 5941 +f 3345 5459 5654 +f 3346 3408 5459 +f 3346 4649 5465 +f 3346 5465 3408 +f 3347 4600 5609 +f 3347 4649 4650 +f 3347 4599 4600 +f 3347 3409 4649 +f 3347 5609 3409 +f 3348 5769 3410 +f 3348 3410 4601 +f 3348 4425 5769 +f 3348 4601 4600 +f 3349 5210 3411 +f 3349 4323 5210 +f 3349 4782 4783 +f 3349 3411 4782 +f 3349 4322 4323 +f 3350 3412 5118 +f 3350 5118 5117 +f 3351 4949 4947 +f 3351 4950 4949 +f 3351 4947 3413 +f 3352 4685 4686 +f 3352 3414 4135 +f 3352 4686 3414 +f 3352 4135 5547 +f 3353 3415 5648 +f 3353 5648 5264 +f 3353 5264 5813 +f 3353 4537 3415 +f 3354 3416 4538 +f 3354 4538 4537 +f 3354 4537 4536 +f 3354 4828 4829 +f 3354 4829 3416 +f 3355 4828 4827 +f 3355 3356 3417 +f 3355 3417 4828 +f 3356 3357 3419 +f 3356 3418 3417 +f 3356 3419 3418 +f 3357 3420 3419 +f 3357 3358 3420 +f 3358 3359 3420 +f 3359 3421 3420 +f 3359 3360 3421 +f 3360 3422 3421 +f 3360 3361 3423 +f 3360 3423 3422 +f 3361 3424 3423 +f 3361 3362 3424 +f 3362 3363 3425 +f 3362 3425 3424 +f 3363 3364 3425 +f 3364 3365 3426 +f 3364 3426 3425 +f 3365 3427 3426 +f 3365 5471 3427 +f 3365 3366 5471 +f 3366 3367 5681 +f 3366 5681 5471 +f 3367 3368 5681 +f 3368 3369 3430 +f 3368 3430 5681 +f 3369 5618 3431 +f 3369 3370 5618 +f 3369 3431 3430 +f 3370 5090 3371 +f 3370 3371 5618 +f 3371 5090 3372 +f 3371 3433 5618 +f 3371 3372 3433 +f 3372 3434 3433 +f 3372 6010 3434 +f 3372 3373 6010 +f 3373 3374 6010 +f 3374 3375 3436 +f 3374 3436 6010 +f 3375 3376 5880 +f 3375 5880 3437 +f 3375 3437 3436 +f 3376 3377 5880 +f 3377 3439 5880 +f 3377 3378 3439 +f 3377 5018 3378 +f 3378 3441 3440 +f 3378 3379 3441 +f 3378 3440 3439 +f 3378 5018 5306 +f 3378 5306 3379 +f 3379 3380 3441 +f 3379 5306 3380 +f 3380 5500 3442 +f 3380 3381 5500 +f 3380 3442 3441 +f 3381 3382 5504 +f 3381 5504 5500 +f 3382 3383 5504 +f 3383 3445 5504 +f 3383 3446 3445 +f 3383 3384 3446 +f 3383 5206 3384 +f 3384 5470 3446 +f 3384 3385 5470 +f 3384 5206 3385 +f 3385 3386 5470 +f 3386 5792 3387 +f 3386 3387 3449 +f 3386 3448 5470 +f 3386 3449 3448 +f 3387 5792 3388 +f 3387 3388 3449 +f 3388 3450 3449 +f 3388 5022 3450 +f 3388 3389 5022 +f 3389 3390 5022 +f 3390 6127 3391 +f 3390 3453 3452 +f 3390 3391 3453 +f 3390 3452 5022 +f 3391 3454 3453 +f 3391 3392 3454 +f 3391 6127 3392 +f 3392 3393 5701 +f 3392 5701 3454 +f 3393 5501 3394 +f 3393 3394 5701 +f 3394 5501 3395 +f 3394 5566 5701 +f 3394 3395 5566 +f 3395 3396 5565 +f 3395 5565 5566 +f 3395 5611 3396 +f 3396 3397 5835 +f 3396 5835 5565 +f 3396 5611 3397 +f 3397 3398 3459 +f 3397 3459 5835 +f 3398 3399 3461 +f 3398 3461 3460 +f 3398 3460 3459 +f 3399 3400 5798 +f 3399 5798 3461 +f 3400 3401 5798 +f 3401 3402 5179 +f 3401 5179 5798 +f 3402 3403 3464 +f 3402 5679 5179 +f 3402 3464 5679 +f 3403 3465 3464 +f 3403 3466 3465 +f 3403 3404 3466 +f 3404 3405 3467 +f 3404 3467 3466 +f 3405 5703 3467 +f 3406 5941 3468 +f 3406 3468 5703 +f 3407 5391 3469 +f 3407 5654 5391 +f 3407 3469 5941 +f 3408 3470 5654 +f 3408 5465 3470 +f 3408 5654 5459 +f 3409 5609 5507 +f 3409 5507 3471 +f 3409 3471 4648 +f 3409 4648 4649 +f 3410 3472 4601 +f 3410 5769 3472 +f 3411 4781 4782 +f 3411 3473 4781 +f 3411 5210 3473 +f 3412 3474 5118 +f 3413 4947 4948 +f 3413 4948 3475 +f 3414 4139 4135 +f 3414 4687 3476 +f 3414 4686 4687 +f 3414 3476 4139 +f 3415 3477 5648 +f 3415 4539 3477 +f 3415 4537 4538 +f 3415 4538 4539 +f 3416 4830 3478 +f 3416 4829 4830 +f 3416 3478 4538 +f 3417 3418 3480 +f 3417 3480 3479 +f 3417 4829 4828 +f 3417 3479 4829 +f 3418 3481 3480 +f 3418 3419 3481 +f 3419 3420 3481 +f 3420 3421 3483 +f 3420 3483 3482 +f 3420 3482 3481 +f 3421 3484 3483 +f 3421 3422 3484 +f 3422 3423 3484 +f 3423 3424 3485 +f 3423 3485 3484 +f 3424 3486 3485 +f 3424 3487 3486 +f 3424 3425 3487 +f 3425 3488 3487 +f 3425 3426 3488 +f 3426 3427 3488 +f 3427 3490 3489 +f 3427 5471 3428 +f 3427 3489 3488 +f 3427 3428 3490 +f 3428 5471 6011 +f 3428 6011 5573 +f 3428 5573 3490 +f 3429 6011 5681 +f 3429 5573 6011 +f 3429 3430 5573 +f 3429 5681 3430 +f 3430 5249 3492 +f 3430 3492 5573 +f 3430 3431 5249 +f 3431 3432 5249 +f 3431 5618 3432 +f 3432 3494 5249 +f 3432 5154 3494 +f 3432 5618 3433 +f 3432 3433 5154 +f 3433 3434 5154 +f 3434 5173 3496 +f 3434 6010 3435 +f 3434 3496 5154 +f 3434 3435 5173 +f 3435 3436 5954 +f 3435 6010 3436 +f 3435 5954 5173 +f 3436 3437 5621 +f 3436 5621 5954 +f 3437 5880 3438 +f 3437 3438 5496 +f 3437 5496 5621 +f 3438 5880 3439 +f 3438 5025 5496 +f 3438 3439 5025 +f 3439 3440 5025 +f 3440 3503 3502 +f 3440 3441 3503 +f 3440 3502 5025 +f 3441 3442 3503 +f 3442 5500 3443 +f 3442 3443 5599 +f 3442 5599 3504 +f 3442 3504 3503 +f 3443 5712 5599 +f 3443 5500 5504 +f 3443 5504 3444 +f 3443 3444 5712 +f 3444 3445 5712 +f 3444 5504 3445 +f 3445 3446 5606 +f 3445 5606 5712 +f 3446 3508 5606 +f 3446 5470 3447 +f 3446 3447 3508 +f 3447 5470 3448 +f 3447 5715 3509 +f 3447 3448 5715 +f 3447 3509 3508 +f 3448 3449 5715 +f 3449 3511 5715 +f 3449 3450 3511 +f 3450 3512 3511 +f 3450 3451 3513 +f 3450 5022 3451 +f 3450 3513 3512 +f 3451 3452 6050 +f 3451 6050 3513 +f 3451 5022 3452 +f 3452 3453 6050 +f 3453 3454 6078 +f 3453 3515 6050 +f 3453 6078 3515 +f 3454 3455 6078 +f 3454 5701 3455 +f 3455 3517 6078 +f 3455 5701 3456 +f 3455 5332 3517 +f 3455 3456 5332 +f 3456 5566 5564 +f 3456 5701 5566 +f 3456 5564 5332 +f 3457 5565 5822 +f 3457 5438 5564 +f 3457 5564 5565 +f 3457 5822 5438 +f 3458 5822 5835 +f 3458 3459 5572 +f 3458 5835 3459 +f 3458 5572 5822 +f 3459 3460 3521 +f 3459 3521 5572 +f 3460 3522 3521 +f 3460 3461 3523 +f 3460 3523 3522 +f 3461 6008 3523 +f 3461 5798 3462 +f 3461 3462 6008 +f 3462 5814 6008 +f 3462 5798 5814 +f 3463 5679 5859 +f 3463 5179 5679 +f 3463 5814 5179 +f 3463 5859 5814 +f 3464 5762 5679 +f 3464 3465 5762 +f 3465 3527 5762 +f 3465 3466 3527 +f 3466 3528 3527 +f 3466 3467 3528 +f 3467 3529 3528 +f 3467 3530 3529 +f 3467 3468 3530 +f 3467 5703 3468 +f 3468 5941 3469 +f 3468 5531 3530 +f 3468 3469 5531 +f 3469 3531 5531 +f 3469 5391 3531 +f 3470 6038 5391 +f 3470 3532 6038 +f 3470 5391 5654 +f 3470 4647 3532 +f 3470 5465 4647 +f 3471 4647 4648 +f 3471 5507 3533 +f 3471 4646 4647 +f 3471 3533 4646 +f 3472 6034 4602 +f 3472 4602 4601 +f 3472 5769 4424 +f 3472 4424 5078 +f 3472 5078 3534 +f 3472 3534 6034 +f 3473 5811 3535 +f 3473 5210 5811 +f 3473 4779 4780 +f 3473 3535 4779 +f 3473 4780 4781 +f 3474 5120 5119 +f 3474 3536 5120 +f 3474 5119 5118 +f 3475 4948 3537 +f 3476 4140 4139 +f 3476 4687 3538 +f 3476 3538 4146 +f 3476 4146 4140 +f 3477 4391 5648 +f 3477 4540 3539 +f 3477 4383 4391 +f 3477 3539 4383 +f 3477 4539 4540 +f 3478 4830 3540 +f 3478 3540 4539 +f 3478 4539 4538 +f 3479 3480 5139 +f 3479 3541 4831 +f 3479 4831 4830 +f 3479 5139 3541 +f 3479 4830 4829 +f 3480 3481 3542 +f 3480 3542 5139 +f 3481 3543 3542 +f 3481 3482 3544 +f 3481 3544 3543 +f 3482 3483 3544 +f 3483 3484 3545 +f 3483 3545 3544 +f 3484 3546 3545 +f 3484 3485 3547 +f 3484 3547 3546 +f 3485 3548 3547 +f 3485 3486 3548 +f 3486 3487 3548 +f 3487 3549 3548 +f 3487 3550 3549 +f 3487 3488 3550 +f 3488 3551 3550 +f 3488 3489 3551 +f 3489 3490 3551 +f 3490 5573 3491 +f 3490 3553 3552 +f 3490 3491 3553 +f 3490 3552 3551 +f 3491 5600 3553 +f 3491 3492 5600 +f 3491 5573 3492 +f 3492 3493 5600 +f 3492 5249 3493 +f 3493 3555 5600 +f 3493 5249 3494 +f 3493 3494 3555 +f 3494 3556 3555 +f 3494 3495 3556 +f 3494 5154 3495 +f 3495 5154 3496 +f 3495 3557 3556 +f 3495 3496 3557 +f 3496 5173 3497 +f 3496 5461 3558 +f 3496 3558 3557 +f 3496 3497 5461 +f 3497 5954 3498 +f 3497 5318 5461 +f 3497 5173 5954 +f 3497 3498 5318 +f 3498 5621 3499 +f 3498 3499 5318 +f 3498 5954 5621 +f 3499 3500 3561 +f 3499 5496 3500 +f 3499 3561 5318 +f 3499 5621 5496 +f 3500 3501 5411 +f 3500 3562 3561 +f 3500 5496 5025 +f 3500 5025 3501 +f 3500 5411 3562 +f 3501 3502 5411 +f 3501 5025 3502 +f 3502 3503 3565 +f 3502 3564 5411 +f 3502 3565 3564 +f 3503 3504 3566 +f 3503 3566 3565 +f 3504 3505 5989 +f 3504 5989 3566 +f 3504 5599 3505 +f 3505 5712 3506 +f 3505 5599 5712 +f 3505 3506 5989 +f 3506 5712 3507 +f 3506 3507 5031 +f 3506 3568 5989 +f 3506 5031 3568 +f 3507 5712 5606 +f 3507 5606 3508 +f 3507 3508 5031 +f 3508 3509 5101 +f 3508 3570 5031 +f 3508 5101 3570 +f 3509 3510 5101 +f 3509 5715 3510 +f 3510 3572 5101 +f 3510 3511 3572 +f 3510 5715 3511 +f 3511 3573 3572 +f 3511 3512 5540 +f 3511 5540 3573 +f 3512 3513 5540 +f 3513 6050 3514 +f 3513 3575 5540 +f 3513 3576 3575 +f 3513 3514 3576 +f 3514 6050 3515 +f 3514 3515 3576 +f 3515 6078 3516 +f 3515 3577 3576 +f 3515 3578 3577 +f 3515 3516 3578 +f 3516 6081 3578 +f 3516 6078 3517 +f 3516 3517 6081 +f 3517 3518 6081 +f 3517 5332 3518 +f 3518 5332 3519 +f 3518 3580 6081 +f 3518 3519 3580 +f 3519 3582 3581 +f 3519 3520 3582 +f 3519 3581 3580 +f 3519 5332 5438 +f 3519 5438 3520 +f 3520 5572 3521 +f 3520 5438 5572 +f 3520 3521 3582 +f 3521 3522 3584 +f 3521 3584 3583 +f 3521 3583 3582 +f 3522 3523 3584 +f 3523 6008 3524 +f 3523 3585 3584 +f 3523 3524 3585 +f 3524 6008 3525 +f 3524 3587 3586 +f 3524 3525 3587 +f 3524 3586 3585 +f 3525 6008 5859 +f 3525 5859 3526 +f 3525 3526 3587 +f 3526 5859 5762 +f 3526 3527 3588 +f 3526 5762 3527 +f 3526 3588 3587 +f 3527 3589 3588 +f 3527 3590 3589 +f 3527 3528 3590 +f 3528 3529 3591 +f 3528 3591 3590 +f 3529 3530 3591 +f 3530 3592 3591 +f 3530 5226 3592 +f 3530 5531 5226 +f 3531 5391 6038 +f 3531 5580 3593 +f 3531 6038 5580 +f 3531 3593 5531 +f 3532 4647 4646 +f 3532 3594 6045 +f 3532 5800 3594 +f 3532 6045 6038 +f 3532 4646 5800 +f 3533 5507 4603 +f 3533 5382 3595 +f 3533 4603 5382 +f 3533 3595 4646 +f 3534 6057 6034 +f 3534 3596 6057 +f 3534 5378 3596 +f 3534 5078 5378 +f 3535 5811 4969 +f 3535 3597 4779 +f 3535 4969 3597 +f 3536 3598 5120 +f 3537 5259 3599 +f 3537 4948 4145 +f 3537 4145 5259 +f 3538 3600 5718 +f 3538 5718 4146 +f 3538 4687 4688 +f 3538 4688 4689 +f 3538 4689 3600 +f 3539 4541 3601 +f 3539 4540 4541 +f 3539 3601 4964 +f 3539 4964 4383 +f 3540 4831 4832 +f 3540 4832 3602 +f 3540 4830 4831 +f 3540 4540 4539 +f 3540 3602 4540 +f 3541 3603 4831 +f 3541 5038 3603 +f 3541 5139 5038 +f 3542 5527 3604 +f 3542 3604 5139 +f 3542 3543 5527 +f 3543 3605 5527 +f 3543 3544 3605 +f 3544 3606 3605 +f 3544 3607 3606 +f 3544 3545 3607 +f 3545 3608 3607 +f 3545 3546 3608 +f 3546 3547 3608 +f 3547 3609 3608 +f 3547 3548 3609 +f 3548 3549 5591 +f 3548 3610 3609 +f 3548 5591 3610 +f 3549 3550 5591 +f 3550 3551 3613 +f 3550 3613 3612 +f 3550 3612 5591 +f 3551 3552 3613 +f 3552 3614 3613 +f 3552 3553 5389 +f 3552 5389 3614 +f 3553 3554 5389 +f 3553 5600 3554 +f 3554 5600 3555 +f 3554 3616 5389 +f 3554 3617 3616 +f 3554 3555 3617 +f 3555 3556 3617 +f 3556 3557 3618 +f 3556 3618 3617 +f 3557 3619 3618 +f 3557 3620 3619 +f 3557 3558 3620 +f 3558 3621 3620 +f 3558 5461 3559 +f 3558 3559 3621 +f 3559 5461 5318 +f 3559 3560 3621 +f 3559 5318 3560 +f 3560 5318 3561 +f 3560 3622 3621 +f 3560 3623 3622 +f 3560 3561 3623 +f 3561 3562 3623 +f 3562 5411 3563 +f 3562 5482 3624 +f 3562 3624 3623 +f 3562 3563 5482 +f 3563 3564 5482 +f 3563 5411 3564 +f 3564 3626 5482 +f 3564 5300 3626 +f 3564 3565 5300 +f 3565 5961 5300 +f 3565 3566 5961 +f 3566 5989 3567 +f 3566 4979 5961 +f 3566 3567 4979 +f 3567 3568 4979 +f 3567 5989 3568 +f 3568 5031 3569 +f 3568 3630 4979 +f 3568 3569 3630 +f 3569 5031 3570 +f 3569 3631 3630 +f 3569 3570 3631 +f 3570 5101 3571 +f 3570 3632 3631 +f 3570 3571 5552 +f 3570 5552 3632 +f 3571 3572 5552 +f 3571 5101 3572 +f 3572 3634 5552 +f 3572 3573 3634 +f 3573 5819 3635 +f 3573 3574 5819 +f 3573 3635 3634 +f 3573 5540 3574 +f 3574 3575 5819 +f 3574 5540 3575 +f 3575 6108 3637 +f 3575 3576 6108 +f 3575 3637 5819 +f 3576 3577 6108 +f 3577 3639 6108 +f 3577 3578 6116 +f 3577 6116 3639 +f 3578 6081 3579 +f 3578 3579 6116 +f 3579 6081 3580 +f 3579 3641 6116 +f 3579 3580 3641 +f 3580 5286 3642 +f 3580 3642 3641 +f 3580 3581 5286 +f 3581 3582 5286 +f 3582 3583 5713 +f 3582 5713 5286 +f 3583 3645 5713 +f 3583 3646 3645 +f 3583 3584 3646 +f 3584 3585 3646 +f 3585 3586 3647 +f 3585 3647 3646 +f 3586 3648 3647 +f 3586 3649 3648 +f 3586 3587 3649 +f 3587 3650 3649 +f 3587 3588 3650 +f 3588 3651 3650 +f 3588 3589 3651 +f 3589 3590 3651 +f 3590 3591 3652 +f 3590 3652 3651 +f 3591 3653 3652 +f 3591 3654 3653 +f 3591 3592 3654 +f 3592 5226 3654 +f 3593 3655 5226 +f 3593 5580 3655 +f 3593 5226 5531 +f 3594 5580 6045 +f 3594 3656 5802 +f 3594 5802 5580 +f 3594 5800 3656 +f 3595 5382 3657 +f 3595 3657 4645 +f 3595 4645 4646 +f 3596 3658 6063 +f 3596 6063 6057 +f 3596 5378 3658 +f 3597 4778 4779 +f 3597 4969 4337 +f 3597 3659 4778 +f 3597 4337 3659 +f 3598 5321 4340 +f 3598 4340 5120 +f 3598 3660 5321 +f 3599 5259 5260 +f 3599 5260 3661 +f 3600 3662 5718 +f 3600 4690 3662 +f 3600 4689 4690 +f 3601 5631 3663 +f 3601 3663 4964 +f 3601 4541 5631 +f 3602 4832 4833 +f 3602 4833 3664 +f 3602 4541 4540 +f 3602 3664 4541 +f 3603 3665 4832 +f 3603 4832 4831 +f 3603 5038 3665 +f 3604 3666 5038 +f 3604 5038 5139 +f 3604 5527 3666 +f 3605 3606 3667 +f 3605 3667 5527 +f 3606 3668 3667 +f 3606 3607 3668 +f 3607 3670 3669 +f 3607 3608 3670 +f 3607 3669 3668 +f 3608 3609 3670 +f 3609 3671 3670 +f 3609 3610 3671 +f 3610 5591 3611 +f 3610 3611 3672 +f 3610 3672 3671 +f 3611 3612 3674 +f 3611 3674 3673 +f 3611 3673 3672 +f 3611 5591 3612 +f 3612 5515 3674 +f 3612 3613 5515 +f 3613 3614 5515 +f 3614 5389 3615 +f 3614 3676 5515 +f 3614 3615 5157 +f 3614 5157 3676 +f 3615 3616 5157 +f 3615 5389 3616 +f 3616 3617 5255 +f 3616 3678 5157 +f 3616 5255 3678 +f 3617 3618 5255 +f 3618 3680 5255 +f 3618 3619 3680 +f 3619 3620 3681 +f 3619 3681 3680 +f 3620 3621 3683 +f 3620 3683 3682 +f 3620 3682 3681 +f 3621 3622 4998 +f 3621 4998 3683 +f 3622 3623 4998 +f 3623 5603 3685 +f 3623 3624 5603 +f 3623 3685 4998 +f 3624 5482 3625 +f 3624 3625 5603 +f 3625 3687 5603 +f 3625 3626 3687 +f 3625 5482 3626 +f 3626 3627 3689 +f 3626 3689 3688 +f 3626 3688 3687 +f 3626 5300 3627 +f 3627 5961 3628 +f 3627 5300 5961 +f 3627 3690 3689 +f 3627 3628 3690 +f 3628 3629 3690 +f 3628 4979 3629 +f 3628 5961 4979 +f 3629 3691 3690 +f 3629 4979 3630 +f 3629 3630 3692 +f 3629 3692 3691 +f 3630 3631 5006 +f 3630 5006 3692 +f 3631 3632 5006 +f 3632 5876 5006 +f 3632 5552 3633 +f 3632 3633 5876 +f 3633 3634 4975 +f 3633 4975 5876 +f 3633 5552 3634 +f 3634 3635 5396 +f 3634 5396 4975 +f 3635 5819 3636 +f 3635 3636 5646 +f 3635 5646 5396 +f 3636 5912 3698 +f 3636 3637 5912 +f 3636 3698 5646 +f 3636 5819 3637 +f 3637 3638 5912 +f 3637 6108 3638 +f 3638 3639 3700 +f 3638 6108 3639 +f 3638 3700 5912 +f 3639 6116 3640 +f 3639 3640 3701 +f 3639 3701 3700 +f 3640 6116 3641 +f 3640 3702 3701 +f 3640 3703 3702 +f 3640 3641 3703 +f 3641 5370 3703 +f 3641 3642 5370 +f 3642 5286 3643 +f 3642 3643 5370 +f 3643 5286 3644 +f 3643 3706 3705 +f 3643 3644 3706 +f 3643 3705 5370 +f 3644 3645 3706 +f 3644 5713 3645 +f 3644 5286 5713 +f 3645 3646 3707 +f 3645 3707 3706 +f 3646 3708 3707 +f 3646 3709 3708 +f 3646 3647 3709 +f 3647 3648 3709 +f 3648 3710 3709 +f 3648 3649 3711 +f 3648 3711 3710 +f 3649 3650 3712 +f 3649 3712 3711 +f 3650 3651 3712 +f 3651 3713 3712 +f 3651 3652 3713 +f 3652 3715 3714 +f 3652 3653 3715 +f 3652 3714 3713 +f 3653 3654 3716 +f 3653 3716 3715 +f 3654 5402 3716 +f 3654 3655 5402 +f 3654 5226 3655 +f 3655 5580 5802 +f 3655 5802 3717 +f 3655 3717 5402 +f 3656 5800 4644 +f 3656 3718 5802 +f 3656 4643 3718 +f 3656 4644 4643 +f 3657 4644 4645 +f 3657 4605 5521 +f 3657 5521 3719 +f 3657 5382 4605 +f 3657 3719 4644 +f 3658 5754 6063 +f 3658 3720 5754 +f 3658 5481 3720 +f 3658 5378 5481 +f 3659 3721 4777 +f 3659 4338 3721 +f 3659 4777 4778 +f 3659 4337 4338 +f 3660 3722 5321 +f 3661 5260 5261 +f 3661 5261 3723 +f 3662 3724 4148 +f 3662 4148 5718 +f 3662 4690 4691 +f 3662 4691 3724 +f 3663 3725 5657 +f 3663 5657 4384 +f 3663 5631 3725 +f 3663 4384 4964 +f 3664 3726 4542 +f 3664 4834 3726 +f 3664 4833 4834 +f 3664 4542 4541 +f 3665 4833 4832 +f 3665 3666 3727 +f 3665 5038 3666 +f 3665 3727 4833 +f 3666 5662 3728 +f 3666 5527 3667 +f 3666 3667 5662 +f 3666 3728 3727 +f 3667 3668 3730 +f 3667 3730 3729 +f 3667 3729 5662 +f 3668 3669 3730 +f 3669 3670 3732 +f 3669 3732 3731 +f 3669 3731 3730 +f 3670 3671 3732 +f 3671 3734 3733 +f 3671 3672 3734 +f 3671 3733 3732 +f 3672 3673 3734 +f 3673 3735 3734 +f 3673 3736 3735 +f 3673 3674 3736 +f 3674 3737 3736 +f 3674 3675 3737 +f 3674 5515 3675 +f 3675 5515 3676 +f 3675 3676 3737 +f 3676 3738 3737 +f 3676 3739 3738 +f 3676 3677 3739 +f 3676 5157 3677 +f 3677 5157 3678 +f 3677 3678 5397 +f 3677 5397 3739 +f 3678 3679 5397 +f 3678 5255 3679 +f 3679 5255 3680 +f 3679 3680 5909 +f 3679 5909 3741 +f 3679 3741 5397 +f 3680 3681 5909 +f 3681 3743 5909 +f 3681 3682 3743 +f 3682 3683 3745 +f 3682 3744 3743 +f 3682 3745 3744 +f 3683 3684 3746 +f 3683 3746 3745 +f 3683 4998 3684 +f 3684 3685 3746 +f 3684 4998 3685 +f 3685 5603 3686 +f 3685 3686 3747 +f 3685 3747 3746 +f 3686 3748 3747 +f 3686 5603 3687 +f 3686 3687 3748 +f 3687 5694 3749 +f 3687 3749 3748 +f 3687 3688 5694 +f 3688 3689 5694 +f 3689 3690 3751 +f 3689 3751 5694 +f 3690 3752 3751 +f 3690 3691 3752 +f 3691 3754 3753 +f 3691 3692 3754 +f 3691 3753 3752 +f 3692 5006 3693 +f 3692 3755 3754 +f 3692 3693 3755 +f 3693 3694 4973 +f 3693 5006 3694 +f 3693 4973 3755 +f 3694 5876 3695 +f 3694 5006 5876 +f 3694 3695 4973 +f 3695 5339 4973 +f 3695 4975 3696 +f 3695 5876 4975 +f 3695 3696 5339 +f 3696 3697 5236 +f 3696 4975 5396 +f 3696 5396 3697 +f 3696 5236 5339 +f 3697 3698 5967 +f 3697 5967 5236 +f 3697 5646 3698 +f 3697 5396 5646 +f 3698 5683 3760 +f 3698 3699 5683 +f 3698 3760 5967 +f 3698 5912 3699 +f 3699 3700 5683 +f 3699 5912 3700 +f 3700 5855 5683 +f 3700 3701 5855 +f 3701 3763 5855 +f 3701 5178 3763 +f 3701 3702 5178 +f 3702 3703 5178 +f 3703 5370 3704 +f 3703 5182 5178 +f 3703 3704 5182 +f 3704 3705 3766 +f 3704 3766 5182 +f 3704 5370 3705 +f 3705 3706 3768 +f 3705 3767 3766 +f 3705 3768 3767 +f 3706 5292 3768 +f 3706 3707 5292 +f 3707 3708 5292 +f 3708 3770 5292 +f 3708 3709 3771 +f 3708 3771 3770 +f 3709 3710 3771 +f 3710 3711 3773 +f 3710 3773 3772 +f 3710 3772 3771 +f 3711 3712 3773 +f 3712 3774 3773 +f 3712 3775 3774 +f 3712 3713 3775 +f 3713 3714 3775 +f 3714 3715 3777 +f 3714 3777 3776 +f 3714 3776 3775 +f 3715 3716 3777 +f 3716 5402 3778 +f 3716 3778 3777 +f 3717 3780 3779 +f 3717 3718 3780 +f 3717 5802 3718 +f 3717 3779 5402 +f 3718 4643 3780 +f 3719 5521 3781 +f 3719 3781 4642 +f 3719 4642 4643 +f 3719 4643 4644 +f 3720 3782 5754 +f 3720 5481 4422 +f 3720 4422 3782 +f 3721 4339 3783 +f 3721 4338 4339 +f 3721 4776 4777 +f 3721 4775 4776 +f 3721 3783 4775 +f 3722 3784 5322 +f 3722 5322 5320 +f 3722 5320 5321 +f 3723 5261 3785 +f 3724 4151 4148 +f 3724 3786 4151 +f 3724 4691 3786 +f 3725 4544 3787 +f 3725 4543 4544 +f 3725 5631 4543 +f 3725 3787 5657 +f 3726 3788 4543 +f 3726 4834 3788 +f 3726 4543 4542 +f 3727 4835 4834 +f 3727 3789 4835 +f 3727 4834 4833 +f 3727 3728 3789 +f 3728 3790 3789 +f 3728 5662 3790 +f 3729 3791 5662 +f 3729 3792 3791 +f 3729 3730 3792 +f 3730 3731 3792 +f 3731 3732 3793 +f 3731 3793 3792 +f 3732 3733 5653 +f 3732 3794 3793 +f 3732 5653 3794 +f 3733 3734 3795 +f 3733 3795 5653 +f 3734 3796 3795 +f 3734 3735 3796 +f 3735 3797 3796 +f 3735 3736 3798 +f 3735 3798 3797 +f 3736 3737 3798 +f 3737 3799 3798 +f 3737 3738 3799 +f 3738 3801 3800 +f 3738 3739 3801 +f 3738 3800 3799 +f 3739 3740 5399 +f 3739 5397 3740 +f 3739 5399 3801 +f 3740 3741 5399 +f 3740 5397 3741 +f 3741 5909 3742 +f 3741 3803 5399 +f 3741 3742 3803 +f 3742 5909 3743 +f 3742 3743 5968 +f 3742 5968 3804 +f 3742 3804 3803 +f 3743 3744 5968 +f 3744 5511 3806 +f 3744 3745 5511 +f 3744 3806 5968 +f 3745 3746 5511 +f 3746 5951 3808 +f 3746 3808 5511 +f 3746 3747 5951 +f 3747 3748 5151 +f 3747 5151 5951 +f 3748 3749 5151 +f 3749 3811 5151 +f 3749 3750 3812 +f 3749 3812 3811 +f 3749 5694 3750 +f 3750 3751 3813 +f 3750 3813 3812 +f 3750 5694 3751 +f 3751 3752 3814 +f 3751 3814 3813 +f 3752 4927 3814 +f 3752 3753 4927 +f 3753 3754 4927 +f 3754 3816 4927 +f 3754 3817 3816 +f 3754 3755 3817 +f 3755 3756 3817 +f 3755 4973 3756 +f 3756 3757 3819 +f 3756 3819 3818 +f 3756 4973 3757 +f 3756 3818 3817 +f 3757 3758 3819 +f 3757 5339 3758 +f 3757 4973 5339 +f 3758 5772 3820 +f 3758 3820 3819 +f 3758 5236 3759 +f 3758 5339 5236 +f 3758 3759 5772 +f 3759 3760 5772 +f 3759 5967 3760 +f 3759 5236 5967 +f 3760 5683 3761 +f 3760 5506 5772 +f 3760 3761 5506 +f 3761 5683 3762 +f 3761 3762 5549 +f 3761 5549 5506 +f 3762 5855 3763 +f 3762 3824 5549 +f 3762 5683 5855 +f 3762 3763 5274 +f 3762 5274 3824 +f 3763 5178 3764 +f 3763 3764 5274 +f 3764 3765 3826 +f 3764 5178 3765 +f 3764 3826 5274 +f 3765 3766 3828 +f 3765 3828 3827 +f 3765 5182 3766 +f 3765 3827 3826 +f 3765 5178 5182 +f 3766 3767 3828 +f 3767 3829 3828 +f 3767 3768 3830 +f 3767 3830 3829 +f 3768 3769 3830 +f 3768 5292 3769 +f 3769 5292 3770 +f 3769 3832 3831 +f 3769 3831 3830 +f 3769 3770 3832 +f 3770 3771 3833 +f 3770 3833 3832 +f 3771 3772 3833 +f 3772 3835 3834 +f 3772 3834 3833 +f 3772 3773 3835 +f 3773 3836 3835 +f 3773 3774 3836 +f 3774 3837 3836 +f 3774 3775 3837 +f 3775 3776 3838 +f 3775 3838 3837 +f 3776 3839 3838 +f 3776 3777 3839 +f 3777 3778 3839 +f 3778 3840 3839 +f 3778 5402 3779 +f 3778 5072 3840 +f 3778 3779 5072 +f 3779 3841 5072 +f 3779 3780 4954 +f 3779 4954 3841 +f 3780 3842 4954 +f 3780 4642 4641 +f 3780 4641 3842 +f 3780 4643 4642 +f 3781 5521 4607 +f 3781 3843 4642 +f 3781 4607 3843 +f 3782 4607 5754 +f 3782 4608 4607 +f 3782 4422 4480 +f 3782 5047 3844 +f 3782 4480 5047 +f 3782 3844 4608 +f 3783 4342 5899 +f 3783 5899 3845 +f 3783 4339 4342 +f 3783 3845 4775 +f 3784 3846 5323 +f 3784 5323 5322 +f 3785 5262 5263 +f 3785 5263 3847 +f 3785 5261 5262 +f 3786 3848 4155 +f 3786 4155 4151 +f 3786 4692 4693 +f 3786 4691 4692 +f 3786 4693 3848 +f 3787 5576 4385 +f 3787 4544 4545 +f 3787 4545 3849 +f 3787 3849 5576 +f 3787 4385 5657 +f 3788 4835 4836 +f 3788 4836 3850 +f 3788 3850 4544 +f 3788 4834 4835 +f 3788 4544 4543 +f 3789 3851 4835 +f 3789 3852 3851 +f 3789 3790 3852 +f 3790 3791 3853 +f 3790 3853 3852 +f 3790 5662 3791 +f 3791 3792 3853 +f 3792 3854 3853 +f 3792 3793 3854 +f 3793 3794 3855 +f 3793 3855 3854 +f 3794 5653 3856 +f 3794 3856 3855 +f 3795 3796 3858 +f 3795 3858 3857 +f 3795 3857 5653 +f 3796 3797 3858 +f 3797 3798 3860 +f 3797 3860 3859 +f 3797 3859 3858 +f 3798 3799 3861 +f 3798 3861 3860 +f 3799 3800 3861 +f 3800 3862 3861 +f 3800 3801 3862 +f 3801 3802 3863 +f 3801 5399 3802 +f 3801 3863 3862 +f 3802 3865 3864 +f 3802 3864 3863 +f 3802 5399 3803 +f 3802 3803 3865 +f 3803 3804 3865 +f 3804 5968 3805 +f 3804 3866 3865 +f 3804 3867 3866 +f 3804 3805 3867 +f 3805 3806 5699 +f 3805 5699 3867 +f 3805 5968 3806 +f 3806 5511 3807 +f 3806 3807 5413 +f 3806 5413 5699 +f 3807 5511 3808 +f 3807 3808 6018 +f 3807 6018 5413 +f 3808 5291 6018 +f 3808 3809 5291 +f 3808 5951 3809 +f 3809 3810 5291 +f 3809 5151 3810 +f 3809 5951 5151 +f 3810 5151 3811 +f 3810 3811 3873 +f 3810 3873 3872 +f 3810 3872 5291 +f 3811 3812 5103 +f 3811 5103 3873 +f 3812 3813 4915 +f 3812 4915 5103 +f 3813 3814 4915 +f 3814 3876 4915 +f 3814 3815 5329 +f 3814 4927 3815 +f 3814 5329 3876 +f 3815 4895 5329 +f 3815 3816 4895 +f 3815 4927 3816 +f 3816 3817 4895 +f 3817 5239 3879 +f 3817 3879 4895 +f 3817 3818 5239 +f 3818 3819 5239 +f 3819 3881 5239 +f 3819 3820 5258 +f 3819 5258 3881 +f 3820 3821 5258 +f 3820 5772 3821 +f 3821 5772 3822 +f 3821 3822 3883 +f 3821 3883 5258 +f 3822 5772 5506 +f 3822 3884 3883 +f 3822 5506 3823 +f 3822 3823 3884 +f 3823 3824 5230 +f 3823 5506 5549 +f 3823 5549 3824 +f 3823 3885 3884 +f 3823 5230 3885 +f 3824 5274 3825 +f 3824 3825 5230 +f 3825 5867 5230 +f 3825 3826 5867 +f 3825 5274 3826 +f 3826 3827 5637 +f 3826 5637 5867 +f 3827 3828 5627 +f 3827 5627 5637 +f 3828 5619 5627 +f 3828 3829 5619 +f 3829 3891 5619 +f 3829 5233 3891 +f 3829 3830 5233 +f 3830 3831 5233 +f 3831 3832 5160 +f 3831 5160 3893 +f 3831 3893 5233 +f 3832 3833 5160 +f 3833 3895 5160 +f 3833 3834 3895 +f 3834 3896 3895 +f 3834 3897 3896 +f 3834 3835 3897 +f 3835 3836 3897 +f 3836 3837 3898 +f 3836 3898 3897 +f 3837 3838 3900 +f 3837 3899 3898 +f 3837 3900 3899 +f 3838 3839 3901 +f 3838 3901 3900 +f 3839 3840 5873 +f 3839 5873 3901 +f 3840 5072 3902 +f 3840 3902 5873 +f 3841 3903 5072 +f 3841 4954 3903 +f 3842 4641 4640 +f 3842 4640 3904 +f 3842 3904 4954 +f 3843 3905 4641 +f 3843 4641 4642 +f 3843 4607 4608 +f 3843 4608 3905 +f 3844 3906 4609 +f 3844 5047 3906 +f 3844 4609 4608 +f 3845 5899 3907 +f 3845 3907 4774 +f 3845 4774 4775 +f 3846 3908 5323 +f 3847 5263 3909 +f 3848 3910 4156 +f 3848 4693 4694 +f 3848 4694 3910 +f 3848 4156 4155 +f 3849 3911 5576 +f 3849 4546 3911 +f 3849 4545 4546 +f 3850 3912 4545 +f 3850 4545 4544 +f 3850 4837 3912 +f 3850 4836 4837 +f 3851 3913 4836 +f 3851 4836 4835 +f 3851 5863 3913 +f 3851 3852 5863 +f 3852 3853 3914 +f 3852 3914 5863 +f 3853 3915 3914 +f 3853 3854 3916 +f 3853 3916 3915 +f 3854 3855 3917 +f 3854 3917 3916 +f 3855 3856 3918 +f 3855 3918 3917 +f 3856 5653 3857 +f 3856 3857 3918 +f 3857 3858 3920 +f 3857 3920 3919 +f 3857 3919 3918 +f 3858 5303 3920 +f 3858 3859 5303 +f 3859 3860 5303 +f 3860 3922 5303 +f 3860 3861 3922 +f 3861 3923 3922 +f 3861 3862 3923 +f 3862 3924 3923 +f 3862 3863 3925 +f 3862 3925 3924 +f 3863 5156 3925 +f 3863 3864 5156 +f 3864 3865 5156 +f 3865 3866 5271 +f 3865 3927 5156 +f 3865 5271 3927 +f 3866 3867 5271 +f 3867 3868 5539 +f 3867 5699 3868 +f 3867 3929 5271 +f 3867 5539 3929 +f 3868 5413 3869 +f 3868 3869 5539 +f 3868 5699 5413 +f 3869 5413 6018 +f 3869 6018 3870 +f 3869 3870 3931 +f 3869 3931 5539 +f 3870 3932 3931 +f 3870 5291 3871 +f 3870 3933 3932 +f 3870 6018 5291 +f 3870 3871 3933 +f 3871 5291 3872 +f 3871 3872 3933 +f 3872 6106 3934 +f 3872 3934 3933 +f 3872 3873 6106 +f 3873 3874 6106 +f 3873 5103 3874 +f 3874 6040 3936 +f 3874 3875 6040 +f 3874 3936 6106 +f 3874 5103 4915 +f 3874 4915 3875 +f 3875 3876 6040 +f 3875 4915 3876 +f 3876 5329 3877 +f 3876 3877 3939 +f 3876 3939 3938 +f 3876 3938 6040 +f 3877 5329 4895 +f 3877 3878 3940 +f 3877 4895 3878 +f 3877 3940 3939 +f 3878 5185 3940 +f 3878 4895 3879 +f 3878 3879 5185 +f 3879 3880 5185 +f 3879 5239 3880 +f 3880 4925 5185 +f 3880 5239 3881 +f 3880 3881 4925 +f 3881 3943 4925 +f 3881 4612 3943 +f 3881 3882 4612 +f 3881 5258 3882 +f 3882 3883 4612 +f 3882 5258 3883 +f 3883 3884 5353 +f 3883 5353 4612 +f 3884 5837 5353 +f 3884 3885 5837 +f 3885 5230 3886 +f 3885 5853 5837 +f 3885 3886 5853 +f 3886 3887 5512 +f 3886 5230 3887 +f 3886 5512 3948 +f 3886 3948 5853 +f 3887 3888 5512 +f 3887 5867 3888 +f 3887 5230 5867 +f 3888 3889 5604 +f 3888 5867 5637 +f 3888 5604 3950 +f 3888 5637 3889 +f 3888 3950 5512 +f 3889 3890 5604 +f 3889 5637 5627 +f 3889 5627 3890 +f 3890 3891 5568 +f 3890 5568 3952 +f 3890 5619 3891 +f 3890 5627 5619 +f 3890 3952 5604 +f 3891 5233 3892 +f 3891 3892 5568 +f 3892 5233 3893 +f 3892 3893 3954 +f 3892 3954 5568 +f 3893 3956 3955 +f 3893 3894 3956 +f 3893 3955 3954 +f 3893 5160 3894 +f 3894 5160 3895 +f 3894 3957 3956 +f 3894 3895 3957 +f 3895 3958 3957 +f 3895 3896 3958 +f 3896 3959 3958 +f 3896 3897 3959 +f 3897 3898 3959 +f 3898 5495 3960 +f 3898 3899 5495 +f 3898 3960 3959 +f 3899 3961 5495 +f 3899 3900 3961 +f 3900 3901 3962 +f 3900 3962 3961 +f 3901 5873 3963 +f 3901 3963 3962 +f 3902 5072 3903 +f 3902 3965 3964 +f 3902 3903 3965 +f 3902 3964 5873 +f 3903 3966 3965 +f 3903 3904 3966 +f 3903 4954 3904 +f 3904 4639 3966 +f 3904 4640 4639 +f 3905 4609 3967 +f 3905 4640 4641 +f 3905 3967 4640 +f 3905 4608 4609 +f 3906 5047 5664 +f 3906 5664 3968 +f 3906 4610 4609 +f 3906 3968 4610 +f 3907 5899 4346 +f 3907 4346 3969 +f 3907 4773 4774 +f 3907 3969 4773 +f 3908 3970 5324 +f 3908 5324 5323 +f 3909 4161 5194 +f 3909 5263 4161 +f 3910 4158 4162 +f 3910 4162 4156 +f 3910 4694 4695 +f 3911 5906 4386 +f 3911 4386 5576 +f 3912 4546 4545 +f 3912 4837 4838 +f 3912 4483 4546 +f 3913 4837 4836 +f 3920 5303 3921 +f 3921 5303 3922 +f 3925 5156 3926 +f 3926 5156 3927 +f 3927 5271 3928 +f 3928 5271 3929 +f 3929 5539 3930 +f 3930 5539 3931 +f 3931 3932 5082 +f 3932 3933 5082 +f 3934 6106 3935 +f 3935 6106 3936 +f 3935 3936 5455 +f 3936 6040 3937 +f 3936 3937 5349 +f 3936 5349 5455 +f 3937 6040 3938 +f 3937 3938 5349 +f 3938 3939 5218 +f 3939 3940 5586 +f 3939 5586 5218 +f 3940 3941 5586 +f 3940 5185 3941 +f 3941 5838 5586 +f 3941 3942 5838 +f 3941 5185 3942 +f 3942 5782 5838 +f 3942 5185 4925 +f 3942 4925 3943 +f 3942 3943 5782 +f 3943 3944 4886 +f 3943 4886 5782 +f 3943 4612 3944 +f 3944 3945 6043 +f 3944 4612 3945 +f 3945 4612 5353 +f 3945 3946 6043 +f 3945 5353 3946 +f 3946 5837 3947 +f 3946 5353 5837 +f 3947 5853 3948 +f 3947 5837 5853 +f 3948 5512 3949 +f 3949 5512 3950 +f 3950 5604 3951 +f 3951 5604 3952 +f 3952 5568 3953 +f 3952 3953 5227 +f 3953 3954 5227 +f 3953 5568 3954 +f 3954 5993 5227 +f 3954 3955 5993 +f 3955 3956 5099 +f 3956 3957 5099 +f 3963 5873 3964 +f 3964 3965 5278 +f 3967 4609 4610 +f 3967 4610 4547 +f 3967 4638 4639 +f 3967 4639 4640 +f 3968 5664 4420 +f 3968 5447 4610 +f 3969 4346 4353 +f 3969 4353 4351 +f 3969 4771 4772 +f 3969 4772 4773 +f 3970 4908 4354 +f 3970 4354 5324 +f 3971 3999 3995 +f 3971 4158 3999 +f 3971 4157 4158 +f 3971 4005 4012 +f 3971 4012 4157 +f 3971 3995 4005 +f 3972 4160 4154 +f 3972 4153 4152 +f 3972 4154 4153 +f 3972 4156 4162 +f 3972 4162 4160 +f 3972 4152 4156 +f 3973 4149 4143 +f 3973 4141 4148 +f 3973 4148 4151 +f 3973 4142 4141 +f 3973 4151 4149 +f 3973 4143 4142 +f 3974 5582 4136 +f 3974 4144 5582 +f 3974 4140 4144 +f 3974 4136 4140 +f 3975 4135 4133 +f 3975 4132 4135 +f 3975 4133 4978 +f 3975 4978 4125 +f 3975 4125 4132 +f 3976 5468 4120 +f 3976 4124 4128 +f 3976 4123 4124 +f 3976 4120 4123 +f 3976 4122 5468 +f 3976 4128 4122 +f 3977 4109 5871 +f 3977 5871 4116 +f 3977 4117 4952 +f 3977 4952 4109 +f 3977 4116 4119 +f 3977 4119 4117 +f 3978 4112 4106 +f 3978 4105 4104 +f 3978 4106 4105 +f 3978 4108 4112 +f 3978 4104 4107 +f 3978 4107 4108 +f 3979 4095 4093 +f 3979 4100 4103 +f 3979 4103 4101 +f 3979 4093 4099 +f 3979 4099 4100 +f 3979 4101 5869 +f 3979 5869 4095 +f 3980 4096 4090 +f 3980 4092 4096 +f 3980 4090 4088 +f 3980 4088 4091 +f 3980 4091 4092 +f 3981 4087 4085 +f 3981 4085 4079 +f 3981 4079 4077 +f 3981 4084 4087 +f 3981 4083 4084 +f 3981 4077 4083 +f 3982 4080 4074 +f 3982 4076 4080 +f 3982 4072 4075 +f 3982 4075 4076 +f 3982 4074 4072 +f 3983 5542 4061 +f 3983 4069 5542 +f 3983 4071 4069 +f 3983 4068 4071 +f 3983 4067 4068 +f 3983 4061 4067 +f 3984 4064 5795 +f 3984 5795 4056 +f 3984 4060 4064 +f 3984 4056 4060 +f 3985 4051 4052 +f 3985 4045 4051 +f 3985 4055 4053 +f 3985 4053 5088 +f 3985 4052 4055 +f 3985 5088 4045 +f 3986 5692 4040 +f 3986 4040 5509 +f 3986 5509 4044 +f 3986 4048 5692 +f 3986 4044 4048 +f 3987 4036 5836 +f 3987 4037 4031 +f 3987 5083 4029 +f 3987 4031 5083 +f 3987 4029 4036 +f 3987 5836 4037 +f 3988 4026 4025 +f 3988 4024 4028 +f 3988 4028 4032 +f 3988 4025 4024 +f 3988 4032 4026 +f 3989 4013 4020 +f 3989 4015 5749 +f 3989 4021 5341 +f 3989 5749 4013 +f 3989 5341 4015 +f 3989 4023 4021 +f 3989 4020 4023 +f 3990 4004 4016 +f 3990 3993 4956 +f 3990 5745 3993 +f 3990 4956 4004 +f 3990 4016 5745 +f 3991 5986 3994 +f 3991 3994 4007 +f 3991 4007 4006 +f 3991 3996 5986 +f 3991 4006 3996 +f 3992 4956 3993 +f 3992 3993 4009 +f 3992 3994 4002 +f 3992 4009 3994 +f 3992 4002 4956 +f 3993 4010 4009 +f 3993 5745 4010 +f 3994 5986 4001 +f 3994 4001 4002 +f 3994 4009 4008 +f 3994 4008 4007 +f 3995 3998 3997 +f 3995 3999 3998 +f 3995 3996 4005 +f 3995 3997 3996 +f 3996 3997 5986 +f 3996 4006 4005 +f 3997 4000 5986 +f 4000 4001 5986 +f 4002 4003 4956 +f 4003 4004 4956 +f 4004 4018 4016 +f 4005 4006 5192 +f 4005 5192 4012 +f 4006 4007 5191 +f 4006 5191 5192 +f 4007 5190 5191 +f 4007 4008 5190 +f 4008 4009 5189 +f 4008 5189 5190 +f 4009 5188 5189 +f 4009 4010 5188 +f 4010 4011 5187 +f 4010 5187 5188 +f 4010 5745 4011 +f 4011 5307 4017 +f 4011 5745 5307 +f 4012 5193 4159 +f 4012 5192 5193 +f 4012 4159 4157 +f 4013 5749 4016 +f 4013 4019 4020 +f 4013 4018 4019 +f 4013 4016 4018 +f 4014 4017 5307 +f 4014 5307 5749 +f 4014 4015 5010 +f 4014 5749 4015 +f 4014 5010 4017 +f 4015 4022 5009 +f 4015 5341 4022 +f 4015 5009 5010 +f 4016 5749 5307 +f 4016 5307 5745 +f 4021 4027 4024 +f 4021 4023 4027 +f 4021 4024 5341 +f 4022 5011 5009 +f 4022 5341 4025 +f 4022 4025 5011 +f 4023 5183 4027 +f 4024 4027 4028 +f 4024 4025 5341 +f 4025 4026 5012 +f 4025 5012 5011 +f 4026 5013 5012 +f 4026 4032 4033 +f 4026 4033 5013 +f 4027 5183 4028 +f 4028 4034 4032 +f 4029 4035 4036 +f 4029 4032 4034 +f 4029 4034 4035 +f 4029 5083 4032 +f 4030 4031 5245 +f 4030 5245 4033 +f 4030 4033 5083 +f 4030 5083 4031 +f 4031 5243 5244 +f 4031 5244 5245 +f 4031 4038 5243 +f 4031 4037 4038 +f 4032 5083 4033 +f 4036 4039 5836 +f 4036 5433 4039 +f 4037 5836 5509 +f 4037 5509 4040 +f 4037 4040 4038 +f 4038 4041 5243 +f 4038 4040 4041 +f 4039 4043 5836 +f 4039 5433 4043 +f 4040 5692 4041 +f 4041 4042 5242 +f 4041 5242 5243 +f 4041 5692 4042 +f 4042 5692 4049 +f 4042 4049 5241 +f 4042 5241 5242 +f 4043 5509 5836 +f 4043 4044 5509 +f 4044 4050 4048 +f 4045 5453 4048 +f 4045 5088 5453 +f 4045 4048 4050 +f 4045 4050 4051 +f 4046 4049 5453 +f 4046 5130 4049 +f 4046 4047 5130 +f 4046 5088 4047 +f 4046 5453 5088 +f 4047 4054 5129 +f 4047 5129 5130 +f 4047 5088 4054 +f 4048 4049 5692 +f 4048 5453 4049 +f 4053 4054 5088 +f 4053 4056 4054 +f 4053 4055 4059 +f 4053 4059 4056 +f 4054 5128 5129 +f 4054 4056 4057 +f 4054 4057 5128 +f 4056 5795 4057 +f 4056 4059 4060 +f 4057 5127 5128 +f 4057 4058 5127 +f 4057 5795 4058 +f 4058 5795 4065 +f 4058 4065 5126 +f 4058 5126 5127 +f 4059 5457 4060 +f 4060 4066 4064 +f 4060 5457 4066 +f 4061 5542 5736 +f 4061 4066 4067 +f 4061 4064 4066 +f 4061 5736 4064 +f 4062 5736 5542 +f 4062 5542 4063 +f 4062 4063 4817 +f 4062 4065 5736 +f 4062 4817 4065 +f 4063 5542 4070 +f 4063 4070 4815 +f 4063 4816 4817 +f 4063 4815 4816 +f 4064 4065 5795 +f 4064 5736 4065 +f 4069 4072 4070 +f 4069 4070 5542 +f 4069 4075 4072 +f 4069 4071 4075 +f 4070 4073 4815 +f 4070 4072 4073 +f 4072 4074 4073 +f 4073 4074 4814 +f 4073 4814 4815 +f 4074 4080 4081 +f 4074 4081 4818 +f 4074 4818 4814 +f 4076 4082 4080 +f 4077 4078 4080 +f 4077 4079 4078 +f 4077 4082 4083 +f 4077 4080 4082 +f 4078 4081 4080 +f 4078 4079 4932 +f 4078 4932 4081 +f 4079 4931 4932 +f 4079 4085 4086 +f 4079 4086 4931 +f 4084 5216 4087 +f 4085 4087 4091 +f 4085 4091 4088 +f 4085 4088 4086 +f 4086 4088 4089 +f 4086 4089 4930 +f 4086 4930 4931 +f 4087 5216 4091 +f 4088 4090 4089 +f 4089 4090 5983 +f 4089 5983 4928 +f 4089 4928 4930 +f 4090 4929 5983 +f 4090 4096 4097 +f 4090 4097 4929 +f 4092 4098 4096 +f 4093 4095 4094 +f 4093 4096 4098 +f 4093 4098 4099 +f 4093 4094 4096 +f 4094 4097 4096 +f 4094 4095 5663 +f 4094 5663 4097 +f 4095 5869 4102 +f 4095 4102 5345 +f 4095 5345 5663 +f 4097 5558 4929 +f 4097 5663 5558 +f 4101 4104 5869 +f 4101 4103 4107 +f 4101 4107 4104 +f 4102 5869 4105 +f 4102 4105 5346 +f 4102 5346 5345 +f 4103 5377 4107 +f 4104 4105 5869 +f 4105 4106 5347 +f 4105 5347 5346 +f 4106 4113 5348 +f 4106 5348 5347 +f 4106 4112 4113 +f 4107 5377 4108 +f 4108 4114 4112 +f 4109 4110 4112 +f 4109 4952 4110 +f 4109 4112 5871 +f 4110 4902 4113 +f 4110 4111 4902 +f 4110 4952 4111 +f 4110 4113 4112 +f 4111 4901 4902 +f 4111 4118 4901 +f 4111 4952 5165 +f 4111 5165 4118 +f 4112 4114 5871 +f 4114 4115 5871 +f 4114 6061 4115 +f 4115 6061 4116 +f 4115 4116 5871 +f 4117 4120 5165 +f 4117 5165 4952 +f 4117 4123 4120 +f 4117 4119 4123 +f 4118 5468 4121 +f 4118 5165 5468 +f 4118 4121 4903 +f 4118 4903 4901 +f 4120 5468 5165 +f 4121 5468 4122 +f 4121 4122 4904 +f 4121 4904 4903 +f 4122 4905 4904 +f 4122 4129 4905 +f 4122 4128 4129 +f 4124 4130 4128 +f 4125 4128 4130 +f 4125 4130 4131 +f 4125 4978 5289 +f 4125 5289 4128 +f 4125 4131 4132 +f 4126 4129 5289 +f 4126 4978 4127 +f 4126 4951 4129 +f 4126 5289 4978 +f 4126 4127 4951 +f 4127 4949 4950 +f 4127 4134 4949 +f 4127 4978 4134 +f 4127 4950 4951 +f 4128 5289 4129 +f 4129 5450 4905 +f 4129 4951 5450 +f 4131 5547 4132 +f 4132 5547 4135 +f 4133 4136 4134 +f 4133 4135 4139 +f 4133 4139 4136 +f 4133 4134 4978 +f 4134 4137 4949 +f 4134 4136 4137 +f 4136 5582 4137 +f 4136 4139 4140 +f 4137 4138 4947 +f 4137 4947 4949 +f 4137 5582 4138 +f 4138 4948 4947 +f 4138 5582 4145 +f 4138 4145 4948 +f 4140 4146 4144 +f 4141 4147 4148 +f 4141 4144 4146 +f 4141 4142 4144 +f 4141 4146 4147 +f 4142 5259 4145 +f 4142 4143 5996 +f 4142 4145 4144 +f 4142 5996 5259 +f 4143 4150 5996 +f 4143 4149 4150 +f 4144 4145 5582 +f 4146 5718 4147 +f 4147 5718 4148 +f 4149 4151 4155 +f 4149 4155 4152 +f 4149 4152 4150 +f 4150 4152 4153 +f 4150 5261 5996 +f 4150 4153 5261 +f 4152 4155 4156 +f 4153 4154 5262 +f 4153 5262 5261 +f 4154 5263 5262 +f 4154 4161 5263 +f 4154 4160 4161 +f 4157 4162 4158 +f 4157 4160 4162 +f 4157 4159 4160 +f 4159 4161 4160 +f 4159 5194 4161 +f 4159 5193 5194 +f 4163 5688 4349 +f 4163 5087 4187 +f 4163 4349 5087 +f 4163 4187 4191 +f 4163 4191 5688 +f 4164 4346 4344 +f 4164 4353 4346 +f 4164 4352 4353 +f 4164 4344 4347 +f 4164 4347 4348 +f 4164 4348 4352 +f 4165 4333 5758 +f 4165 4341 4342 +f 4165 4342 4339 +f 4165 4339 4333 +f 4165 5758 4341 +f 4166 4332 4336 +f 4166 4330 4328 +f 4166 4331 4332 +f 4166 4336 4330 +f 4166 4328 4331 +f 4167 4317 5768 +f 4167 4325 4326 +f 4167 4326 4323 +f 4167 5768 4325 +f 4167 4323 4317 +f 4168 5823 4312 +f 4168 4314 5823 +f 4168 4320 4314 +f 4168 4312 4316 +f 4168 4316 4320 +f 4169 4302 4303 +f 4169 4307 4301 +f 4169 4301 4302 +f 4169 4303 4309 +f 4169 5730 4307 +f 4169 4309 5730 +f 4170 4304 4298 +f 4170 4300 4304 +f 4170 4296 4299 +f 4170 4298 4297 +f 4170 4299 4300 +f 4170 4297 4296 +f 4171 4285 5111 +f 4171 4294 4291 +f 4171 4293 4294 +f 4171 5111 4293 +f 4171 4291 4290 +f 4171 4290 4285 +f 4172 4280 5381 +f 4172 4288 4282 +f 4172 5381 4288 +f 4172 4282 4281 +f 4172 4281 4280 +f 4173 5293 4269 +f 4173 4277 4278 +f 4173 4278 4275 +f 4173 4275 5293 +f 4173 4968 4277 +f 4173 4269 4968 +f 4174 4268 4272 +f 4174 4272 4266 +f 4174 4264 4268 +f 4174 4265 4264 +f 4174 4266 4265 +f 4175 4262 4259 +f 4175 4261 4262 +f 4175 4254 4255 +f 4175 4253 4254 +f 4175 4258 4253 +f 4175 4259 4258 +f 4175 4255 4261 +f 4176 4248 4251 +f 4176 4251 4252 +f 4176 4252 4256 +f 4176 4256 4250 +f 4176 4250 4248 +f 4177 5060 4245 +f 4177 4243 4242 +f 4177 4242 4237 +f 4177 4237 5060 +f 4177 4245 4246 +f 4177 4246 4243 +f 4178 5652 4240 +f 4178 4240 4234 +f 4178 5195 4232 +f 4178 4234 5195 +f 4178 4232 5652 +f 4179 4221 5420 +f 4179 5420 4229 +f 4179 4229 4230 +f 4179 4230 4227 +f 4179 4227 4221 +f 4180 4218 4217 +f 4180 4216 5114 +f 4180 4224 4218 +f 4180 5114 4224 +f 4180 4217 4216 +f 4181 4205 5892 +f 4181 5892 4213 +f 4181 4211 4205 +f 4181 4214 4211 +f 4181 4213 4214 +f 4182 4203 4208 +f 4182 4185 4203 +f 4182 5376 4185 +f 4182 4208 5777 +f 4182 5777 5376 +f 4183 4186 5821 +f 4183 4199 4186 +f 4183 4198 4199 +f 4183 4188 4198 +f 4183 5973 4188 +f 4183 5821 5973 +f 4184 5110 4186 +f 4184 4201 4185 +f 4184 4186 4201 +f 4184 4185 5376 +f 4184 5376 5110 +f 4185 4201 4202 +f 4185 4202 4203 +f 4186 4193 5821 +f 4186 5110 4193 +f 4186 4200 4201 +f 4186 4199 4200 +f 4187 5973 4190 +f 4187 5087 5202 +f 4187 4188 5973 +f 4187 5202 4188 +f 4187 4190 4191 +f 4188 5202 4198 +f 4189 4190 5973 +f 4189 5973 4192 +f 4191 4351 5688 +f 4192 5973 5821 +f 4192 5821 4193 +f 4193 4194 5526 +f 4193 5110 4194 +f 4194 5110 4195 +f 4194 4195 5526 +f 4195 5376 5777 +f 4195 5777 4196 +f 4195 4196 5443 +f 4195 5110 5376 +f 4196 4209 5443 +f 4196 5985 4209 +f 4196 5777 5985 +f 4197 4198 5202 +f 4197 4910 4198 +f 4197 4204 4910 +f 4197 5087 4204 +f 4197 5202 5087 +f 4198 4910 4911 +f 4198 4911 4199 +f 4199 4912 4200 +f 4199 4911 4912 +f 4200 4913 4201 +f 4200 4912 4913 +f 4201 4913 4914 +f 4201 4914 4202 +f 4202 5034 4203 +f 4202 4914 5034 +f 4203 4212 4208 +f 4204 4909 4910 +f 4204 4908 4909 +f 4204 4350 4908 +f 4204 5217 4350 +f 4204 5087 5217 +f 4205 4210 5985 +f 4205 5272 5892 +f 4205 4208 5272 +f 4205 4211 4210 +f 4205 5985 4208 +f 4206 4207 5892 +f 4206 5892 5272 +f 4206 5272 4212 +f 4206 4942 4207 +f 4206 4212 4942 +f 4207 4941 4940 +f 4207 4940 4215 +f 4207 4942 4941 +f 4207 4215 5892 +f 4208 4212 5272 +f 4208 5985 5777 +f 4209 5985 4210 +f 4213 5892 4215 +f 4213 4215 4216 +f 4213 4216 4217 +f 4213 4217 4214 +f 4214 4217 5483 +f 4215 4219 4216 +f 4215 4940 4219 +f 4216 4219 5114 +f 4217 4218 5483 +f 4218 4224 4225 +f 4218 4225 5625 +f 4219 4940 4943 +f 4219 4220 5114 +f 4219 4943 4220 +f 4220 4944 4228 +f 4220 4228 5114 +f 4220 4943 4944 +f 4221 5636 5420 +f 4221 4227 4226 +f 4221 4226 4225 +f 4221 4225 4224 +f 4221 4224 5636 +f 4222 5420 5636 +f 4222 4223 5420 +f 4222 4228 4813 +f 4222 5636 4228 +f 4222 4813 4223 +f 4223 4809 4231 +f 4223 4231 5420 +f 4223 4813 4809 +f 4224 4228 5636 +f 4224 5114 4228 +f 4225 4226 5888 +f 4225 5888 5625 +f 4226 4227 5888 +f 4229 5420 4231 +f 4229 4231 4232 +f 4229 4232 5195 +f 4229 5195 4230 +f 4230 5195 4233 +f 4231 4810 4235 +f 4231 4809 4810 +f 4231 4235 4232 +f 4232 4235 5652 +f 4233 5195 4234 +f 4234 4240 4241 +f 4235 4810 4811 +f 4235 4236 5652 +f 4235 4811 4236 +f 4236 4812 4244 +f 4236 4811 4812 +f 4236 4244 5652 +f 4237 4240 4238 +f 4237 4238 5060 +f 4237 4241 4240 +f 4237 4242 4241 +f 4238 4244 4994 +f 4238 4240 4244 +f 4238 4239 5060 +f 4238 4994 4239 +f 4239 4994 4990 +f 4239 4990 4247 +f 4239 4247 5060 +f 4240 5652 4244 +f 4241 4242 5575 +f 4242 4243 5359 +f 4242 5359 5575 +f 4243 4246 5741 +f 4243 5741 5359 +f 4245 4249 4246 +f 4245 4248 4249 +f 4245 5060 4247 +f 4245 4247 4248 +f 4246 5639 5741 +f 4246 4249 5639 +f 4247 4990 4991 +f 4247 4251 4248 +f 4247 4991 4251 +f 4248 4250 4249 +f 4249 4250 5639 +f 4250 4256 4257 +f 4251 4992 4252 +f 4251 4991 4992 +f 4252 4992 4993 +f 4252 4260 4256 +f 4252 4993 4260 +f 4253 4256 4254 +f 4253 4257 4256 +f 4253 4258 4257 +f 4254 4260 5338 +f 4254 5338 4255 +f 4254 4256 4260 +f 4255 5337 4263 +f 4255 4263 4261 +f 4255 5338 5337 +f 4258 4259 5430 +f 4259 4262 5430 +f 4261 4265 4262 +f 4261 4263 4264 +f 4261 4264 4265 +f 4263 5336 4267 +f 4263 5337 5336 +f 4263 4267 4264 +f 4264 4267 4268 +f 4266 4272 4273 +f 4267 5336 5335 +f 4267 5335 4268 +f 4268 5335 5334 +f 4268 4276 4272 +f 4268 5334 4276 +f 4269 5293 4272 +f 4269 4270 4968 +f 4269 4272 4270 +f 4270 4276 4936 +f 4270 4272 4276 +f 4270 4271 4968 +f 4270 4936 4271 +f 4271 4935 4279 +f 4271 4279 4968 +f 4271 4936 4935 +f 4272 5293 4273 +f 4273 5293 4274 +f 4274 5293 4275 +f 4276 5362 4936 +f 4276 5334 5362 +f 4277 4279 4280 +f 4277 4968 4279 +f 4277 4281 4278 +f 4277 4280 4281 +f 4278 4281 5743 +f 4279 4283 4280 +f 4279 4934 4283 +f 4279 4935 4934 +f 4280 4283 5381 +f 4281 4282 5743 +f 4282 4288 4289 +f 4283 4937 4284 +f 4283 4934 4937 +f 4283 4284 5381 +f 4284 4292 5969 +f 4284 4937 4938 +f 4284 5969 5381 +f 4284 4938 4292 +f 4285 5659 5111 +f 4285 4288 5659 +f 4285 4290 4289 +f 4285 4289 4288 +f 4286 5969 4292 +f 4286 5111 5659 +f 4286 4287 5111 +f 4286 4292 4892 +f 4286 5659 5969 +f 4286 4892 4287 +f 4287 5436 5111 +f 4287 4295 5436 +f 4287 4891 4295 +f 4287 4892 4891 +f 4288 5969 5659 +f 4288 5381 5969 +f 4292 6165 4892 +f 4292 4938 6165 +f 4293 5436 4296 +f 4293 5111 5436 +f 4293 4296 4297 +f 4293 4297 4294 +f 4295 4890 4299 +f 4295 4891 4890 +f 4295 4299 5436 +f 4296 5436 4299 +f 4298 4305 5962 +f 4298 4304 4305 +f 4299 4890 4889 +f 4299 4889 4300 +f 4300 4308 4304 +f 4300 4888 4308 +f 4300 4889 4888 +f 4301 4304 4302 +f 4301 4307 4306 +f 4301 4305 4304 +f 4301 4306 4305 +f 4302 4308 4899 +f 4302 4304 4308 +f 4302 4899 4303 +f 4303 4311 4309 +f 4303 4897 4311 +f 4303 4899 4898 +f 4303 4898 4897 +f 4305 4306 5962 +f 4306 4307 5620 +f 4307 4310 5846 +f 4307 5846 5620 +f 4307 5730 4310 +f 4308 4888 5844 +f 4308 5844 4899 +f 4309 4312 5823 +f 4309 5823 5730 +f 4309 4311 4312 +f 4310 4313 5846 +f 4310 5730 4313 +f 4311 4315 4312 +f 4311 4897 4315 +f 4312 4315 4316 +f 4313 5823 4314 +f 4313 5730 5823 +f 4314 4320 4321 +f 4315 4896 4316 +f 4315 4897 4896 +f 4316 4900 4324 +f 4316 4896 4900 +f 4316 4324 4320 +f 4317 5801 5768 +f 4317 4320 5801 +f 4317 4323 4322 +f 4317 4322 4321 +f 4317 4321 4320 +f 4318 5801 4324 +f 4318 4319 5768 +f 4318 5116 4319 +f 4318 4324 5116 +f 4318 5768 5801 +f 4319 4327 5768 +f 4319 5116 5117 +f 4319 5117 4327 +f 4320 4324 5801 +f 4323 4326 5210 +f 4325 4327 4328 +f 4325 5768 4327 +f 4325 4329 4326 +f 4325 4328 4329 +f 4326 4329 5811 +f 4326 5811 5210 +f 4327 4331 4328 +f 4327 5117 5118 +f 4327 5118 4331 +f 4328 4330 4329 +f 4329 4969 5811 +f 4329 4330 4969 +f 4330 4337 4969 +f 4330 4336 4337 +f 4331 5118 5119 +f 4331 5119 4332 +f 4332 5120 4340 +f 4332 4340 4336 +f 4332 5119 5120 +f 4333 4336 4334 +f 4333 4334 5758 +f 4333 4339 4338 +f 4333 4337 4336 +f 4333 4338 4337 +f 4334 4336 4340 +f 4334 4335 5758 +f 4334 5321 4335 +f 4334 4340 5321 +f 4335 4343 5759 +f 4335 5320 4343 +f 4335 5759 5758 +f 4335 5321 5320 +f 4341 4344 4345 +f 4341 4345 4342 +f 4341 5758 5759 +f 4341 5759 4344 +f 4342 4345 5899 +f 4343 5322 4347 +f 4343 4347 5759 +f 4343 5320 5322 +f 4344 4346 4345 +f 4344 5759 4347 +f 4345 4346 5899 +f 4347 5323 4348 +f 4347 5322 5323 +f 4348 5323 5324 +f 4348 4354 4352 +f 4348 5324 4354 +f 4349 5217 5087 +f 4349 4352 5217 +f 4349 5688 4352 +f 4350 4354 4908 +f 4350 5217 4354 +f 4351 4353 5688 +f 4352 4354 5217 +f 4352 5688 4353 +f 4355 4418 4696 +f 4355 4387 5906 +f 4355 5695 4418 +f 4355 4696 4387 +f 4356 4698 4418 +f 4356 5581 4417 +f 4356 5695 5581 +f 4356 4418 5695 +f 4356 4417 4698 +f 4357 4416 4700 +f 4357 5731 4416 +f 4357 4417 5109 +f 4357 4700 4417 +f 4357 5109 5731 +f 4358 4702 4701 +f 4358 4701 4416 +f 4358 5615 4415 +f 4358 4416 5731 +f 4358 5731 5615 +f 4358 4415 4702 +f 4359 4704 4415 +f 4359 4415 5774 +f 4359 5774 4414 +f 4359 4414 4704 +f 4360 5432 4413 +f 4360 4413 4706 +f 4360 4706 4414 +f 4361 4413 5432 +f 4361 4412 4708 +f 4361 4708 4413 +f 4362 4411 4710 +f 4362 4710 4412 +f 4363 4410 4712 +f 4363 4712 4411 +f 4364 4410 5416 +f 4364 4714 4410 +f 4364 4409 4714 +f 4364 5416 4409 +f 4365 4408 4716 +f 4365 4716 4409 +f 4366 4407 4718 +f 4366 5049 4407 +f 4366 4408 5049 +f 4366 4717 4408 +f 4366 4718 4717 +f 4367 6042 4406 +f 4367 4720 4407 +f 4367 4406 4720 +f 4368 6042 6009 +f 4368 4406 6042 +f 4368 4405 4722 +f 4368 4722 4406 +f 4368 6009 4405 +f 4369 4724 4405 +f 4369 5672 4404 +f 4369 4404 4724 +f 4370 4404 5672 +f 4370 4403 4726 +f 4370 4726 4404 +f 4370 4894 4403 +f 4371 4728 4403 +f 4371 4403 4894 +f 4371 4402 4728 +f 4371 5079 4402 +f 4372 4402 5079 +f 4372 4401 4730 +f 4372 4730 4402 +f 4373 4400 4732 +f 4373 4732 4401 +f 4374 4399 4734 +f 4374 4734 4733 +f 4374 4733 4400 +f 4374 5753 4399 +f 4374 4400 5753 +f 4375 4399 5017 +f 4375 5017 4398 +f 4375 4736 4399 +f 4375 4398 4736 +f 4376 5716 6035 +f 4376 6035 4397 +f 4376 4398 5716 +f 4376 4397 4738 +f 4376 4738 4398 +f 4377 6035 6037 +f 4377 4397 6035 +f 4377 4396 4740 +f 4377 5002 4396 +f 4377 4740 4397 +f 4377 6037 5002 +f 4378 4396 5002 +f 4378 5046 4395 +f 4378 4395 4742 +f 4378 5002 5046 +f 4378 4742 4396 +f 4379 4680 4395 +f 4379 4394 4680 +f 4379 5938 4394 +f 4380 4393 4682 +f 4380 4682 4681 +f 4380 5938 5934 +f 4380 5934 5831 +f 4380 4394 5938 +f 4380 4681 4394 +f 4380 5831 4393 +f 4381 5813 5264 +f 4381 5264 4392 +f 4381 4684 4393 +f 4381 4392 4684 +f 4381 4393 5831 +f 4381 5831 5813 +f 4382 5648 4391 +f 4382 5264 5648 +f 4382 4392 5264 +f 4382 4686 4685 +f 4382 4685 4392 +f 4382 4391 4686 +f 4383 4390 4688 +f 4383 4964 4390 +f 4383 4688 4391 +f 4384 5657 4389 +f 4384 4389 4690 +f 4384 4690 4390 +f 4384 4390 4964 +f 4385 5576 4388 +f 4385 4692 4389 +f 4385 4388 4692 +f 4385 4389 5657 +f 4386 4388 5576 +f 4386 4694 4388 +f 4386 4387 4694 +f 4386 5906 4387 +f 4387 4696 4695 +f 4387 4695 4694 +f 4388 4694 4693 +f 4388 4693 4692 +f 4389 4692 4691 +f 4389 4691 4690 +f 4390 4689 4688 +f 4390 4690 4689 +f 4391 4687 4686 +f 4391 4688 4687 +f 4392 4685 4684 +f 4393 4684 4683 +f 4393 4683 4682 +f 4394 4681 4680 +f 4395 4680 4743 +f 4395 4743 4742 +f 4396 4741 4740 +f 4396 4742 4741 +f 4397 4740 4739 +f 4397 4739 4738 +f 4398 4738 4737 +f 4398 4737 4736 +f 4399 4735 4734 +f 4399 4736 4735 +f 4400 4733 4732 +f 4401 4732 4731 +f 4401 4731 4730 +f 4402 4729 4728 +f 4402 4730 4729 +f 4403 4728 4727 +f 4403 4727 4726 +f 4404 4725 4724 +f 4404 4726 4725 +f 4405 4724 4723 +f 4405 4723 4722 +f 4406 4722 4721 +f 4406 4721 4720 +f 4407 4719 4718 +f 4407 4720 4719 +f 4408 4717 4716 +f 4409 4715 4714 +f 4409 4716 4715 +f 4410 4714 4713 +f 4410 4713 4712 +f 4411 4711 4710 +f 4411 4712 4711 +f 4412 4710 4709 +f 4412 4709 4708 +f 4413 4707 4706 +f 4413 4708 4707 +f 4414 4706 4705 +f 4414 4705 4704 +f 4415 4704 4703 +f 4415 4703 4702 +f 4416 4701 4700 +f 4417 4699 4698 +f 4417 4700 4699 +f 4418 4697 4696 +f 4418 4698 4697 +f 4419 4482 4770 +f 4419 4770 4452 +f 4420 4481 4772 +f 4420 5664 4481 +f 4420 4772 4482 +f 4421 4774 4481 +f 4421 4480 4774 +f 4421 4481 5664 +f 4421 5664 5047 +f 4421 5047 4480 +f 4422 5481 4479 +f 4422 4776 4480 +f 4422 4479 4776 +f 4423 4478 4778 +f 4423 4479 5481 +f 4423 5481 5378 +f 4423 5078 4478 +f 4423 4778 4777 +f 4423 4777 4479 +f 4423 5378 5078 +f 4424 5769 4477 +f 4424 4780 4478 +f 4424 4477 4780 +f 4424 4478 5078 +f 4425 4477 5769 +f 4425 4782 4477 +f 4425 4476 4782 +f 4426 4475 4784 +f 4426 4784 4476 +f 4426 4955 4475 +f 4427 4786 4475 +f 4427 4474 4786 +f 4427 4955 5252 +f 4427 4475 4955 +f 4427 5252 4474 +f 4428 4788 4474 +f 4428 4473 4788 +f 4429 4472 4790 +f 4429 4790 4473 +f 4430 4792 4472 +f 4430 4471 4792 +f 4431 5752 4470 +f 4431 4471 5752 +f 4431 4794 4793 +f 4431 4793 4471 +f 4431 4470 4794 +f 4432 4796 4470 +f 4432 5403 4469 +f 4432 4469 4796 +f 4433 4469 5403 +f 4433 4468 5422 +f 4433 4798 4469 +f 4433 6029 4468 +f 4433 5422 4798 +f 4434 4468 6029 +f 4434 4467 5355 +f 4434 5355 4468 +f 4435 5887 4466 +f 4435 4466 4802 +f 4435 4802 4467 +f 4436 5887 6006 +f 4436 4465 4804 +f 4436 4804 4466 +f 4436 4971 4465 +f 4436 6006 4971 +f 4436 4466 5887 +f 4437 5134 4464 +f 4437 4971 5134 +f 4437 4465 4971 +f 4437 4806 4465 +f 4437 4464 4806 +f 4438 4463 4744 +f 4438 4744 4464 +f 4439 4746 4745 +f 4439 4745 4463 +f 4439 4463 5554 +f 4439 4906 4462 +f 4439 4462 4746 +f 4439 5554 4906 +f 4440 4462 4906 +f 4440 4461 4748 +f 4440 5077 4461 +f 4440 4748 4462 +f 4441 4461 5077 +f 4441 4460 4750 +f 4441 4750 4461 +f 4442 5213 4459 +f 4442 4752 4460 +f 4442 4459 4752 +f 4443 4754 4459 +f 4443 4458 4754 +f 4443 4459 5213 +f 4444 4756 4458 +f 4444 4457 4756 +f 4445 4456 4758 +f 4445 4758 4457 +f 4446 4456 4939 +f 4446 4455 4760 +f 4446 4760 4456 +f 4446 4939 4455 +f 4447 4762 4761 +f 4447 4761 4455 +f 4447 4454 4762 +f 4448 4764 4454 +f 4448 4453 4764 +f 4448 5445 4453 +f 4449 4453 5445 +f 4449 4766 4453 +f 4449 4451 4766 +f 4450 4452 4768 +f 4450 4768 4451 +f 4451 4767 4766 +f 4451 4768 4767 +f 4452 4769 4768 +f 4452 4770 4769 +f 4453 4765 4764 +f 4453 4766 4765 +f 4454 4764 4763 +f 4454 4763 4762 +f 4455 4761 4760 +f 4456 4759 4758 +f 4456 4760 4759 +f 4457 4758 4757 +f 4457 4757 4756 +f 4458 4756 4755 +f 4458 4755 4754 +f 4459 4754 4753 +f 4459 4753 4752 +f 4460 4752 4751 +f 4460 4751 4750 +f 4461 4749 4748 +f 4461 4750 4749 +f 4462 4747 4746 +f 4462 4748 4747 +f 4463 4745 4744 +f 4464 4807 4806 +f 4464 4744 4807 +f 4465 4806 4805 +f 4465 4805 4804 +f 4466 4804 4803 +f 4466 4803 4802 +f 4467 4801 5355 +f 4467 4802 4801 +f 4468 5355 5422 +f 4469 4798 4797 +f 4469 4797 4796 +f 4470 4796 4795 +f 4470 4795 4794 +f 4471 4793 4792 +f 4472 4792 4791 +f 4472 4791 4790 +f 4473 4789 4788 +f 4473 4790 4789 +f 4474 4787 4786 +f 4474 4788 4787 +f 4475 4785 4784 +f 4475 4786 4785 +f 4476 4784 4783 +f 4476 4783 4782 +f 4477 4782 4781 +f 4477 4781 4780 +f 4478 4779 4778 +f 4478 4780 4779 +f 4479 4777 4776 +f 4480 4776 4775 +f 4480 4775 4774 +f 4481 4773 4772 +f 4481 4774 4773 +f 4482 4771 4770 +f 4482 4772 4771 +f 4483 4484 5953 +f 4484 4485 5953 +f 4486 5280 4487 +f 4487 5280 4488 +f 4489 5456 4490 +f 4490 5456 4945 +f 4490 4945 4491 +f 4491 5738 4492 +f 4491 4945 5738 +f 4492 5738 4493 +f 4495 4496 5419 +f 4496 4497 5419 +f 4496 5824 4497 +f 4497 5847 5419 +f 4497 5824 4498 +f 4497 4498 5847 +f 4501 5917 4502 +f 4502 5917 4503 +f 4504 4505 5363 +f 4505 4506 5363 +f 4512 5474 4513 +f 4513 5097 4514 +f 4513 5474 5097 +f 4514 5097 4515 +f 4517 4518 5626 +f 4518 4519 5626 +f 4519 5247 4520 +f 4520 5247 4521 +f 4523 5791 4524 +f 4524 5791 4525 +f 4530 5970 4531 +f 4531 5970 4532 +f 4541 4542 5631 +f 4542 4543 5631 +f 4547 5447 4548 +f 4547 4610 5447 +f 4548 5169 4549 +f 4549 5169 4550 +f 4554 4555 5059 +f 4555 4556 5059 +f 4567 4568 5312 +f 4568 4569 5312 +f 4570 4571 5796 +f 4571 4572 5796 +f 4572 4573 5197 +f 4573 4574 5197 +f 4575 5487 4576 +f 4575 4576 5577 +f 4576 5487 4577 +f 4576 4577 5577 +f 4577 4578 5248 +f 4577 5845 4578 +f 4578 5845 4579 +f 4578 5945 5248 +f 4578 4579 5945 +f 4579 5927 5942 +f 4579 5845 5927 +f 4579 5519 5945 +f 4579 5942 4580 +f 4579 4580 5519 +f 4580 5942 4581 +f 4584 4585 5352 +f 4585 4586 5352 +f 4587 4588 5921 +f 4588 4589 5921 +f 4595 5778 4596 +f 4596 5778 4597 +f 4600 4601 5609 +f 4601 4602 5609 +f 4602 6034 4603 +f 4602 4603 5507 +f 4602 5507 5609 +f 4603 6034 4604 +f 4603 4604 5382 +f 4604 6034 6057 +f 4604 6057 4605 +f 4604 4605 5382 +f 4605 6057 6063 +f 4605 6063 4606 +f 4605 4606 5521 +f 4606 6063 5754 +f 4606 4607 5521 +f 4606 5754 4607 +f 4611 6126 6070 +f 4616 5864 4617 +f 4617 5864 4618 +f 4644 5800 4645 +f 4645 5800 4646 +f 4647 5465 4648 +f 4648 5465 4649 +f 4651 5931 4652 +f 4652 5931 4653 +f 4663 4664 5490 +f 4674 6111 4675 +f 4674 4675 6030 +f 4675 4676 6030 +f 4675 6119 4676 +f 4675 6111 6119 +f 4676 6044 6046 +f 4676 6046 4677 +f 4676 5548 6030 +f 4676 4677 5548 +f 4676 6119 6044 +f 4677 6046 5423 +f 4677 5423 4678 +f 4783 4784 5709 +f 4784 4785 5709 +f 4798 5422 4799 +f 4799 5422 4800 +f 4800 5422 5355 +f 4800 5355 4801 +f 4843 5889 5425 +f 4843 5425 4844 +f 4844 4982 4845 +f 4844 5425 4982 +f 4845 5270 4846 +f 4845 4982 5270 +f 4846 5270 4847 +f 4856 5431 4857 +f 4857 5431 4858 +f 4880 5895 4881 +f 4881 5895 4882 +f 4884 6012 5525 +f 4884 4989 5690 +f 4885 5440 5452 +f 4928 5983 4929 +f 4928 4929 5579 +f 4929 5558 5579 +f 4950 5562 4951 +f 4951 5562 5450 +f 4953 4999 5788 +f 5037 5839 5882 +f 5037 5780 5839 +f 5045 6110 6113 +f 5045 5080 6110 +f 5113 6124 6122 +f 5137 5896 5901 +f 5144 6162 6161 +f 5179 5814 5798 +f 5209 6150 6161 +f 5209 6154 6150 +f 5212 6163 6160 +f 5212 6085 6163 +f 5224 5590 5589 +f 5231 6159 6086 +f 5248 5945 5519 +f 5259 5996 5260 +f 5260 5996 5261 +f 5265 5794 5780 +f 5269 5722 5617 +f 5281 5907 5767 +f 5304 5825 5313 +f 5304 5313 5596 +f 5326 5589 5651 +f 5332 5564 5438 +f 5344 5345 5570 +f 5344 5570 5558 +f 5344 5663 5345 +f 5344 5558 5663 +f 5371 5704 5673 +f 5374 5691 5690 +f 5398 6064 6000 +f 5423 6046 6051 +f 5438 5822 5572 +f 5471 5681 6011 +f 5472 6135 6158 +f 5472 6158 6155 +f 5472 6156 6139 +f 5472 6155 6156 +f 5472 6139 6100 +f 5472 6100 6135 +f 5489 6025 5717 +f 5517 5640 5826 +f 5543 6123 6122 +f 5555 5826 5903 +f 5563 6101 6112 +f 5564 5566 5565 +f 5565 5835 5822 +f 5580 6038 6045 +f 5588 5638 5825 +f 5673 5697 5674 +f 5679 5762 5859 +f 5691 5932 6012 +f 5717 5949 6087 +f 5717 5924 5949 +f 5756 5773 5805 +f 5757 5897 5896 +f 5787 5881 6093 +f 5799 5854 5810 +f 5799 5947 5854 +f 5806 5820 5817 +f 5814 5859 6008 +f 5843 6003 5925 +f 5845 5886 5927 +f 5882 5980 5936 +f 5883 5924 5952 +f 5884 6132 6104 +f 5886 6006 5887 +f 5890 5898 5891 +f 5907 5908 5955 +f 5935 5959 6079 +f 5935 5936 5980 +f 5936 6073 6099 +f 5949 6059 6071 +f 5971 5992 5979 +f 5972 6096 6003 +f 6000 6064 6005 +f 6003 6096 6053 +f 6013 6073 6079 +f 6013 6079 6026 +f 6026 6032 6031 +f 6031 6060 6048 +f 6032 6114 6092 +f 6044 6051 6046 +f 6044 6119 6109 +f 6051 6090 6126 +f 6060 6092 6098 +f 6066 6086 6084 +f 6066 6139 6156 +f 6070 6126 6090 +f 6070 6090 6091 +f 6072 6097 6094 +f 6084 6163 6085 +f 6085 6147 6105 +f 6085 6105 6137 +f 6090 6109 6091 +f 6094 6117 6110 +f 6100 6143 6146 +f 6100 6142 6140 +f 6100 6146 6135 +f 6100 6139 6142 +f 6101 6152 6135 +f 6101 6102 6112 +f 6101 6135 6102 +f 6102 6135 6146 +f 6102 6146 6103 +f 6102 6132 6112 +f 6103 6143 6144 +f 6103 6146 6143 +f 6104 6144 6105 +f 6104 6105 6147 +f 6105 6144 6141 +f 6105 6141 6137 +f 6109 6119 6111 +f 6135 6152 6158 +f 6136 6140 6142 +f 6136 6145 6140 +f 6136 6137 6145 +f 6137 6141 6145 +f 6141 6144 6143 +f 6148 6158 6152 +f 6148 6151 6158 +f 6148 6149 6150 +f 6151 6155 6158 +f 6151 6154 6156 +f 6151 6156 6155 +f 6159 6160 6163 diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt new file mode 100644 index 00000000..1558cdbc --- /dev/null +++ b/docs/CMakeLists.txt @@ -0,0 +1,15 @@ +# Documentation is intentionally a standalone target: it parses headers and +# must not import the Python extension or compile CUDA code. +find_package(Doxygen REQUIRED) +find_program(SPHINX_BUILD_EXECUTABLE NAMES sphinx-build REQUIRED) + +add_custom_target( + docs + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_SOURCE_DIR}/_build/doxygen + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile + COMMAND ${SPHINX_BUILD_EXECUTABLE} -W --keep-going -b html + ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/_build/html + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building DEM-Engine Doxygen and Sphinx documentation" + VERBATIM +) diff --git a/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md similarity index 100% rename from CONTRIBUTORS.md rename to docs/CONTRIBUTORS.md diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 00000000..77af9432 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,71 @@ +# DEM-Engine public C++ API documentation. +# +# Run this file from the docs directory. Sphinx consumes the generated XML +# through Breathe and presents it alongside the user and Python guides. + +PROJECT_NAME = "DEM-Engine" +PROJECT_NUMBER = "3" +PROJECT_BRIEF = "GPU-accelerated discrete element method solver" + +OUTPUT_DIRECTORY = _build/doxygen +CREATE_SUBDIRS = NO +ALLOW_UNICODE_NAMES = YES +OUTPUT_LANGUAGE = English + +INPUT = ../src/DEM/API.h \ + ../src/DEM/AuxClasses.h \ + ../src/DEM/BdrsAndObjs.h \ + ../src/DEM/Models.h \ + ../src/DEM/Structs.h \ + ../src/DEM/VariableTypes.h \ + ../src/DEM/utils/ClumpMassProperties.hpp \ + ../src/DEM/utils/MeshUtils.hpp \ + ../src/DEM/utils/Samplers.hpp +FILE_PATTERNS = *.h *.hpp +RECURSIVE = NO +EXCLUDE = ../src/DEM/dT.h ../src/DEM/kT.h + +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO +EXTRACT_PACKAGE = NO +EXTRACT_STATIC = NO +HIDE_UNDOC_MEMBERS = YES +HIDE_UNDOC_CLASSES = YES +HIDE_FRIEND_COMPOUNDS = YES +INTERNAL_DOCS = NO + +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = YES +MARKDOWN_SUPPORT = NO +MULTILINE_CPP_IS_BRIEF = NO +INHERIT_DOCS = YES +SEPARATE_MEMBER_PAGES = NO +SORT_MEMBER_DOCS = YES +SORT_BRIEF_DOCS = YES + +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +SKIP_FUNCTION_MACROS = YES +PREDEFINED = __host__= __device__= __global__= \ + __forceinline__= __shared__= __align__(x)= \ + __launch_bounds__(x)= + +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml +XML_PROGRAMLISTING = NO + +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = NO +WARN_IF_DOC_ERROR = YES +WARN_IF_INCOMPLETE_DOC = YES +WARN_NO_PARAMDOC = NO +WARN_LOGFILE = _build/doxygen-warnings.log + +STRIP_FROM_PATH = .. +FULL_PATH_NAMES = YES +REFERENCES_RELATION = YES +REFERENCED_BY_RELATION = YES +SOURCE_BROWSER = NO diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..583754e2 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,31 @@ +SPHINXBUILD ?= sphinx-build +DOXYGEN ?= doxygen +PYTHON ?= python3 +SOURCEDIR = . +BUILDDIR = _build + +.PHONY: help examples python-reference doxygen html clean + +help: + @echo "make html Build Doxygen XML and the Sphinx HTML site" + @echo "make examples Check Python documentation example syntax" + @echo "make python-reference Regenerate API RST from an installed deme module" + @echo "make doxygen Build only the Doxygen XML" + @echo "make clean Remove generated documentation" + +examples: + $(PYTHON) -m compileall -q python/examples + +python-reference: + $(PYTHON) generate_python_reference.py + +doxygen: + @mkdir -p $(BUILDDIR)/doxygen + $(DOXYGEN) Doxyfile + +html: examples doxygen + $(SPHINXBUILD) -W --keep-going -b html $(SOURCEDIR) $(BUILDDIR)/html + @echo "Documentation: $(CURDIR)/$(BUILDDIR)/html/index.html" + +clean: + $(SPHINXBUILD) -M clean $(SOURCEDIR) $(BUILDDIR) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..fa73e791 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,147 @@ +# DEM-Engine documentation + +Start with the [documentation index](index.rst), [installation](installation.rst), +or [Python quickstart](python/quickstart.rst). Maintainers can find +[hosting options](developer/hosting.rst) and [wheel packaging](developer/packaging.rst) +in the developer guide. + +## Building and previewing + +The documentation site combines: + +- authored Sphinx pages for installation, concepts, and Python usage; +- a C++ API reference generated by Doxygen and Breathe; and +- a static Python API reference generated from the installed pybind11 module. + +Generated HTML, Doxygen XML, and other files under `docs/_build/` are ignored +by Git. + +## Install the documentation tools + +Doxygen must be installed as an executable. Create a Python environment and +install the Sphinx dependencies: + +```bash +python3 -m venv .venv-docs +source .venv-docs/bin/activate +python -m pip install -r docs/requirements.txt +``` + +Install Doxygen through the operating system or Conda if it is not already +available: + +```bash +conda install -c conda-forge doxygen +``` + +Check the toolchain: + +```bash +doxygen --version +sphinx-build --version +``` + +## Build the complete site + +From the repository root: + +```bash +/usr/bin/make -C docs html +``` + +Using `/usr/bin/make` is helpful on machines where `make` is aliased to +`ninja`. This command: + +1. syntax-checks the Python examples; +2. generates Doxygen XML for the C++ public API; and +3. builds the complete Sphinx HTML site with warnings treated as errors. + +The resulting entry page is: + +```text +docs/_build/html/index.html +``` + +To remove generated output: + +```bash +/usr/bin/make -C docs clean +``` + +The equivalent CMake documentation target is: + +```bash +cmake -S . -B build-docs -DDEME_BUILD_DOCS=ON +cmake --build build-docs --target docs +``` + +## Regenerate the Python API reference + +Normal HTML builds use the committed `docs/python/reference.rst` and therefore +do not import DEME or require a CUDA GPU. + +Regenerate that file after changing pybind11 signatures or docstrings. First +build and stage the Python extension: + +```bash +cmake -S . -B build-python \ + -DDEME_BUILD_PYTHON=ON \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build-python --target _deme --parallel +cmake --install build-python \ + --component python \ + --prefix /tmp/deme-python-reference +``` + +Then run the generator with the same Python interpreter used to build the +extension: + +```bash +PYTHONPATH=/tmp/deme-python-reference \ + /usr/bin/make -C docs python-reference +``` + +Finally, rebuild the site: + +```bash +/usr/bin/make -C docs html +``` + +Do not edit `docs/python/reference.rst` directly. Improve the corresponding +docstring in `src/DEM/python/bindings.cpp`, rebuild the extension, and +regenerate the page. + +## Preview on the same machine + +Run a local HTTP server from the repository root: + +```bash +python3 -m http.server 8000 \ + --bind 127.0.0.1 \ + --directory docs/_build/html +``` + +Open in a browser. Stop the server with `Ctrl+C`. + +## Preview a remote build through SSH + +Start the same loopback-only HTTP server on the remote build machine. From the +local workstation, create an SSH tunnel: + +```bash +ssh -L 8000:127.0.0.1:8000 username@remote-host +``` + +Keep the SSH connection open and visit locally. + +For a trusted local network, the server can instead listen on every interface: + +```bash +python3 -m http.server 8000 \ + --bind 0.0.0.0 \ + --directory docs/_build/html +``` + +The site is then available at `http://HOST_IP:8000/`, subject to firewall +rules. Prefer SSH forwarding when the remote host should not expose the +documentation server publicly. diff --git a/docs/_ext/repository_links.py b/docs/_ext/repository_links.py new file mode 100644 index 00000000..181eaa24 --- /dev/null +++ b/docs/_ext/repository_links.py @@ -0,0 +1,48 @@ +"""Resolve GitHub-readable relative source links in the compiled documentation.""" + +from pathlib import PurePosixPath +from urllib.parse import urlsplit + +from docutils import nodes +from sphinx import addnodes + + +def resolve_repository_links(app, doctree): + """Promote local source links before Sphinx collects downloads and resolves pages. + + Authored RST uses ordinary links so GitHub can render them. Sphinx needs + document references for .rst pages and download references for source files; + otherwise the HTML would point to files absent from the published site. + """ + for node in list(doctree.findall(nodes.reference)): + uri = node.get("refuri", "") + target = urlsplit(uri) + if target.scheme or target.netloc or not target.path or target.query or target.fragment: + continue + suffix = PurePosixPath(target.path).suffix + if suffix == ".rst": + replacement = addnodes.pending_xref( + node.rawsource, + refdomain="std", + reftype="doc", + reftarget=target.path[:-4], + refdoc=app.env.docname, + refexplicit=True, + refwarn=True, + ) + elif suffix in {".md", ".cpp", ".py"}: + replacement = addnodes.download_reference( + node.rawsource, reftarget=target.path, refdoc=app.env.docname + ) + else: + continue + replacement.source = node.source + replacement.line = node.line + replacement.extend(node.children) + node.replace_self(replacement) + + +def setup(app): + """Run before the download collector so Sphinx copies linked source files.""" + app.connect("doctree-read", resolve_repository_links, priority=100) + return {"version": "1", "parallel_read_safe": True, "parallel_write_safe": True} diff --git a/docs/archive/implementation-notes.md b/docs/archive/implementation-notes.md new file mode 100644 index 00000000..d41bb6ea --- /dev/null +++ b/docs/archive/implementation-notes.md @@ -0,0 +1,723 @@ +# Historical implementation notes + +These notes record earlier implementations and are not current API or build instructions. +Use the [documentation index](../index.rst), [mesh guide](../mesh-particles.rst), +and [installation guide](../installation.rst) for supported usage. File paths and +API signatures below describe the implementation at the time of writing. + +## Original file: IMPLEMENTATION_NOTES.md + +### Python Package Implementation Summary + +#### Overview +This implementation adds complete Python package building capability to the DEM-Engine project, allowing users to install it via `pip install .` and distribute it as a wheel file. + +#### Key Architectural Decisions + +##### 1. Modern Python Packaging (PEP 517/518) +- Uses `pyproject.toml` instead of legacy `setup.py` +- Leverages scikit-build-core for seamless CMake integration +- Follows current Python packaging standards + +##### 2. CMake Integration +- Added `DEME_BUILD_PYTHON` option to conditionally enable Python builds +- Separate `DEMERuntimeDataHelper_python` target for Python-specific needs +- Minimal changes to existing build system (no breaking changes) + +##### 3. Code Organization +- Python bindings isolated in `src/DEM/python/bindings.cpp` +- Clean separation from C++ library code +- Easy to maintain and extend + +##### 4. Robustness Improvements +- Multiple fallback mechanisms for site-packages detection +- Works in various Python environments (virtualenv, conda, system) +- Informative error messages + +#### Files Modified/Created + +##### Created: +1. `pyproject.toml` - Python package configuration +2. `MANIFEST.in` - Additional files to include in package +3. `PYTHON_BUILD.md` - Build documentation +4. `src/DEM/python/bindings.cpp` - Python bindings +5. `thirdparty/pybind11/` - pybind11 submodule (git submodule) + +##### Modified: +1. `CMakeLists.txt` - Added Python build support +2. `src/core/CMakeLists.txt` - Added Python-specific runtime helper +3. `src/DEM/CMakeLists.txt` - Added Python module build +4. `.gitmodules` - Added pybind11 submodule + +#### Building and Testing + +##### To Build the Package: +```bash +pip install . +``` + +##### To Create a Wheel: +```bash +pip install build +python -m build +``` + +##### To Test (requires CUDA): +```bash +python -c "import deme, DEME; print(deme.__version__, DEME.__version__)" +``` + +#### Advantages Over pyDEME_demo Branch + +1. **Modern Standards**: Uses pyproject.toml (PEP 517/518) +2. **Cleaner Integration**: scikit-build-core vs custom setup.py +3. **Better Organization**: Bindings in separate directory +4. **Less Duplication**: Refactored CMake code +5. **More Portable**: No hardcoded conda paths +6. **More Robust**: Multiple fallbacks for environment detection +7. **Better Maintainability**: Standard Python packaging practices + +#### Limitations and Future Work + +##### Current Limitations: +1. Requires CUDA toolkit at build time +2. Platform-specific wheels (not universal) +3. Cannot be tested in CI without GPU runners + +##### Recommended Future Enhancements: +1. Pre-built wheels for common platforms (Linux/CUDA combinations) +2. CI/CD pipeline for automated wheel building +3. Separate the JIT kernels from the binary for easier updates +4. Optional CPU-only mode for development/testing +5. More comprehensive Python test suite + +#### Security Considerations + +- No secrets or credentials in code +- Uses standard library constants (M_PI) +- Proper error handling for path detection +- No command injection vulnerabilities +- Safe type conversions in bindings + +#### Testing Recommendations + +Since CUDA is required and not available in all CI environments, testing should be done on a CUDA-enabled system: + +1. **Build Test**: Verify package builds successfully +2. **Import Test**: Verify module can be imported +3. **Basic Functionality**: Test DEMSolver instantiation +4. **Sampler Test**: Test various sampler functions +5. **Memory Test**: Verify no memory leaks in bindings + +#### Maintenance Notes + +- Keep pybind11 submodule updated (currently on stable branch) +- Update Python version classifiers in pyproject.toml as needed +- Monitor scikit-build-core for API changes +- Test with new CUDA versions as they're released + +## Original file: IMPLEMENTATION_SUMMARY.md + +### Implementation Summary: Mesh Template Functionality + +#### Objective + +Add mesh template functionality similar to clump templates, allowing users to: +1. Load meshes as templates (not immediately in simulation) +2. Instantiate multiple mesh particles from templates at different locations +3. Copy/duplicate existing mesh objects using shared_ptr + +#### Implementation Overview + +##### Core Features Implemented + +###### 1. LoadMeshType() - Mesh Template Loading +**Purpose**: Load a mesh as a template without adding it to the simulation + +**Overloads**: +```cpp +// From file with material +std::shared_ptr LoadMeshType( + const std::string& filename, + const std::shared_ptr& mat, + bool load_normals = true, + bool load_uv = false +); + +// From file without material +std::shared_ptr LoadMeshType( + const std::string& filename, + bool load_normals = true, + bool load_uv = false +); + +// From DEMMesh object +std::shared_ptr LoadMeshType(DEMMesh& mesh); +``` + +**Implementation Details**: +- Templates stored in `m_mesh_templates` vector (separate from `cached_mesh_objs`) +- Increments `nMeshTemplateLoad` counter +- Returns shared_ptr to template for later use +- Warns if mesh has 0 triangles + +###### 2. AddMeshFromTemplate() - Template Instantiation +**Purpose**: Create a mesh instance from a template at a specified location + +**Overloads**: +```cpp +// Using float3 position +std::shared_ptr AddMeshFromTemplate( + const std::shared_ptr& mesh_template, + const float3& init_pos +); + +// Using vector position +std::shared_ptr AddMeshFromTemplate( + const std::shared_ptr& mesh_template, + const std::vector& init_pos +); +``` + +**Implementation Details**: +- Creates a copy of the template: `DEMMesh mesh = *mesh_template` +- Sets initial position: `mesh.SetInitPos(init_pos)` +- Adds to simulation via existing `AddWavefrontMeshObject(mesh)` +- Returns shared_ptr to the created instance + +###### 3. Duplicate() - Mesh Object Copying +**Purpose**: Create a deep copy of an existing mesh object + +**Signature**: +```cpp +std::shared_ptr Duplicate( + const std::shared_ptr& ptr +); +``` + +**Implementation Details**: +- Creates a copy: `DEMMesh obj = *ptr` +- Adds via existing `AddWavefrontMeshObject(obj)` +- Returns shared_ptr to the new copy +- User can modify properties (position, mass, etc.) after duplication + +##### Storage and Tracking + +###### New Data Members (API.h): +```cpp +// Cached mesh templates (not yet instantiated in simulation) +std::vector> m_mesh_templates; + +// Number of mesh templates loaded. Never decreases. +size_t nMeshTemplateLoad = 0; + +// Last time initialized counter +size_t nLastTimeMeshTemplateLoad = 0; +``` + +##### Key Design Decisions + +1. **Separation of Concerns**: + - Templates stored in `m_mesh_templates` + - Instances stored in `cached_mesh_objs` + - Clear distinction between template and instance + +2. **Pattern Consistency**: + - Follows exact pattern of `LoadClumpType()` and `AddClumps()` + - Similar method naming convention + - Consistent error handling + +3. **One-by-one Instantiation**: + - As specified in requirements + - No batch creation like `AddClumps()` + - Simplifies implementation and usage + +4. **Minimal Changes**: + - Only additions, no modifications to existing code + - No changes to initialization logic (templates never cleared) + - Leverages existing `AddWavefrontMeshObject()` for instances + +#### Files Modified + +##### 1. src/DEM/API.h +**Changes**: ~60 lines added +- Method declarations (3 LoadMeshType overloads) +- Method declarations (2 AddMeshFromTemplate overloads) +- Duplicate() overload for DEMMesh +- Storage vector and counters + +##### 2. src/DEM/APIPublic.cpp +**Changes**: ~65 lines added +- Implementation of all new methods +- Error handling with DEME_ERROR/DEME_WARNING +- Follows existing code patterns exactly + +##### 3. test_mesh_template.cpp (new file) +**Purpose**: Comprehensive demonstration +- ~150 lines +- Tests all new features +- Includes full simulation +- Generates output files +- Follows existing demo patterns + +##### 4. MESH_TEMPLATE_USAGE.md (new file) +**Purpose**: User documentation +- ~150 lines +- Complete API reference +- Usage examples +- Comparison with clump templates +- Best practices + +##### 5. IMPLEMENTATION_SUMMARY.md (this file) +**Purpose**: Developer documentation +- Implementation details +- Design decisions +- Testing checklist + +#### Usage Example + +```cpp +#include + +int main() { + DEMSolver DEMSim; + auto mat = DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}}); + + // Step 1: Load mesh as template + auto mesh_template = DEMSim.LoadMeshType( + "path/to/mesh.obj", + mat, + true, // load_normals + false // load_uv + ); + + // Step 2: Create multiple instances + auto mesh1 = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(0, 0, 0)); + mesh1->SetFamily(0); + mesh1->SetMass(1000.); + mesh1->SetMOI(make_float3(200., 200., 200.)); + + auto mesh2 = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(2, 0, 0)); + mesh2->SetFamily(0); + mesh2->SetMass(1000.); + mesh2->SetMOI(make_float3(200., 200., 200.)); + + // Step 3: Duplicate existing mesh + auto mesh3 = DEMSim.Duplicate(mesh1); + mesh3->SetInitPos(make_float3(-2, 0, 0)); + + // Initialize and run simulation + DEMSim.Initialize(); + // ... simulation loop ... + + return 0; +} +``` + +#### Benefits + +1. **Memory Efficiency**: Load mesh geometry once, instantiate many times +2. **Code Clarity**: Separates template definition from instantiation +3. **Workflow Consistency**: Same pattern as clump templates +4. **Flexibility**: Easy to create and modify copies +5. **No Breaking Changes**: All existing code continues to work + +#### Testing Checklist + +##### Code Quality +- [x] Follows existing code style +- [x] Proper error handling +- [x] Comprehensive documentation +- [x] Clear method naming +- [x] Consistent with existing patterns + +##### Functionality +- [x] LoadMeshType() stores templates correctly +- [x] AddMeshFromTemplate() creates instances +- [x] Duplicate() creates deep copies +- [x] Templates stored separately from instances +- [x] Counters increment properly + +##### Integration +- [ ] Builds successfully (requires CUDA) +- [ ] Demo runs without errors +- [ ] Output files generated correctly +- [ ] Compatible with existing features +- [ ] No memory leaks + +##### Documentation +- [x] API methods documented +- [x] Usage examples provided +- [x] Comparison with clump templates +- [x] Best practices included + +#### Future Enhancements (Optional) + +While not required by the current problem statement, future improvements could include: + +1. **Batch Creation**: Similar to `AddClumps()`, allow creating multiple instances in one call +2. **Template Management**: Methods to list, remove, or query templates +3. **Template Caching**: Optimize template storage for very large meshes +4. **Transform Templates**: Methods to scale, rotate, or translate templates + +#### Conclusion + +The implementation successfully addresses all requirements from the problem statement: + +✅ Added method to load mesh templates (similar to clump templates) +✅ Templates stored separately, not in simulation +✅ User can instantiate many meshed particles at different locations +✅ One-by-one instantiation (as specified) +✅ Added method to copy loaded mesh using shared_ptr + +The implementation is minimal, follows existing patterns, and includes comprehensive documentation and testing examples. + +## Original file: PATCH_LOCATION_IMPLEMENTATION.md + +### Patch Location Implementation Summary + +#### Overview +This document describes the implementation of patch location data in the DEM-Engine mesh system. The patch location represents the relative position (to the implicit CoM of the mesh) of each patch in a mesh. + +#### Problem Statement +The requirement was to add a data member and associated methods to DEMMesh to store the relative location (XYZ) of mesh patches. This data should: +- Be a vector of length `nPatches` +- Allow explicit user setting +- Auto-compute if not explicitly set: + - Single patch: location is (0,0,0) (same as CoM) + - Multiple patches: average of all triangle centroids per patch +- Be transferred to dT at initialization +- Be stored in dT similar to `relPosNode1`, `relPosNode2`, `relPosNode3` + +#### Implementation Details + +##### 1. DEMMesh Class Changes (`src/DEM/BdrsAndObjs.h`) + +###### New Data Members: +```cpp +// Relative location (to CoM) of each patch (vector of length nPatches) +std::vector m_patch_locations; +// Whether patch locations have been explicitly set +bool patch_locations_explicitly_set = false; +``` + +###### New Methods: +```cpp +// Set patch locations explicitly +void SetPatchLocations(const std::vector& patch_locations); + +// Get patch locations +const std::vector& GetPatchLocations() const; + +// Check if patch locations were explicitly set +bool ArePatchLocationsExplicitlySet() const; + +// Compute patch locations (auto-calculation logic) +std::vector ComputePatchLocations() const; +``` + +##### 2. Automatic Patch Location Computation (`src/DEM/MeshUtils.cpp`) + +The `ComputePatchLocations()` method implements the following logic: + +###### Single Patch: +- Returns location (0,0,0) - representing the mesh's CoM + +###### Multiple Patches: +- Computes the centroid of each triangle +- Averages all triangle centroids belonging to the same patch +- Includes bounds checking to prevent out-of-bounds access + +```cpp +std::vector DEMMesh::ComputePatchLocations() const { + std::vector patch_locations(nPatches, make_float3(0, 0, 0)); + + if (nPatches == 1) { + // Single patch: location is just CoM (0,0,0) + return patch_locations; + } + + // Multiple patches: compute average of triangle centroids per patch + // ... (implementation details in MeshUtils.cpp) + + return patch_locations; +} +``` + +##### 3. dT Storage (`src/DEM/dT.h`) + +Added a new DualArray to store patch locations in dT: + +```cpp +// Relative position (to mesh CoM) of each mesh patch +DualArray relPosPatch = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); +``` + +This follows the same pattern as: +- `relPosNode1` - relative position of triangle vertex 1 +- `relPosNode2` - relative position of triangle vertex 2 +- `relPosNode3` - relative position of triangle vertex 3 + +##### 4. DEMDataDT Structure Update (`src/DEM/Defines.h`) + +Added pointer to the patch location data: + +```cpp +struct DEMDataDT { + // ... existing members ... + float3* relPosNode1; + float3* relPosNode2; + float3* relPosNode3; + float3* relPosPatch; // NEW: Patch locations + materialsOffset_t* patchMaterialOffset; + // ... +}; +``` + +##### 5. Data Transfer During Initialization (`src/DEM/dT.cpp`) + +###### Array Initialization: +```cpp +// Resize to the number of mesh patches +DEME_DUAL_ARRAY_RESIZE(ownerPatchMesh, nMeshPatches, 0); +DEME_DUAL_ARRAY_RESIZE(patchMaterialOffset, nMeshPatches, 0); +DEME_DUAL_ARRAY_RESIZE(relPosPatch, nMeshPatches, make_float3(0)); // NEW +``` + +###### Data Population: +```cpp +// Populate patch locations for this mesh +// If explicitly set, use those; otherwise compute them +std::vector this_mesh_patch_locations; +if (input_mesh_objs.at(i)->patch_locations_explicitly_set) { + this_mesh_patch_locations = input_mesh_objs.at(i)->m_patch_locations; +} else { + this_mesh_patch_locations = input_mesh_objs.at(i)->ComputePatchLocations(); +} +// Store the patch locations in the global array +for (size_t patch_idx = p_start; patch_idx < p; patch_idx++) { + relPosPatch[nExistingMeshPatches + patch_idx] = this_mesh_patch_locations[patch_idx - p_start]; +} +``` + +###### Device Binding: +```cpp +// In packDataPointers() +relPosPatch.bindDevicePointer(&(granData->relPosPatch)); + +// In data transfer +relPosPatch.toDeviceAsync(streamInfo.stream); +``` + +#### Usage Examples + +##### Example 1: Automatic Computation (Single Patch) +```cpp +auto mesh = std::make_shared(); +mesh->LoadWavefrontMesh("my_convex_mesh.obj"); +// mesh has 1 patch by default +auto locations = mesh->ComputePatchLocations(); +// locations[0] will be (0, 0, 0) +``` + +##### Example 2: Automatic Computation (Multiple Patches) +```cpp +auto mesh = std::make_shared(); +mesh->LoadWavefrontMesh("my_complex_mesh.obj"); +mesh->SplitIntoConvexPatches(30.0f); // Split into multiple patches +auto locations = mesh->ComputePatchLocations(); +// locations[i] will be the average centroid of all triangles in patch i +``` + +##### Example 3: Explicit Setting +```cpp +auto mesh = std::make_shared(); +mesh->LoadWavefrontMesh("my_mesh.obj"); +mesh->SplitIntoConvexPatches(30.0f); + +// Manually specify patch locations +std::vector my_locations = { + make_float3(0.5, 0.5, 0.0), + make_float3(-0.5, -0.5, 0.0) +}; +mesh->SetPatchLocations(my_locations); +``` + +#### Testing + +A test file `test_patch_locations.cpp` has been created to verify: +1. Single patch meshes have location (0,0,0) +2. Multi-patch meshes correctly compute average centroids +3. Explicitly set locations are correctly stored and retrieved + +#### Integration with Existing Code + +The implementation is designed to be: +- **Non-intrusive**: Does not modify existing mesh functionality +- **Backward compatible**: Default behavior remains unchanged +- **Consistent**: Follows existing patterns (similar to `relPosNode1/2/3`) +- **Efficient**: Computed only once at initialization + +#### Future Work + +While the data is now stored in dT and transferred to the device, it is not yet used in simulation kernels. Future work may include: +- Using patch locations in contact force calculations +- Using patch locations for visualization +- Using patch locations for collision detection optimization + +#### Files Modified + +1. `src/DEM/BdrsAndObjs.h` - DEMMesh class definition +2. `src/DEM/MeshUtils.cpp` - Patch location computation implementation +3. `src/DEM/dT.h` - dT data storage +4. `src/DEM/dT.cpp` - Data initialization and transfer +5. `src/DEM/Defines.h` - DEMDataDT structure + +#### Files Added + +1. `test_patch_locations.cpp` - Test file for patch location functionality +2. `PATCH_LOCATION_IMPLEMENTATION.md` - This documentation file + +## Original file: IMPLEMENTATION_SUMMARY.txt + +```text +================================================================================ +PATCH LOCATION IMPLEMENTATION - SUMMARY OF CHANGES +================================================================================ + +OBJECTIVE: +Add a data member and associated methods to DEMMesh to store the relative +location (to the implicit CoM of the mesh) of each patch in a mesh. + +================================================================================ +CHANGES MADE: +================================================================================ + +1. DEMMesh Class (src/DEM/BdrsAndObjs.h) + ---------------------------------------- + Added Data Members: + - std::vector m_patch_locations + - bool patch_locations_explicitly_set = false + + Added Methods: + - void SetPatchLocations(const std::vector& patch_locations) + - const std::vector& GetPatchLocations() const + - bool ArePatchLocationsExplicitlySet() const + - std::vector ComputePatchLocations() const + + Updated Methods: + - Clear() - now clears patch locations and flags + +2. Mesh Utilities (src/DEM/MeshUtils.cpp) + ---------------------------------------- + Implemented: + - ComputePatchLocations() method with auto-calculation logic: + * Single patch: returns (0,0,0) + * Multiple patches: computes average of triangle centroids per patch + * Includes bounds checking for safety + +3. dT Storage (src/DEM/dT.h) + -------------------------- + Added: + - DualArray relPosPatch (similar to relPosNode1/2/3) + +4. Data Transfer Structure (src/DEM/Defines.h) + -------------------------------------------- + Updated DEMDataDT struct: + - Added: float3* relPosPatch + +5. Initialization and Transfer (src/DEM/dT.cpp) + -------------------------------------------- + Modified: + - packDataPointers(): added relPosPatch.bindDevicePointer() + - Array initialization: DEME_DUAL_ARRAY_RESIZE(relPosPatch, ...) + - Data transfer: relPosPatch.toDeviceAsync() + - Mesh loading: compute/set patch locations during initialization + +6. Testing (test_patch_locations.cpp) + ----------------------------------- + Created comprehensive test file to verify: + - Single patch mesh has location (0,0,0) + - Multi-patch mesh computes correct centroids + - Explicit patch location setting works correctly + +7. Documentation (PATCH_LOCATION_IMPLEMENTATION.md) + ------------------------------------------------ + Created detailed documentation covering: + - Implementation details + - Usage examples + - Integration with existing code + - Testing approach + +================================================================================ +KEY FEATURES: +================================================================================ + +✓ Non-intrusive: Does not modify existing mesh functionality +✓ Backward compatible: Default behavior remains unchanged +✓ Consistent: Follows existing patterns (similar to relPosNode1/2/3) +✓ Efficient: Computed only once at initialization +✓ Safe: Includes bounds checking to prevent out-of-bounds access +✓ Flexible: Supports both automatic computation and explicit setting +✓ Well-tested: Includes test file for verification +✓ Well-documented: Comprehensive documentation provided + +================================================================================ +FILES MODIFIED: +================================================================================ + +1. src/DEM/BdrsAndObjs.h (+29 lines) +2. src/DEM/MeshUtils.cpp (+50 lines) +3. src/DEM/dT.h (+3 lines) +4. src/DEM/dT.cpp (+17 lines) +5. src/DEM/Defines.h (+1 line) + +FILES ADDED: +1. test_patch_locations.cpp (175 lines) - Test file +2. PATCH_LOCATION_IMPLEMENTATION.md (203 lines) - Documentation +3. IMPLEMENTATION_SUMMARY.txt (This file) + +Total changes: +478 lines across 7 files + +================================================================================ +VERIFICATION: +================================================================================ + +✓ Code review completed - 3 issues addressed +✓ Security check (codeql) passed - no vulnerabilities detected +✓ All changes follow existing code patterns +✓ Bounds checking implemented +✓ Variable naming improved +✓ Test file created for validation + +================================================================================ +USAGE EXAMPLE: +================================================================================ + +// Automatic computation (default) +auto mesh = std::make_shared(); +mesh->LoadWavefrontMesh("my_mesh.obj"); +mesh->SplitIntoConvexPatches(30.0f); +// Patch locations will be auto-computed at initialization + +// Explicit setting (optional) +std::vector my_locations = { + make_float3(0.5, 0.5, 0.0), + make_float3(-0.5, -0.5, 0.0) +}; +mesh->SetPatchLocations(my_locations); + +================================================================================ +FUTURE WORK: +================================================================================ + +The patch location data is now stored in dT and ready for use. Future +enhancements may include: +- Using patch locations in contact force calculations +- Using patch locations for visualization +- Using patch locations for collision detection optimization + +================================================================================ +END OF SUMMARY +================================================================================ +``` diff --git a/docs/concepts.rst b/docs/concepts.rst new file mode 100644 index 00000000..306ed0d9 --- /dev/null +++ b/docs/concepts.rst @@ -0,0 +1,61 @@ +Core concepts +============= + +For more information about DEME's design concepts, see the +`DEM-Engine paper in Computer Physics Communications (2024) +`_. This page summarizes the concepts +used by the current API; the paper provides the background on the solver's +design and computational approach. + +Setup and runtime +----------------- + +DEME caches materials, templates, geometry, family prescriptions, output +choices, and force-model inputs before ``DEMSolver::Initialize``. Initialization +allocates device state and prepares runtime-compiled kernels. Avoid rebuilding +setup data in high-frequency dynamics calls. + +Dynamic and kinematic workers +----------------------------- + +DEME uses two cooperating workers: + +``dT`` + Integrates body state and evaluates contact forces. + +``kT`` + Performs collision detection and supplies potential contact pairs. + +Their asynchronous handoff is part of the solver's performance model. User code +should use the public solver API rather than directly synchronizing worker data. + +Owners and members +------------------ + +An owner represents a simulated body. Members are the collision geometries +belonging to that owner. Clumps, analytical objects, meshes, and combined owners +have different setup and tracking behavior. A combined owner preserves rigid +relative motion among its members. + +Families and prescriptions +-------------------------- + +Families group owners for contact rules and prescribed motion. Prescriptions +can constrain position, velocity, orientation, or acceleration. Their order +relative to integration and user-added acceleration matters. + +Frames and quaternions +---------------------- + +User-facing positions and linear velocities are generally in the global frame. +Some angular quantities are stored in the owner's local principal frame. +Quaternion values use ``(x, y, z, w)`` ordering. Check each API description +before combining local and global quantities. + +Runtime CUDA compilation +------------------------ + +DEME uses NVRTC and Jitify to compile kernels and user force models at runtime. +An installed package therefore needs compatible CUDA headers, NVRTC, packaged +kernel sources, and a supported NVIDIA driver—not merely a loadable Python +extension. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..a83951b8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,40 @@ +"""Sphinx configuration for the DEM-Engine documentation.""" + +from pathlib import Path +import sys + + +DOCS_DIR = Path(__file__).resolve().parent +REPOSITORY_DIR = DOCS_DIR.parent +DEME_RELEASE = (REPOSITORY_DIR / "VERSION").read_text(encoding="utf-8").strip() + +project = "DEM-Engine" +author = "DEM-Engine contributors" +copyright = "2021–2026, DEM-Engine contributors" +version = DEME_RELEASE.split(".", maxsplit=1)[0] +release = DEME_RELEASE + +sys.path.insert(0, str(DOCS_DIR / "_ext")) + +extensions = [ + "repository_links", + "breathe", + "sphinx.ext.autosectionlabel", +] + +breathe_projects = { + "DEME": str(DOCS_DIR / "_build" / "doxygen" / "xml"), +} +breathe_default_project = "DEME" +breathe_default_members = ("members", "undoc-members") + +autosectionlabel_prefix_document = True +# Breathe sees implementation types that are deliberately outside the public +# Doxygen input set. Keep authored directive and document warnings fatal while +# allowing those signature types to render as plain identifiers. +nitpicky = False + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +html_theme = "furo" +html_title = f"DEM-Engine {version}" diff --git a/docs/cpp-api/index.rst b/docs/cpp-api/index.rst new file mode 100644 index 00000000..a1386177 --- /dev/null +++ b/docs/cpp-api/index.rst @@ -0,0 +1,72 @@ +C++ API reference +================= + +The reference below is generated from the documented public C++ headers. Worker +internals and CUDA implementation headers are intentionally excluded. + +Solver +------ + +.. doxygenclass:: deme::DEMSolver + :project: DEME + :members: + :protected-members: + +Tracking and inspection +----------------------- + +.. doxygenclass:: deme::DEMTracker + :project: DEME + :members: + +.. doxygenclass:: deme::DEMInspector + :project: DEME + :members: + +Materials and force models +-------------------------- + +.. doxygenclass:: deme::DEMMaterial + :project: DEME + :members: + +.. doxygenclass:: deme::DEMForceModel + :project: DEME + :members: + +Geometry and initialization +--------------------------- + +.. doxygenclass:: deme::DEMInitializer + :project: DEME + :members: + +.. doxygenclass:: deme::DEMClumpBatch + :project: DEME + :members: + +.. doxygenclass:: deme::DEMExternObj + :project: DEME + :members: + +.. doxygenclass:: deme::DEMMesh + :project: DEME + :members: + +``deme::DEMMeshConnected`` is a compatibility type alias for +``deme::DEMMesh`` and therefore does not have a separate Doxygen class page. + +Sampling +-------- + +.. doxygenclass:: deme::PDSampler + :project: DEME + :members: + +.. doxygenclass:: deme::GridSampler + :project: DEME + :members: + +.. doxygenclass:: deme::HCPSampler + :project: DEME + :members: diff --git a/docs/deme3-new-features.rst b/docs/deme3-new-features.rst new file mode 100644 index 00000000..8669a8ab --- /dev/null +++ b/docs/deme3-new-features.rst @@ -0,0 +1,211 @@ +DEME 3 features and migration considerations +============================================ + +DEME 3 adds mesh-particle contacts, rigid combined bodies, direct GPU data +exchange, persistent compiled-kernel caching, and interactive visualization. +The sections below explain when these capabilities are useful and what to +consider when moving an existing DEME 2 application. + +Mesh particles and aggregated mesh contacts +------------------------------------------- + +DEME 3 supports mesh–mesh contacts, allowing triangle meshes to represent +interacting grains as well as external bodies. Enable mesh contacts with other +meshes and analytical geometry using ``SetMeshUniversalContact(true)`` before +initialization. Its default is false; see `Mesh particles and combined owners `__ for setup. + +The clump–mesh contact scheme also changes. Rather than evaluating each +sphere–triangle contribution as an independent force interaction, the mesh +contact pipeline can combine triangle contributions into patch/island contacts +and evaluate the force model using the resulting contact geometry. The patch +force kernels consume aggregated area, normal, penetration, and contact-point +data. This provides a combined treatment of a mesh's effect on a clump instead +of treating every contacted triangle independently. + +The sphere side still identifies an individual component sphere. A clump with +multiple spheres, or a mesh with distinct patches/contact islands, can therefore +have multiple resulting contacts. Aggregation does not mean exactly one contact +per clump–mesh pair, and primitive geometry checks are still needed. + +``SetSimplePatchCombination`` selects grouping by patch-ID pair; the more +involved contact-island handling distinguishes contact regions. +``SetStablePatchIslandIDs`` controls stabilizing flooded island identities +across detection steps. Consult `C++ API reference `__ for these controls rather +than assuming all neighboring triangles are always merged into one contact. + +This changes the contact representation used by force models and contact +history. Revalidate representative forces, torques, and bulk behavior when +porting a DEME 2 simulation, especially if its custom model relied on separate +triangle contacts. See `Internal type codes for debugging `__ when inspecting primitive +and patch contact arrays. + +Using DEME 2-style mesh patches within DEME 3 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Call ``SetDEME2MeshBehavior(true)`` **before loading meshes or mesh templates** +to assign every triangle of subsequently loaded meshes to its own patch. This +is useful when comparing an existing triangle-based contact setup with DEME 3's +normal patch assignments. With a solver, material, and mesh file already +available, use this setup fragment: + +.. code-block:: cpp + + solver.SetDEME2MeshBehavior(true); + auto mesh_type = solver.LoadMeshType("surface.obj", material); + auto mesh = solver.AddMeshFromTemplate(mesh_type); + +The Python equivalent is: + +.. code-block:: python + + solver.SetDEME2MeshBehavior(True) + mesh_type = solver.LoadMeshType("surface.obj", material) + mesh = solver.AddMeshFromTemplate(mesh_type) + +The default is false. Calling ``SetDEME2MeshBehavior(false)`` leaves patch +assignments unchanged for subsequent loads; it does not merge triangles back +together or undo changes on already loaded meshes/templates. In particular, +set the option before ``LoadMeshType`` when using templates. + +This controls mesh patch assignment only. It neither switches to the DEME 2 +solver nor restores its memory footprint or GPU backends, and it does not +replace ``SetMeshUniversalContact``. Validate forces and motion when using it +for migration comparisons. + +Rigid combined bodies and geometry-wildcard migration +----------------------------------------------------- + +Combined bodies provide the replacement workflow for constructions that used +geometry wildcards to distinguish parts of a body. Define members as clump or +mesh templates, assign their properties at the member-owner level, and group +them using ``LoadCombinedClumpType`` or ``LoadCombinedMeshType`` followed by +``AddCombinedFromTemplate``. Member-relative positions and orientations remain +fixed as the group moves. Owner wildcards can carry member-specific custom +properties; contact wildcards remain available for interaction history. + +Geometry-wildcard-based constructions were difficult to manage because force +interactions were resolved for individual geometry pairs. Two bodies with +``n`` and ``m`` primitives can expose up to ``n * m`` candidate pairs (quadratic +when both have ``n`` primitives), with the realized contacts depending on +geometry and collision filtering. This can produce far more interactions and +per-contact state than an application intended. Wildcards themselves do not +create geometric overlap; assigning data to each geometry does not turn those +geometries into one aggregate interaction either. + +Combined bodies make the ownership and rigid grouping explicit. Forces and +moments from the members contribute to the group's motion, and contacts among +members of the same group are suppressed by default. The +``SetAllowIntraCombinedOwnerContacts`` control can change that policy. Grouping +does not eliminate external primitive-pair detection or guarantee one contact +per combined-body pair. Patch aggregation and rigid grouping solve different +parts of the problem. + +Migration is a modeling change, not a mechanical rename of geometry wildcards: +choose member boundaries and move custom properties to the appropriate owner or +contact variables. Combined bodies are rigid assemblies, not joints or +structural deformation models. See `Mesh particles and combined owners `__ and `Core concepts `__. + +On-device coupling +------------------ + +DEME 3 can transfer state and contact-force data directly into CUDA device +buffers for consumption by other GPU packages. Device-input APIs also support +updating simulation state from GPU buffers. This avoids a CPU round trip for +the exchanged data and enables coupling with GPU-based dynamics or continuum +solvers. Host-based access remains available. + +Direct device exchange is not a promise of zero-copy access or asynchronous +execution: APIs can copy data and synchronize. Follow the pointer, capacity, +device-selection, and lifetime requirements in `Retrieving simulation data `__ and +the C++ reference. The Python guide includes CuPy and Warp examples. + +Persistent compiled-kernel caching +---------------------------------- + +The runtime compiler stores serialized kernel instantiations on disk and reuses +compatible entries across processes. Repeated runs can therefore skip kernel +compilation and reduce startup time. A cold cache or changed specialization +still requires compilation; this feature does not increase the speed of the +simulation timesteps themselves. + +Cache keys include generated source, compilation flags/options, kernel and +template specialization, API version, CUDA/NVRTC versions, and GPU architecture. +A matching cache entry is loaded; a missing or unreadable serialized entry is +compiled again. Source substitutions or a different toolchain/device can require +new entries. Keep the cache in a persistent writable directory, for example: + +.. code-block:: console + + mkdir -p "$HOME/.cache/deme/kernels" + export DEME_JIT_CACHE_DIR="$HOME/.cache/deme/kernels" + +Without this override, DEME selects ``jit_cache`` under its runtime data path +when that path exists, otherwise ``dem-jit`` in the temporary directory. See +``src/core/utils/JitHelper.cpp`` for cache selection and key construction. + +This is separate from ``DEME_PERSISTENT_JITIFY_CACHE``, the opt-in cache of +CUDA header sources used during Jitify discovery. That header cache has its own +path and invalidation considerations; see `Troubleshooting `__. Clear affected +caches after changing included headers if their contents are not otherwise +reflected in the cache key. + +Visualization and Python workflows +---------------------------------- + +The interactive visualizer displays the current simulation state when asked to +render, making it useful for inspecting geometry and motion during a run. See +`Interactive visualization `__ for examples and display requirements. + +Python exposes the new mesh, combined-body, visualization, and device-data +capabilities. Start with `Python quickstart `__, then consult +`Python API overview `__ and the Python demos under ``python/demos/``. Python +bindings and customizable contact physics also existed in DEME 2; their presence +alone is not a DEME 3 addition. + +When to stay with DEME 2 +------------------------ + +DEME 3 currently requires NVIDIA GPUs and CUDA; it does not yet provide a +non-NVIDIA GPU backend. If you need the AMD HIP/ROCm source-build path available +in DEME 2, stay with that version. This does not imply AMD support in the +published Python wheels; follow the selected version's backend requirements. + +DEME 3 may also use more memory. Its mesh-contact pipeline carries primitive +and patch contact data, aggregation intermediates, and history/identity state. +The overhead depends on the scene and settings; there is no fixed memory ratio +between versions. Turning off mesh–mesh contacts does not necessarily remove +all of that overhead or restore DEME 2's clump–mesh force treatment. + +If you do not need mesh–mesh contact or the new aggregated clump–mesh scheme, +DEME 2 may remain a suitable choice, particularly for a memory-constrained or +already validated application. Compare representative workloads before moving +an established pipeline, rather than assuming identical forces or memory use. + +Use the final DEME 2 release: 2.4.2 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For C++, use the pinned +`v2.4.2 source tree `_ +from the upstream repository. A separate checkout preserves an existing DEME 3 +working tree: + +.. code-block:: console + + git clone --branch v2.4.2 --recurse-submodules https://github.com/projectchrono/DEM-Engine.git DEM-Engine-2.4.2 + cd DEM-Engine-2.4.2 + +Cloning a tag creates a detached checkout, which is fine for building. Follow +the README in that checkout for the appropriate CUDA or HIP build instructions. +Do not track the moving ``main`` branch if you intend to remain on 2.4.2. + +For pyDEME, the Python distribution name is ``deme``. Explicitly install the +2.4.2 version, preferably in a separate environment from DEME 3: + +.. code-block:: console + + python -m pip install "deme==2.4.2" + +Keep ``deme==2.4.2`` in your application's requirements file to retain the pin. +Use 2.4.2's Python/platform/CUDA requirements and examples, rather than assuming +DEME 3's ``cuda12`` extra or new APIs are available in DEME 2. An unpinned install +or upgrade can select a newer major version. diff --git a/docs/developer/architecture.rst b/docs/developer/architecture.rst new file mode 100644 index 00000000..16b5f5c0 --- /dev/null +++ b/docs/developer/architecture.rst @@ -0,0 +1,46 @@ +Developer architecture +====================== + +Public API boundary +------------------- + +Public operations enter through ``DEMSolver`` and related setup objects. New +simulation state normally follows this path: + +#. public API or cached initializer data; +#. dynamic and kinematic worker storage; +#. device-data pointer binding; +#. host-to-device transfer; +#. JIT substitutions or kernel arguments; +#. runtime update behavior. + +Missing one stage can produce failures only after initialization or only on a +second GPU. + +CUDA and JIT kernels +-------------------- + +Kernel launch argument types must match exactly. Validate and explicitly narrow +host values at the launch boundary. Treat block size as correctness-sensitive: +large JIT kernels can exceed register or launch limits even when a smaller +kernel accepts the same configuration. + +Synchronization +--------------- + +The dT/kT streams, events, and handoff buffers implement the intended +synchronization. Avoid adding device-wide synchronization to hot paths. When +cross-device data is transferred, preserve the lifetime of the source storage +until the destination operation has completed. + +Adding Python bindings +---------------------- + +When exposing a public API: + +#. bind the method in ``src/DEM/python/bindings.cpp``; +#. use explicit overload casts; +#. provide parameter names and a practical docstring; +#. document units, frame conventions, and object lifetime; +#. add an installed-wheel test, not only an in-tree import test; +#. update the Python guide when behavior differs from C++. diff --git a/docs/developer/documentation.rst b/docs/developer/documentation.rst new file mode 100644 index 00000000..b03e00a9 --- /dev/null +++ b/docs/developer/documentation.rst @@ -0,0 +1,84 @@ +Building the documentation +========================== + +The concise build, Python-reference regeneration, and local/remote preview +commands are collected in `docs/README.md <../README.md>`__. + +Prerequisites +------------- + +Install Doxygen through the operating system or Conda, then create an isolated +Python environment: + +.. code-block:: console + + python3 -m venv .venv-docs + . .venv-docs/bin/activate + python -m pip install -r docs/requirements.txt + +Standalone build +---------------- + +.. code-block:: console + + make -C docs html + +Open ``docs/_build/html/index.html`` in a browser. + +Regenerating the Python API reference +------------------------------------- + +After building and installing the current ``deme`` extension into the active +Python environment, regenerate the static reference from pybind11 signatures +and docstrings: + +.. code-block:: console + + make -C docs python-reference + make -C docs html + +The generated ``docs/python/reference.rst`` is committed so normal +documentation builds do not import the native extension or require a GPU. +Improve descriptions in ``src/DEM/python/bindings.cpp`` and regenerate rather +than editing the reference page directly. + +CMake target +------------ + +When configuring the complete project: + +.. code-block:: console + + cmake -S . -B build-docs -DDEME_BUILD_DOCS=ON + cmake --build build-docs --target docs + +Documentation policy +-------------------- + +* Doxygen parses the public C++ headers and emits XML. +* Sphinx and Breathe render the C++ API alongside authored guides. +* Worker internals and CUDA kernels belong in architecture pages rather than + the public API reference. +* New public functions should describe intent, parameters, units, frames, + setup/runtime restrictions, and return values. +* The HTML build uses warnings-as-errors so broken API references do not + silently enter published documentation. + +See `Hosting the documentation `__ for publication options and source-tree organization, and +`Python wheel maintenance `__ for Python wheel maintenance. + +Links in authored guides +------------------------ + +Use standard reStructuredText relative hyperlinks to source files, with a +readable label and the actual extension (for example, ``.rst``). These work +when readers browse the repository on GitHub. Prefer anonymous hyperlinks +(two trailing underscores) so repeated labels do not create duplicate targets. +Sphinx-specific document and download roles are not understood by GitHub. + +The local ``repository_links`` extension converts links to ``.rst`` pages into +Sphinx document references and links to ``.md``, ``.cpp``, and ``.py`` files into +downloads during the site build. Keep these relative file links free of queries +and fragments; the adapter leaves external URLs and same-page anchors alone. +The generated API pages and toctrees still require the Sphinx build for their +full rendering. diff --git a/docs/developer/hosting.rst b/docs/developer/hosting.rst new file mode 100644 index 00000000..4b953d24 --- /dev/null +++ b/docs/developer/hosting.rst @@ -0,0 +1,127 @@ +Hosting the documentation +========================= + +Recommended: GitHub Pages +------------------------- + +GitHub Pages fits this repository's existing static documentation build. +One site contains the authored guides, the Doxygen/Breathe C++ reference, and +the committed Python API reference. A normal HTML build does not import the +native extension, compile the solver, or need a GPU. See +`Building the documentation `__ for the build and reference-regeneration steps. + +Pages is free for public repositories on GitHub Free. Published sites have a +1 GB size limit and a soft bandwidth limit of 100 GB/month. These should suit +API documentation; keep large simulation videos and datasets elsewhere. +Check `GitHub's current Pages limits +`_ +before choosing it for a larger site. + +Publish with the included workflow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``.github/workflows/docs.yml`` builds both API references and publishes the +combined site. The repository variable ``DOCS_PUBLISH_BRANCH`` selects the +publishing branch. Pushes to other branches skip the build and deployment; +relevant pull requests build an HTML artifact without deploying. An unset +variable disables publication. + +One-time GitHub setup +^^^^^^^^^^^^^^^^^^^^^ + +#. Open **Settings → Secrets and variables → Actions → Variables** and create + a repository variable named ``DOCS_PUBLISH_BRANCH`` with value + ``DEME3_docs``. Use a repository variable, not an environment variable or + secret, because the build job checks it before entering an environment. +#. In **Settings → Pages → Build and deployment → Source**, select + **GitHub Actions**. +#. In **Settings → Environments**, create or open ``github-pages``. Under + deployment branches and tags, allow the branch ``DEME3_docs``. If required + reviewers are configured, each deployment waits for their approval; omit + required reviewers if publication should be fully automatic. +#. Push the commit containing the workflow and documentation to ``DEME3_docs``. + Every subsequent push to that branch builds and publishes the site. +#. Open **Actions → Build and publish documentation** and inspect the build + and deploy jobs. The deployment links to the live site, normally + ``https://ruochun.github.io/DEM-Engine/`` for this repository. + +If the branch was already pushed before setup, use **Re-run all jobs** on its +workflow run after configuring Pages and the variable, or push a new commit. +Keep generated HTML out of Git; the workflow uploads ``docs/_build/html`` as +an artifact and publishes it only after a successful build. + +The workflow also supports **Run workflow**. GitHub requires the workflow file +to exist on the repository's default branch before manual dispatch is available. +Until then, use the push trigger on ``DEME3_docs``; changing the repository's +default branch is unnecessary. Once manual dispatch is available, select the +branch matching ``DOCS_PUBLISH_BRANCH`` in the branch dropdown. + +Switch publication to main later +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. Merge the documentation and workflow into ``main``. +#. Allow ``main`` in the ``github-pages`` environment's deployment branch rules. +#. Change the repository variable ``DOCS_PUBLISH_BRANCH`` to ``main``. +#. Push a new commit to ``main``, or manually run the workflow on ``main``. + Changing the variable alone does not trigger a build. +#. Remove ``DEME3_docs`` from the environment's allowed deployment branches + when it is no longer used for publication. + +No workflow edit is needed. The selected branch must contain the workflow and +its documentation sources. Builds check out the triggering commit, so a push +publishes documentation corresponding to that commit rather than a moving +branch head. The Python API reference must still be regenerated and committed +when bindings change; see `Building the documentation `__. + +See GitHub's official instructions for +`Pages publishing sources +`_, +`custom Pages workflows +`_, and +`manual workflow runs +`_. + +Already-compiled documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To publish an existing local build, copy the contents of ``docs/_build/html/`` +to a dedicated publishing branch or a separate documentation repository. Add +an empty ``.nojekyll`` file alongside ``index.html`` and configure Pages to +serve that branch's root. Keep generated files out of the development branch. +A workflow build is easier to reproduce and keep synchronized with releases. + +The existing Doxyfile emits XML for Breathe. If you also want Doxygen's own +standalone HTML layout, enable ``GENERATE_HTML`` in a separate Doxygen +configuration and place its output in a subdirectory of the published site. +The current combined Sphinx site already includes the C++ reference, so this +extra output is optional. + +Alternative: Read the Docs Community +------------------------------------ + +`Read the Docs Community `_ +hosts open-source documentation at no cost. It is a good alternative when +built-in documentation versions and pull-request previews are priorities. +`Community hosting `_ is advertising-supported. + +Use ``docs/conf.py`` and ``docs/requirements.txt`` in a ``.readthedocs.yaml`` +configuration. Install Doxygen in the build environment and run +``make -C docs doxygen`` before Sphinx so Breathe has its XML input. Keep the +committed Python reference for hosted builds; regenerate it on a suitable +native-extension build host when bindings change. See the official +`Sphinx setup guide `_. + +Maintaining the source tree +--------------------------- + +Keep the repository README short and link to maintained pages under ``docs/``. +Use reStructuredText for pages that should appear in the compiled site, and +add each page to a toctree. Markdown is reserved for repository-facing indexes, +credits, and historical notes; it is not parsed by the current Sphinx setup. +The root ``LICENSE.md`` and ``AGENTS.md`` remain in place for license discovery +and agent tooling. + +Earlier implementation reports are preserved in +`the historical notes <../archive/implementation-notes.md>`__. +They are not current API or installation instructions; update the maintained +guides and references when behavior changes instead of adding another report. diff --git a/docs/developer/packaging.rst b/docs/developer/packaging.rst new file mode 100644 index 00000000..0332935a --- /dev/null +++ b/docs/developer/packaging.rst @@ -0,0 +1,219 @@ +Python wheel maintenance +======================== + +For prerequisites, a single wheel build, and runtime validation, see +`Installation <../installation.rst>`__. The commands below run from the repository root unless +a step explicitly changes directory. + +Build the complete supported Python matrix +------------------------------------------ + +One native wheel must be built by each targeted CPython interpreter. Building +with Python 3.13, for example, creates only the ``cp313`` wheel; it does not +also create wheels for the other supported Python versions. DEM-Engine 3 +currently targets CPython 3.9 through 3.14. + +The following reproducible Conda workflow creates a separate build environment +for every target. Run the environment-creation commands from the repository +root: + +.. code-block:: console + + conda create --yes --name deme-wheel-py39 python=3.9 pip + conda create --yes --name deme-wheel-py310 python=3.10 pip + conda create --yes --name deme-wheel-py311 python=3.11 pip + conda create --yes --name deme-wheel-py312 python=3.12 pip + conda create --yes --name deme-wheel-py313 python=3.13 pip + conda create --yes --name deme-wheel-py314 python=3.14 pip + + for env in deme-wheel-py39 deme-wheel-py310 deme-wheel-py311 deme-wheel-py312 deme-wheel-py313 deme-wheel-py314; do + conda run --name "$env" python -m pip install --upgrade pip build twine auditwheel + done + +Build once with each interpreter. As in the single-version workflow, run the +builder from the checkout's parent directory to prevent the local ``build/`` +directory from shadowing the PyPA ``build`` package: + +.. code-block:: console + + cd .. + for env in deme-wheel-py39 deme-wheel-py310 deme-wheel-py311 deme-wheel-py312 deme-wheel-py313 deme-wheel-py314; do + conda run --name "$env" python -m build --wheel --outdir DEM-Engine/dist DEM-Engine + done + cd DEM-Engine + + conda run --name deme-wheel-py313 python -m twine check dist/*.whl + +The resulting directory should contain six distinct wheels with ``cp39``, +``cp310``, ``cp311``, ``cp312``, ``cp313``, and ``cp314`` tags. +Confirm that explicitly: + +.. parsed-literal:: + + ls -1 dist/deme-|release|-cp3\*-linux\_\*.whl + for wheel in dist/\*.whl; do + conda run --name deme-wheel-py313 python -m auditwheel show "$wheel" + done + +Generating all six files is only the build step. Each wheel must still be +installed and exercised with its matching Python version before that version +is considered validated. CUDA, Linux ABI, and GPU compatibility also require +separate testing; ``auditwheel show`` reports the native shared-library and +``glibc`` requirements but does not prove runtime compatibility. + +Build release wheels with cibuildwheel +-------------------------------------- + +The Conda commands above are useful for native development builds. Release +wheels use ``cibuildwheel`` and PyPA's CUDA-enabled +``manylinux_2_28_x86_64_cuda12_9`` container so the result does not inherit the +Linux ABI of the maintainer's workstation. The configuration is stored in +``pyproject.toml``. + +With Docker available, build the same complete matrix locally from the parent +of the checkout: + +.. parsed-literal:: + + python3 -m venv .venv-cibuildwheel + source .venv-cibuildwheel/bin/activate + python -m pip install --upgrade pip + python -m pip install "cibuildwheel==4.1.1" twine auditwheel + + cd .. + python -m cibuildwheel --platform linux --output-dir DEM-Engine/wheelhouse DEM-Engine + cd DEM-Engine + + python -m twine check wheelhouse/\*.whl + for wheel in wheelhouse/\*.whl; do + python -m auditwheel show "$wheel" + done + +This release process uses ``auditwheel repair`` to copy ordinary redistributable +native dependencies into each wheel and assign the +``manylinux_2_28_x86_64`` tag. It explicitly excludes ``libcuda.so.1``, +``libcudart.so.12``, and ``libnvrtc.so.12``. DEME runtime-compiles CUDA kernels, +so NVRTC, its builtins, and headers must be supplied by the ``cuda12`` extra +or a compatible system CUDA 12.9 toolkit. The NVIDIA driver remains a host +requirement. Bundling a driver +stub is incorrect, while bundling NVRTC without all of its dynamically loaded +resources produces an incomplete runtime. Before publishing, inspect the +repaired wheel and ``auditwheel show`` output to confirm that CUDA is the only +non-system external dependency. + +Automated wheel builds +---------------------- + +``.github/workflows/python-wheels.yml`` runs the same policy as six parallel +jobs, one for each CPython ABI. It runs on relevant pull requests, release tags, +or manual dispatch. Every job: + +* checks out Git submodules recursively; +* builds in the CUDA 12.9 manylinux 2.28 container; +* repairs the wheel with ``auditwheel``; +* checks package metadata and the expected Python/platform filename tags; +* installs the ``cuda12`` extra in a clean Python container and compiles CUDA, + CURAND, and CCCL headers with pip-provided NVRTC, without a system toolkit; and +* uploads the wheel as a workflow artifact for testing or release assembly. + +The hosted build runners do not provide a usable NVIDIA GPU. Consequently this +workflow validates compilation, repair, metadata, and tags but deliberately +does not claim GPU runtime validation. Install each artifact on a compatible +GPU host and run the tests below before publishing it. + +Publish the deme distribution +----------------------------- + +Publishing changes external package state and is only for authorized +maintainers. ``python-wheels.yml`` uses PyPI Trusted Publishing, so it does not +store a long-lived PyPI token in GitHub. + +Before the first upload, create a GitHub environment named ``pypi`` under +``Settings`` then ``Environments``. Configure required reviewers so that the +publication job always pauses for approval. Then sign in to PyPI, open the +account-level ``Publishing`` page, and add a pending GitHub publisher with: + +* PyPI project name: ``deme``; +* GitHub owner: ``Ruochun``; +* repository: ``DEM-Engine``; +* workflow filename: ``python-wheels.yml``; and +* environment: ``pypi``. + +The pending publisher creates ``deme`` on the first successful upload. It +does not reserve the name before that upload. The project name must exactly +match ``name = "deme"`` in ``pyproject.toml``. + +To build without publishing, open the repository's ``Actions`` tab, select +``Build Python wheels``, choose ``Run workflow``, leave +``publish_to_pypi`` disabled, and run it from the intended commit or branch. +Download and test all six artifacts after the jobs succeed. + +To publish the already-reviewed source commit, dispatch the same workflow +again with ``publish_to_pypi`` enabled. The six build jobs run again; only if +all succeed does the ``Publish deme wheels to PyPI`` job enter the protected +``pypi`` environment. Approve that deployment after checking the commit and +wheel jobs. The publishing job downloads the six artifacts and uploads them +with a short-lived PyPI OIDC credential. + +PyPI does not allow replacing a file or reusing an existing release version. +If any ``deme`` version |release| file has already been uploaded, increment the +project version and rebuild the complete wheel set rather than retrying with +different bytes under the same version. + +Wheel portability +----------------- + +Before distributing a wheel, record and test at least: + +* the Python and ABI tag in the wheel filename; +* the Linux distribution and minimum compatible ``glibc`` baseline; +* the CUDA Toolkit used for compilation; +* the minimum NVIDIA driver version; +* the GPU architectures included by the CUDA build; and +* imported shared-library dependencies. + +Until the supported compatibility matrix is published, build and validate +wheels on the oldest intended deployment platform and test them on each +supported Python, CUDA/driver, and GPU configuration. + +Release distribution +-------------------- + +The wheel workflow publishes the ``deme`` distribution through the ``pypi`` +environment on ``main`` and ``Mesh_Particles``. Publication remains an explicit +manual dispatch. + +Python-only CUDA configuration +------------------------------ + +The ``cuda12`` extra supplies NVIDIA component wheels. Before importing the +extension, ``deme._cuda`` loads the required libraries by absolute path and +returns their header directories. A private binding configures these directories +in the Python core variant (``core_python``), before any solver workers start. +The native ``core`` target does not compile that configuration hook and retains +its original CUDA header search order. No CUDA environment variable is set, so +executables launched from Python retain their normal native configuration. + +PyPI project description +------------------------ + +``pyproject.toml`` sets ``readme = "README.md"``. Each wheel embeds that README +as its Markdown description, so publishing a new DEME 3 release built from this +source also publishes the DEME 3 front page. Editing GitHub's README alone does +not update metadata in an already uploaded release; rebuild and publish a new +version through the release workflow. Older version pages retain their release +metadata. + +Before building wheels, ``python-wheels.yml`` runs +``docs/prepare_pypi_readme.py`` in its disposable checkout. The script changes +relative README links into absolute GitHub URLs pinned to the build commit. +This keeps the installation guides and demo links usable on PyPI while leaving +the repository README's relative links and remote cover images intact. No +separate PyPI README needs to be maintained. The workflow checks that each +wheel's description matches the prepared README and runs ``twine check``. + +For a release built outside this workflow, run the same preparation script in +a disposable source checkout, supplying its repository URL and revision, before +building. The script rewrites that checkout's README in place. Inspect the wheel +metadata and rendered description before publication; the documentation-site +workflow does not update PyPI. diff --git a/docs/developer/type-codes.rst b/docs/developer/type-codes.rst new file mode 100644 index 00000000..6546428b --- /dev/null +++ b/docs/developer/type-codes.rst @@ -0,0 +1,131 @@ +Internal type codes for debugging +================================= + +Use these tables when inspecting contact arrays, owner state, or debugger +output. The source of truth is ``src/DEM/Defines.h``; storage types are defined +in ``src/DEM/VariableTypes.h``. Values describe this checkout and should not be +assumed identical in older releases. Prefer symbolic constants in application +code. + +Contact types +------------- + +``contact_t`` is an unsigned 8-bit integer. These codes appear in +``contactTypePrimitive`` and ``contactTypePatch``. They describe the geometry +pair, not the owner IDs or the position of a contact in an array. + +.. csv-table:: + :header: "Decimal", "Hex", "Internal name", "Geometry A", "Geometry B" + :widths: 8 8 44 20 20 + + 0, 0x00, ``NOT_A_CONTACT``, None, None + 17, 0x11, ``SPHERE_SPHERE_CONTACT``, Sphere, Sphere + 18, 0x12, ``SPHERE_TRIANGLE_CONTACT``, Sphere, Triangle + 20, 0x14, ``SPHERE_ANALYTICAL_CONTACT``, Sphere, Analytical component + 34, 0x22, ``TRIANGLE_TRIANGLE_CONTACT``, Triangle, Triangle + 36, 0x24, ``TRIANGLE_ANALYTICAL_CONTACT``, Triangle, Analytical component + +``SPHERE_MESH_CONTACT`` is a legacy alias for ``SPHERE_TRIANGLE_CONTACT`` +(value 18), not an additional contact type. ``NUM_SUPPORTED_CONTACT_TYPES`` +is 5: it is a count, not the maximum code. ``ALL_CONTACT_TYPES`` lists the five +supported pairs; ``isSupportedContactType`` validates a code. + +Encoding and decoding +~~~~~~~~~~~~~~~~~~~~~ + +The high four bits hold geometry A's type, and the low four bits hold geometry +B's type: + +.. code-block:: text + + contact_code = (geometry_A << 4) | geometry_B + geometry_A = contact_code >> 4 + geometry_B = contact_code & 0x0F + +For example, 36 is ``0x24``: triangle (2) against analytical component (4). +Order matters: ``0x21`` is not a supported substitute for ``0x12``. Packing two +geometry codes does not by itself make the resulting pair supported. + +The C++ helpers are ``encodeType``, ``decodeTypeA``, and ``decodeTypeB``. Their +packing width depends on the template argument types. When decoding an +integer from a log, cast it to ``deme::contact_t`` first so the helpers use +four-bit fields rather than fields sized for ``int``: + +.. code-block:: cpp + + const auto type = static_cast(36); + const auto type_a = deme::decodeTypeA(type); // GEO_T_TRIANGLE + const auto type_b = deme::decodeTypeB(type); // GEO_T_ANALYTICAL + +Geometry and owner codes +------------------------ + +``geoType_t`` and ``ownerType_t`` are unsigned 8-bit integers. A geometry is a +collision component belonging to an owner; a clump can contain many spheres, +and a mesh can contain many triangles. + +.. csv-table:: + :header: "Value", "Geometry code", "Internal owner code", "Meaning" + :widths: 8 30 32 30 + + 0, ``NOT_A_GEO``, ``NOT_A_OWNER``, No geometry / no owner type + 1, ``GEO_T_SPHERE``, ``OWNER_T_CLUMP``, Sphere component / clump owner + 2, ``GEO_T_TRIANGLE``, ``OWNER_T_MESH``, Triangle component / mesh owner + 4, ``GEO_T_ANALYTICAL``, ``OWNER_T_ANALYTICAL``, Analytical component / analytical owner + +The analytical code is 4, not 3; these values also participate in bitwise +operations. ``ownerTypes`` stores the internal owner codes. Combined owners +have no separate ``OWNER_T_COMBINED`` code: rigid grouping does not introduce +a fourth geometry or owner type. See `Core concepts <../concepts.rst>`__ for owners and members. + +Do not confuse these codes with ``OWNER_TYPE`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The separate C++ ``enum class OWNER_TYPE``, also exposed in Python, has the +following underlying values. It is not numerically interchangeable with the +internal ``OWNER_T_*`` constants: + +.. csv-table:: + :header: "Enum", "Enum value", "Corresponding internal code", "Internal value" + :widths: 35 10 40 15 + + ``OWNER_TYPE::CLUMP``, 0, ``OWNER_T_CLUMP``, 1 + ``OWNER_TYPE::ANALYTICAL``, 1, ``OWNER_T_ANALYTICAL``, 4 + ``OWNER_TYPE::MESH``, 2, ``OWNER_T_MESH``, 2 + +For Python enum access, use ``deme.OWNER_TYPE.CLUMP`` and the analogous names. +Check the field or API's declared type before interpreting a numeric value. + +Analytical geometry subtypes +---------------------------- + +``objType_t`` is an unsigned 8-bit integer identifying the shape of an +analytical component. All these shapes have geometry code +``GEO_T_ANALYTICAL`` (4); their subtype is not packed into the contact code. + +.. csv-table:: + :header: "Value", "Internal name", "Shape" + :widths: 10 50 40 + + 0, ``ANAL_OBJ_TYPE_PLANE``, Plane + 1, ``ANAL_OBJ_TYPE_PLATE``, Plate + 2, ``ANAL_OBJ_TYPE_CYL_INF``, Infinite cylinder + 3, ``ANAL_OBJ_TYPE_CONE_INF``, Infinite cone + 4, ``ANAL_OBJ_TYPE_CONE``, Finite cone + +This table lists declared subtype codes; it does not assert that every shape +supports every contact path. In particular, subtype 0 means a plane, not an +invalid geometry. The separate normal-direction flag uses +``ENTITY_NORMAL_INWARD = 0`` and ``ENTITY_NORMAL_OUTWARD = 1``. + +Reading IDs and debugger output +------------------------------- + +* Type codes are distinct from owner IDs, geometry IDs, and family numbers. + An owner ID of 0 may be valid even though the owner *type* code 0 is a sentinel. +* Primitive and patch contacts use the same contact type encoding but different + geometry indexing. ``DEME_GET_GEO_OWNER_ID`` maps triangles through + ``ownerTriMesh``; ``DEME_GET_PATCH_OWNER_ID`` maps mesh patches through + ``ownerPatchMesh``. Do not use a mesh patch index as a triangle index. +* Unsigned 8-bit values may print as characters in C++ streams. Cast a type + code to ``unsigned int`` when printing its numeric value. diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..95ad06b8 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,222 @@ +Examples and co-simulation +========================== + +Run a C++ demo +-------------- + +From the repository root, after configuring as described in `Installation `__: + +.. code-block:: console + + cmake --build build --target DEMdemo_SingleSphereCollide --config Release + cd build + ./bin/DEMdemo_SingleSphereCollide + +On Windows, use ``bin/Release/DEMdemo_SingleSphereCollide.exe`` for a +multi-configuration Release build. Demos can create output files in the working +directory. Read each source file's introductory comments for inputs, output, +and any prerequisite terrain preparation. + +``SingleSphereCollide``, ``MeshCollide``, and ``MeshFalling`` use the interactive +viewer when it is enabled. See `Interactive visualization `__ for responsive loop patterns, +the recommended PoC/small-scale scope, and simulation-throughput tradeoffs. + +Python demos +------------ + +After installing ``deme``, run a first demo from the repository root: + +.. code-block:: console + + python python/demos/single_sphere_collide.py --smoke-test + +The three physics demos run headlessly and accept ``--device``, ``--duration``, +``--output-dir``, and ``--smoke-test``. Use ``--help`` to inspect their options +without loading CUDA. Cold-cache initialization can take much longer than the +short simulated duration. They write output files for visualization; see +`the Python demos guide <../python/demos/README.md>`__ for commands, +porting scope, and output instructions. + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Script (under ``python/demos/``) + - What it does + * - `single_sphere_collide.py <../python/demos/single_sphere_collide.py>`__ + - Collides two spheres over meshes with cohesive contact; inserts the second + sphere after initialization and writes contact and geometry output. + * - `ball_drop.py <../python/demos/ball_drop.py>`__ + - Settles a polydisperse spherical bed, releases a meshed projectile, and + measures penetration at the requested final time. Runs one impact rather + than the C++ parameter sweep. + * - `centrifuge.py <../python/demos/centrifuge.py>`__ + - Rotates an analytical drum containing ellipsoids and equal-mass spheres + at three densities; reports contact torque. Uses larger particles than + the C++ case by default to reduce the particle count. + * - `jitify_cache_timing.py <../python/demos/jitify_cache_timing.py>`__ + - Times initialization of a one-sphere setup and reports the persistent + Jitify header-cache file size. Requires ``DEME_PERSISTENT_JITIFY_CACHE`` + to be an explicit filename. Compare separate runs; compiled-kernel cache + state also affects the timing. + +For smaller API-specific examples, `Python quickstart `__ runs a sphere drop, +while `Retrieving simulation data `__ demonstrates CuPy and Warp device buffers. +The ``docs/python/examples/`` directory also includes explicit GPU selection. +Use these examples with a matching version of the installed extension. + +C++ demo catalog +---------------- + +The following catalog covers every demo target in ``src/demo/CMakeLists.txt``. +Target names have the prefix ``DEMdemo_``; links below download the corresponding +source under ``src/demo/``. Choose ``SingleSphereCollide`` for a first run, +``MeshCollide`` for mesh contacts, or ``Mixer`` for a larger application. + +First simulations and contact inspection +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Demo (without ``DEMdemo_``) + - What it does + * - `SingleSphereCollide <../src/demo/DEMdemo_SingleSphereCollide.cpp>`__ + - A small sphere-collision setup with mesh boundaries, cohesive material properties, runtime insertion, and contact-force inspection. A starting point for the API and interactive viewer. + * - `MeshCollide <../src/demo/DEMdemo_MeshCollide.cpp>`__ + - Collides two cube meshes above an analytical plane, demonstrating mesh universal contact, tracking, and contact-force retrieval. + * - `MeshFalling <../src/demo/DEMdemo_MeshFalling.cpp>`__ + - Drops a mixture of meshed boxes, spheres, cones, and cylinders onto a plane to exercise mesh–mesh and mesh–plane contacts. + * - `ContactChain <../src/demo/DEMdemo_ContactChain.cpp>`__ + - Applies an external load to the topmost particle of a granular arrangement to examine contact-chain propagation. + * - `TestPack <../src/demo/DEMdemo_TestPack.cpp>`__ + - Contains rolling-up-an-incline, falling-ellipsoid, and stacked-sphere validation cases. The current main function runs the incline case; the other calls are commented out. + * - `TestRestart <../src/demo/DEMdemo_TestRestart.cpp>`__ + - Loads example contact pairs and contact wildcards from CSV, attaches them to a sphere batch, and duplicates batches to exercise contact-history initialization. + +Flow, mixing, and particle generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Demo (without ``DEMdemo_``) + - What it does + * - `Repose <../src/demo/DEMdemo_Repose.cpp>`__ + - Releases particles through a mesh funnel to form a pile for an angle-of-repose experiment. + * - `Repose2D <../src/demo/DEMdemo_Repose2D.cpp>`__ + - An angle-of-repose variant using ForceModel2D.cu and particle centers sampled in the X–Z plane. + * - `Plow <../src/demo/DEMdemo_Plow.cpp>`__ + - Moves a bowl-shaped plow through granular material using prescribed motion. + * - `Sieve <../src/demo/DEMdemo_Sieve.cpp>`__ + - Prescribes back-and-forth motion of a clump-built sieve so sufficiently small particles can fall through. + * - `RotatingDrum <../src/demo/DEMdemo_RotatingDrum.cpp>`__ + - Rotates a drum represented by clumped spheres with ellipsoidal grains inside to study the granular free-surface slope. + * - `Centrifuge <../src/demo/DEMdemo_Centrifuge.cpp>`__ + - Rotates an analytical container containing grains of different shapes and densities; family labels distinguish the populations. + * - `DrumCubes <../src/demo/DEMdemo_DrumCubes.cpp>`__ + - Rotates an analytical cylindrical drum and lids containing cube mesh particles, demonstrating repeated mesh templates and mesh contacts. + * - `DrumCubesSmall <../src/demo/DEMdemo_DrumCubesSmall.cpp>`__ + - A smaller cube-particle setup using 10 mm cubes. The current code uses rotating planar side walls rather than the cylindrical wall described in its introductory comment. + * - `ResponseAngleMesh <../src/demo/DEMdemo_ResponseAngleMesh.cpp>`__ + - Loads mesh-particle templates from STL and rotates a meshed drum with analytical end caps to study the surface response angle. + * - `Mixer <../src/demo/DEMdemo_Mixer.cpp>`__ + - Prescribes rotation of a meshed bladed mixer through three-sphere clump particles inside a cylindrical chamber. + * - `MixerCubes <../src/demo/DEMdemo_MixerCubes.cpp>`__ + - Replaces the mixer demo’s clumps with cube mesh particles, enabling contacts with the mixer, chamber, and other cubes. + * - `HopperSphereCylinder <../src/demo/DEMdemo_HopperSphereCylinder.cpp>`__ + - Settles and discharges a mixture of spheres and cylinder-shaped clumps through a mesh funnel after opening its gate. + * - `HopperSphereMeshedCylinder <../src/demo/DEMdemo_HopperSphereMeshedCylinder.cpp>`__ + - Discharges spheres mixed with low-poly mesh cylinders by disabling a mesh plug’s contacts. Includes a small self-checking --smoke-test mode. + * - `PolydisperseGeneration <../src/demo/DEMdemo_PolydisperseGeneration.cpp>`__ + - Settles a spherical bed, inspects its height, and writes a clump checkpoint. The proposed runtime particle-enlargement loop is currently commented out. + * - `Shake <../src/demo/DEMdemo_Shake.cpp>`__ + - Shakes a jar of particles to explore changes in packing and bulk density. + +Impact and soil characterization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Demo (without ``DEMdemo_``) + - What it does + * - `BallDrop <../src/demo/DEMdemo_BallDrop.cpp>`__ + - Drops a meshed ball into a loose granular bed over a sweep of projectile densities and drop heights. + * - `BallDrop2D <../src/demo/DEMdemo_BallDrop2D.cpp>`__ + - A ball-impact variant using ForceModel2D.cu, with a settling stage followed by release of a tracked mesh projectile. + * - `PlateSinkage <../src/demo/DEMdemo_PlateSinkage.cpp>`__ + - Prepares and saves a cohesionless bed, reloads its positions and orientations with a cohesive model, then presses a circular plate into it to record pressure versus sinkage. + * - `ConePenetration <../src/demo/DEMdemo_ConePenetration.cpp>`__ + - Compresses a bed of clumped particles before performing a cone-penetration test; demonstrates stepwise control of the compressor. + * - `Indentation <../src/demo/DEMdemo_Indentation.cpp>`__ + - Compresses a granular sample and uses contact-neighbor information and custom properties to examine its strain distribution. + +Terrain preparation and wheel mobility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Demo (without ``DEMdemo_``) + - What it does + * - `GRCPrep_Part1 <../src/demo/DEMdemo_GRCPrep_Part1.cpp>`__ + - Generates and settles batches of GRC simulant particles with a distribution of sizes and shapes; writes the initial bed for Part 2. + * - `GRCPrep_Part2 <../src/demo/DEMdemo_GRCPrep_Part2.cpp>`__ + - Reads Part 1 output and replicates the settled particles to create a thicker bed. This is a large, potentially multimillion-particle case. + * - `GRCPrep_Part3 <../src/demo/DEMdemo_GRCPrep_Part3.cpp>`__ + - Reads Part 2 particle positions and orientations, builds the terrain, compresses it with a plane, and saves the final bed. + * - `RoverWheel <../src/demo/DEMdemo_RoverWheel.cpp>`__ + - Drives a sphere-clump wheel with prescribed angular velocity over a bed of ellipsoidal clumps; demonstrates wheel principal-frame setup. + * - `WheelDP <../src/demo/DEMdemo_WheelDP.cpp>`__ + - Runs a drawbar-pull test with a meshed Curiosity wheel and prepared GRC terrain. Requires GRC_3e6.csv from the GRC preparation sequence in the working directory. + * - `WheelDPSimplified <../src/demo/DEMdemo_WheelDPSimplified.cpp>`__ + - Builds its own three-sphere-clump terrain and prescribes wheel translation and rotation for one slip case, measuring terrain forces without a GRC checkpoint prerequisite. + * - `WheelSlopeSlip <../src/demo/DEMdemo_WheelSlopeSlip.cpp>`__ + - Studies slip versus slope using a sphere-clump representation of a Viper-style wheel. Requires the prepared GRC_3e6.csv terrain checkpoint. + +Custom physics and coupling building blocks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 28 72 + + * - Demo (without ``DEMdemo_``) + - What it does + * - `Electrostatic <../src/demo/DEMdemo_Electrostatic.cpp>`__ + - Uses rigid combined groups for charged particles and a segmented mesh rod. Owner charge wildcards and a custom force model demonstrate spatially varying electrostatic interactions. + * - `FractureBox <../src/demo/DEMdemo_FractureBox.cpp>`__ + - Breaks a concrete bar using a custom force model with interparticle bonds and contact-history variables. + * - `FlexibleMesh <../src/demo/DEMdemo_FlexibleMesh.cpp>`__ + - Retrieves mesh-node data and prescribes deformation while interacting with grains. Illustrates the data exchange needed for flexible-body coupling; it does not include a structural solver. + * - `SolarSystem <../src/demo/DEMdemo_SolarSystem.cpp>`__ + - Uses a custom nonlocal gravitational force to model the Sun and planets. Demonstrates interactions without physical overlap; broadly expanded interaction ranges can create many candidate pairs. + * - `GameOfLife <../src/demo/DEMdemo_GameOfLife.cpp>`__ + - Implements cellular-automaton evolution with DEME’s APIs to illustrate uses beyond granular mechanics. + +Run the GRC preparation stages in order from the same working directory so +later stages can find earlier output directories. Before running ``WheelDP`` +or ``WheelSlopeSlip``, place the final terrain checkpoint where the source +expects ``GRC_3e6.csv``. These large terrain cases are not installation smoke +tests. Modular regression tests live separately under ``src/demo/ModularTests``. + +Co-simulation +------------- + +DEME handles granular dynamics and exposes forces and body state to external +solvers, including direct CUDA-buffer exchange with other GPU packages. +Coupled applications can use another solver for joints, motors, +controllers, or deformable-body mechanics. See `Retrieving simulation data `__ for +host and CUDA-array exchange and `Installation `__ for installing the C++ +library. + +The historical ``feature/DEME`` branch of ``projectchrono/chrono-projects`` +contains Chrono coupling examples. Those examples target their corresponding +Chrono/DEME revisions and may need porting to this branch. Their CMake setup +uses ``Chrono_DIR``, ``ENABLE_PROJECTS=ON``, ``ENABLE_DEME_TESTS=ON``, and +``DEME_DIR`` after both libraries are installed. diff --git a/docs/generate_python_reference.py b/docs/generate_python_reference.py new file mode 100644 index 00000000..58f9144b --- /dev/null +++ b/docs/generate_python_reference.py @@ -0,0 +1,140 @@ +"""Generate static Sphinx pages from the installed pybind11 extension. + +The generated files are ordinary RST, so subsequent documentation builds do +not import DEME or require a CUDA-capable documentation host. +""" + +from __future__ import annotations + +import argparse +import importlib +import inspect +from pathlib import Path +from types import ModuleType + + +def indent(text: str, spaces: int = 3) -> str: + prefix = " " * spaces + return "\n".join(prefix + line if line else "" for line in text.splitlines()) + + +def public_members(value: object) -> list[tuple[str, object]]: + """Return stable, public members defined by a module or bound class.""" + return sorted( + ( + (name, member) + for name, member in vars(value).items() + if not name.startswith("_") and name not in {"name", "value"} + ), + key=lambda item: item[0].lower(), + ) + + +def clean_doc(value: object, fallback: str) -> str: + return inspect.getdoc(value) or fallback + + +def render_doc(value: object, fallback: str) -> str: + """Render arbitrary pybind text literally so signatures cannot become RST.""" + return f" .. code-block:: text\n\n{indent(clean_doc(value, fallback), 6)}" + + +def render_function(name: str, value: object, owner: str | None = None) -> str: + directive = "method" if owner else "function" + qualified = f"{owner}.{name}" if owner else name + return ( + f".. py:{directive}:: {qualified}\n\n" + f"{render_doc(value, 'No description is currently available.')}\n" + ) + + +def render_class(name: str, value: type) -> str: + lines = [ + name, + "-" * len(name), + "", + f".. py:class:: {name}", + "", + render_doc(value, "No class description is currently available."), + "", + ] + + # Dunder methods are omitted from the general member list, but the + # constructor is fundamental API documentation and belongs first. + constructor = vars(value).get("__init__") + if constructor is not None: + lines.extend([render_function("__init__", constructor, name), ""]) + + for member_name, member in public_members(value): + if callable(member): + lines.extend([render_function(member_name, member, name), ""]) + elif not inspect.isclass(member): + lines.extend( + [ + f".. py:attribute:: {name}.{member_name}", + "", + render_doc(member, "Bound attribute."), + "", + ] + ) + return "\n".join(lines) + + +def generate(module: ModuleType, output: Path) -> None: + """Write one deterministic reference document for a bound module.""" + title = f"Python API reference: ``{module.__name__}``" + lines = [ + title, + "=" * len(title), + "", + ".. This file is generated by docs/generate_python_reference.py.", + ".. Do not edit it directly; improve the pybind11 docstring and regenerate.", + "", + "This reference is generated from the signatures and docstrings exposed by", + f"the installed ``{module.__name__}`` extension.", + "", + ] + + functions: list[tuple[str, object]] = [] + classes: list[tuple[str, type]] = [] + for name, value in public_members(module): + if inspect.isclass(value) and getattr(value, "__module__", "").split(".")[-1] in { + module.__name__.split(".")[-1], + "_deme", + }: + classes.append((name, value)) + elif inspect.isbuiltin(value) or inspect.isfunction(value): + functions.append((name, value)) + + if functions: + lines.extend(["Module functions", "----------------", ""]) + for name, value in functions: + lines.extend([render_function(name, value), ""]) + + if classes: + # Put the main user entry points before supporting types and enums. + class_priority = {"DEMSolver": 0, "DEMVisualizer": 1, "Tracker": 2} + classes.sort(key=lambda item: (class_priority.get(item[0], 2), item[0].lower())) + lines.extend(["Classes", "-------", ""]) + for name, value in classes: + lines.extend([render_class(name, value), ""]) + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--module", default="deme", help="Installed module to inspect") + parser.add_argument( + "--output", + type=Path, + default=Path(__file__).parent / "python" / "reference.rst", + help="RST file to write", + ) + args = parser.parse_args() + generate(importlib.import_module(args.module), args.output) + + +if __name__ == "__main__": + main() diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..69f1b787 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,40 @@ +DEM-Engine documentation +======================== + +DEM-Engine (DEME) is a GPU-accelerated discrete element method solver with a +Chrono-like C++ API and Python bindings distributed as ``deme``. + +This documentation covers installation, the simulation model, the Python and +C++ interfaces, and the constraints that matter when running JIT-compiled CUDA +kernels. + +.. toctree:: + :maxdepth: 2 + :caption: Getting started + + deme3-new-features + installation + quickstart + concepts + mesh-particles + examples + visualization + troubleshooting + +.. toctree:: + :maxdepth: 2 + :caption: Interfaces + + python/index + cpp-api/index + +.. toctree:: + :maxdepth: 2 + :caption: Development + + developer/architecture + developer/type-codes + developer/documentation + developer/packaging + developer/hosting + project diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 00000000..fb2e4e2b --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,237 @@ +Installation +============ + +System requirements +------------------- + +DEME requires: + +* a 64-bit Linux system for the currently supported Python package; +* an NVIDIA GPU; +* an NVIDIA driver compatible with the selected CUDA Toolkit; +* CUDA runtime libraries, NVRTC, and headers (installed by the ``cuda12`` extra, + or provided by a compatible system CUDA Toolkit); +* CMake 3.18 or newer and a CUDA-compatible C++ compiler when building from + source. + +The default source build also includes the interactive visualizer. On Linux, +install the X11 and OpenGL development headers listed in +`Interactive visualization `__. Set ``DEME_BUILD_VISUALIZER=OFF`` only for an explicitly +headless build; published Python wheels build the visualizer by default. + +The exact Python, CUDA, compiler, driver, and GPU architecture matrix is being +validated for DEM-Engine 3. A wheel should not be assumed portable across +CUDA major versions until that matrix is published. + +The release-wheel policy currently covers 64-bit x86 Linux with glibc 2.28 or +newer, CPython 3.9 through 3.14, and CUDA 12.9. The installed machine must +provide an NVIDIA driver compatible with CUDA 12.9. Source builds can continue +to use other supported CUDA Toolkit versions, but those builds are outside the +binary-wheel compatibility policy. + +Python package +-------------- + +Install a released wheel with: + +.. code-block:: console + + python -m pip install "deme[cuda12]" + +This installation flavor is for the Python package only. It configures the +private Python extension without changing CUDA environment variables or the +header discovery used by standalone C++ programs. C++ builds and applications +continue to use their normal system CUDA Toolkit. + +The ``cuda12`` extra installs CUDA 12.9 runtime/compiler libraries and headers +from NVIDIA wheels. No system CUDA Toolkit is required for binary-wheel users. +On WSL2, install a compatible NVIDIA driver on Windows; do not install a Linux +GPU driver inside WSL. A working GPU driver is still required and is not +installed by pip. Building from source still requires a CUDA development toolkit. + +Use plain ``pip install deme`` if you provide a system toolkit yourself. + +The canonical import is: + +.. code-block:: python + + import deme + +The historical ``import DEME`` spelling remains available as a compatibility +alias. New code should use the canonical lowercase ``deme`` distribution and +import name. + +Build a wheel from a checkout +----------------------------- + +Build prerequisites +~~~~~~~~~~~~~~~~~~~ + +The wheel contains a native CUDA/C++ extension and is compiled on the machine +that creates it. Before building, verify that the intended Python interpreter, +CMake, CUDA compiler, and NVIDIA driver are available: + +.. code-block:: console + + python3 --version + cmake --version + nvcc --version + nvidia-smi + +Initialize the bundled Git dependencies: + +.. code-block:: console + + git submodule update --init --recursive + +Run the environment-creation commands from the repository root. Start with an +empty ``dist/`` directory, or move artifacts from earlier builds elsewhere, so +that validation and installation cannot accidentally select an older wheel. + +Create and validate the wheel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use a dedicated virtual environment for packaging: + +.. code-block:: console + + python3 -m venv .venv-wheel + source .venv-wheel/bin/activate + + python -m pip install --upgrade pip + python -m pip install build twine + + cd .. + python -m build --wheel --outdir DEM-Engine/dist DEM-Engine + cd DEM-Engine + python -m twine check dist/* + +The build command deliberately runs from the checkout's parent directory. A +pre-existing local ``build/`` directory in the repository root can otherwise +shadow the PyPA package named ``build`` and cause +``No module named build.__main__``. Replace ``DEM-Engine`` with the checkout +directory name if it differs. + +``python -m build --wheel`` invokes the ``scikit-build-core`` backend from +``pyproject.toml``. That backend configures CMake with +``DEME_BUILD_PYTHON=ON``, compiles the native ``deme._deme`` extension in +Release mode, and places the resulting wheel under ``dist/``. + +The repository-root ``VERSION`` file is the authoritative DEM-Engine release +version. Update only that file when preparing a release; CMake, Python package +metadata, the Conda recipe, runtime ``__version__``, and these rendered +documentation examples derive their versions from it. + +``twine check`` validates the wheel metadata and the rendering of its package +description. A successful build should produce a platform-specific file whose +name resembles: + +.. parsed-literal:: + + dist/deme-|release|---linux_.whl + +This is not a pure-Python or universal wheel. Its filename tags determine which +Python interpreter and operating-system ABI pip will accept, while CUDA and +driver compatibility must also be validated separately. + +Test the wheel in a clean environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Leave the packaging environment, create a separate test environment, and +install the wheel there: + +.. parsed-literal:: + + deactivate + + python3 -m venv /tmp/deme-wheel-test + source /tmp/deme-wheel-test/bin/activate + + python -m pip install --upgrade pip + python -m pip install dist/deme-|release|-\*.whl + python -m pip check + +Run import checks from outside the source tree. Otherwise, files in the +checkout could hide missing wheel contents: + +.. code-block:: console + + cd /tmp + python -c "import deme; print(deme.__version__, deme.__file__)" + python -c "import DEME; print(DEME.__version__)" + +The first command should report version |release| and a module path inside +``/tmp/deme-wheel-test``. The second verifies the compatibility import; new +applications should continue to use lowercase ``import deme``. + +On a host with a supported visible NVIDIA GPU, also construct a solver to +exercise CUDA initialization and confirm the selected logical device: + +.. code-block:: console + + python -c "import deme; s = deme.DEMSolver([0]); print(s.GetGPUDeviceIDs())" + +Expected output is ``[0, 0]``. Import checks alone do not exercise solver +construction, CUDA device selection, or worker allocation. + +Return to the checkout when testing is complete: + +.. code-block:: console + + deactivate + cd /path/to/DEM-Engine + +C++ build +--------- + +.. code-block:: console + + git submodule update --init --recursive + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --parallel + +Use a focused demo or modular-test target first when validating a change. + +On native Windows, configure with CMake GUI or the command line using a +CUDA-compatible Visual Studio toolchain, then build the Release configuration: + +.. code-block:: console + + cmake --build build --config Release + +Executables from multi-configuration generators are normally under +``build/bin/Release``. Linux and WSL use ``build/bin``. WSL follows the Linux +instructions; graphical output additionally needs the display setup in +`Interactive visualization `__. + +Install the C++ library +----------------------- + +Select an installation prefix during configuration, then install after building: + +.. code-block:: console + + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/path/to/deme-install + cmake --build build --config Release --parallel + cmake --install build --config Release + +For a consuming CMake project, point ``DEME_DIR`` to the installed directory +containing ``DEMEConfig.cmake`` (under ``lib/cmake/DEME`` or +``lib64/cmake/DEME``, depending on the installation). + +Development and release packaging +--------------------------------- + +For a local source installation use ``python -m pip install .``; use +``python -m pip install -e .`` for an editable installation. These still build +a native extension and need the source-build prerequisites. Select a specific +interpreter for a manual CMake build with +``-DPython_EXECUTABLE=/path/to/python`` and ``-DDEME_BUILD_PYTHON=ON``. + +The Conda recipe is under ``recipe/``. To build it locally, install +``conda-build`` and run ``conda build recipe/ -c conda-forge``. Use compilers +and runtime libraries compatible with the target environment; see +`Troubleshooting `__ for ``GLIBCXX`` errors. + +For the supported wheel matrix, CI, portability checks, and PyPI publishing, +see `Python wheel maintenance `__. diff --git a/docs/mesh-particles.rst b/docs/mesh-particles.rst new file mode 100644 index 00000000..d3a8fd3e --- /dev/null +++ b/docs/mesh-particles.rst @@ -0,0 +1,74 @@ +Mesh particles and combined owners +================================== + +See `DEME 3 features and migration considerations `__ for contact aggregation and migration from +geometry-wildcard-based constructions to combined bodies. + +Contact scope +------------- + +This branch supports meshes as particles as well as external bodies. Enable +mesh contacts with other meshes and analytical objects during setup: + +.. code-block:: cpp + + solver.SetMeshUniversalContact(true); + +The default is false: meshes then contact clumps only. This option replaces +the older README's blanket statement that mesh–mesh and mesh–analytical contacts +are unsupported. Family contact rules and geometry-specific contact settings +still apply. See `C++ API reference `__ for the public API and `Examples and co-simulation `__ +for mesh collision and hopper examples. + +Mesh templates +-------------- + +``LoadMeshType`` loads a reusable mesh template without adding a body to the +simulation. It accepts a filename with an optional material, or a ``DEMMesh&``. +``AddMeshFromTemplate`` copies that template into a simulation instance and +preserves its template identity for template-level mass/MOI jitification. +Configure shared geometry and physical properties before instantiation. + +The following setup fragment assumes an existing solver and material, and a +mesh asset with mass and principal moments of inertia computed for its scale: + +.. code-block:: cpp + + auto mesh_type = solver.LoadMeshType("particle.obj", material); + mesh_type->SetMass(mass); + mesh_type->SetMOI(principal_moi); + auto first = solver.AddMeshFromTemplate(mesh_type, make_float3(-2.f, 0.f, 0.f)); + auto second = solver.AddMeshFromTemplate(mesh_type, make_float3(2.f, 0.f, 0.f)); + first->SetFamily(0); + second->SetFamily(0); + +The C++ position parameter is ``float3`` and defaults to the origin. Create +instances individually; there is no vector-position overload or batch overload +for this method. Loading a template once avoids repeated file loading, but +instances still copy mesh setup data; this API does not promise zero-copy +geometry storage. Changes to a template do not retroactively edit its copies. + +``Duplicate(mesh)`` copies a mesh and adds the copy to the solver. Use +``SetInitPos`` on the returned object during setup to place it. For newly loaded +standalone meshes, prefer ``AddMesh``; ``AddWavefrontMeshObject`` is a legacy API. +Resolve templates and instance properties before ``Initialize()``. Consult the +setup/runtime and ``Update()`` rules before adding bodies to a running solver. + +Rigid combined bodies +--------------------- + +``LoadCombinedClumpType`` and ``LoadCombinedMeshType`` define groups with fixed +member-relative positions and orientations. ``AddCombinedFromTemplate`` places +a group at a global pose. The selected master member provides the reference +frame. Combined bodies preserve rigid relative motion; they do not implement +joints or deformable multibody dynamics. See `Core concepts `__ and the generated +C++ and Python references for units, quaternion ordering, and tracker behavior. + +Mesh patches +------------ + +``DEMMesh::SetPatchLocations`` supplies locations relative to the mesh's implicit +center of mass, one entry per patch. ``ComputePatchLocations`` can derive them: +a single patch uses the origin, while multiple patches use averages of their +triangle centroids. These are setup quantities, not global body positions. +The C++ reference documents patch geometry and contact controls. diff --git a/docs/prepare_pypi_readme.py b/docs/prepare_pypi_readme.py new file mode 100644 index 00000000..60fc36ca --- /dev/null +++ b/docs/prepare_pypi_readme.py @@ -0,0 +1,55 @@ +"""Make README links portable in the disposable checkout used to build PyPI wheels.""" + +import argparse +from pathlib import Path +import re +from urllib.parse import quote, urlsplit + + +def prepare_readme(readme, repository_url, revision): + """Resolve local Markdown links against the exact source revision being packaged. + + GitHub resolves relative README links itself; PyPI has no repository context. + Leave external URLs, heading anchors, and the existing remote images intact. + Only the wheel-build checkout is rewritten, keeping one maintained README. + """ + base = repository_url.rstrip("/") + if urlsplit(base).scheme != "https": + raise ValueError("The repository URL must use HTTPS") + revision = quote(revision, safe="") + + def replace_link(match): + target = match.group(2) + parsed = urlsplit(target) + if parsed.scheme or parsed.netloc or not parsed.path: + return match.group(0) + path = readme.parent / parsed.path + if not path.exists(): + raise ValueError(f"README link target does not exist: {target}") + kind = "tree" if path.is_dir() else "blob" + url = f"{base}/{kind}/{revision}/{quote(parsed.path, safe='/')}" + if parsed.query: + url += "?" + parsed.query + if parsed.fragment: + url += "#" + parsed.fragment + return f"[{match.group(1)}]({url})" + + source = readme.read_text(encoding="utf-8") + # The maintained README uses inline Markdown links without titles or spaces + # in their targets; HTML images already use absolute remote URLs. + result = re.sub(r"\[([^\]\n]+)\]\(([^\s)]+)\)", replace_link, source) + readme.write_text(result, encoding="utf-8") + + +def main(): + """Prepare the supplied build-checkout README without fetching remote content.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("readme", type=Path) + parser.add_argument("--repository-url", required=True) + parser.add_argument("--revision", required=True) + args = parser.parse_args() + prepare_readme(args.readme, args.repository_url, args.revision) + + +if __name__ == "__main__": + main() diff --git a/docs/project.rst b/docs/project.rst new file mode 100644 index 00000000..893fddf8 --- /dev/null +++ b/docs/project.rst @@ -0,0 +1,85 @@ +Project information +=================== + +Community +--------- + +Discuss DEME on the `Project Chrono forum `_. +`Demo videos `_ +illustrate typical applications. + +Licensing and contributors +-------------------------- + +DEME uses the `BSD-3-Clause license <../LICENSE.md>`__. +The project is the collective intellectual property of its authors and the +University of Wisconsin–Madison. Add authors to +`the contributor list `__ rather than individual +copyright headers. Include this notice in new or modified source files: + +.. code-block:: text + + Copyright (c) 2021, Simulation-Based Engineering Laboratory + Copyright (c) 2021, University of Wisconsin - Madison + + SPDX-License-Identifier: BSD-3-Clause + +Code sourced from Chrono must also retain the appropriate original license. +The following is the source-header example previously provided in the README: + +.. code-block:: text + + Copyright (c) 2021, SBEL GPU Development Team + Copyright (c) 2021, University of Wisconsin - Madison + + SPDX-License-Identifier: BSD-3-Clause + + + This file contains modifications of the code authored by the Project Chrono + Development Team. The original license can be found below: + + Copyright (c) 2016, Project Chrono Development Team + All rights reserved. + + Use of this source code is governed by a BSD-style license that can be found + in the LICENSE file at the top level of the distribution and at + http://projectchrono.org/license-chrono.txt. A copy of the license is below. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + - Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Citation +-------- + +The DEM-Engine design and usage paper (Computer Physics Communications, 2024): + +.. code-block:: bibtex + + @article{zhang_2024_deme, + title = {Chrono {DEM-Engine}: A Discrete Element Method dual-{GPU} simulator with customizable contact forces and element shape}, + journal = {Computer Physics Communications}, + volume = {300}, + pages = {109196}, + year = {2024}, + issn = {0010-4655}, + doi = {https://doi.org/10.1016/j.cpc.2024.109196}, + author = {Ruochun Zhang and Bonaventura Tagliafierro and Colin {Vanden Heuvel} and Shlok Sabarwal and Luning Bakke and Yulong Yue and Xin Wei and Radu Serban and Dan Negruţ}, + keywords = {Discrete Element Method, GPU computing, Physics-based simulation, Scientific package, BSD3 open-source}, + } + +The clump-based granular and rover simulation paper (Engineering with Computers, 2024): + +.. code-block:: bibtex + + @article{ruochunGRC-DEM2023, + title={A {GPU}-accelerated simulator for the {DEM} analysis of granular systems composed of clump-shaped elements}, + author={Ruochun Zhang and Colin {Vanden Heuvel} and Alexander Schepelmann and Arno Rogg and Dimitrios Apostolopoulos and Samuel Chandler and Radu Serban and Dan Negrut}, + year={2024}, + journal={Engineering with Computers}, + doi={https://doi.org/10.1007/s00366-023-01921-9} + } diff --git a/docs/python/api-overview.rst b/docs/python/api-overview.rst new file mode 100644 index 00000000..8a1bb2aa --- /dev/null +++ b/docs/python/api-overview.rst @@ -0,0 +1,96 @@ +Python API overview +=================== + +DEME currently preserves its established C++-style method names in Python. +This page maps common tasks to the principal bound objects; runtime docstrings +and the `C++ API reference <../cpp-api/index.rst>`__ provide signature-level detail. + +Core objects +------------ + +``DEMSolver`` + Owns workers and simulation state. It loads setup data, configures the + solver, initializes, advances time, creates trackers and inspectors, and + writes output. + +``DEMMaterial`` + Stores material properties loaded through ``DEMSolver.LoadMaterial``. + +``DEMClumpTemplate`` and ``DEMClumpBatch`` + Describe clump topology and a group of instances. A batch returned by + ``AddClumps`` can be passed to ``Track``. + +``DEMMesh`` and ``DEMExternObj`` + Represent mesh particles/bodies and analytical external geometry. See + `Mesh particles and combined owners <../mesh-particles.rst>`__ for mesh templates and contact scope. + +``Tracker`` (native ``DEMTracker``) + Reads or modifies a tracked batch/object after initialization. See + `Retrieving simulation data `__. + +``DEMInspector`` + Queries aggregate or spatial simulation properties. + +``DEMForceModel`` + Selects or customizes the contact force calculation and its material or + wildcard requirements. + +``DEMVisualizer`` + Displays the current solver state on each explicit ``Render`` call without + advancing the simulation. See `Interactive visualization <../visualization.rst>`__. + +Common task map +--------------- + +.. list-table:: + :header-rows: 1 + + * - Task + - Starting methods + * - Select GPUs + - ``DEMSolver(...)``, ``GetGPUDeviceIDs`` + * - Load material + - ``LoadMaterial`` + * - Define the domain + - ``InstructBoxDomainDimension``, ``InstructBoxDomainBoundingBC`` + * - Create particles + - ``LoadSphereType``, clump-template loaders, ``AddClumps`` + * - Create geometry + - ``LoadMeshType``, mesh/external-object add methods, ``AddBCPlane`` + * - Enable mesh–mesh and mesh–analytical contacts + - ``SetMeshUniversalContact(True)`` during setup + * - Create rigid combined bodies + - ``LoadCombinedClumpType``, ``LoadCombinedMeshType``, ``AddCombinedFromTemplate`` + * - Configure motion + - gravity, family prescriptions, timestep methods + * - Start simulation + - ``Initialize`` + * - Advance simulation + - ``DoDynamics``, ``DoDynamicsThenSync``, ``DoStepDynamics`` + * - Inspect or control objects + - ``Track`` and ``Tracker`` methods + * - Retrieve CUDA data + - ``Tracker`` methods ending in ``ToDevice`` + * - Write output + - ``WriteSphereFile``, mesh/contact output methods, + ``WaitForPendingOutput`` + * - Render interactively + - ``DEMVisualizer``, ``Render``, ``SetRenderSpheres``, + ``SetRenderTriangles`` + +Discovering installed signatures +-------------------------------- + +The pybind11 extension exposes method signatures and docstrings directly: + +.. code-block:: python + + import deme + + help(deme.DEMSolver) + help(deme.Tracker) + print(deme.DEMSolver.Initialize.__doc__) + +Static type stubs and automatically generated Python API pages are not yet +shipped. Consequently, IDE completion may be incomplete even though a method +is available at runtime. diff --git a/docs/python/data-access.rst b/docs/python/data-access.rst new file mode 100644 index 00000000..c7eb7d69 --- /dev/null +++ b/docs/python/data-access.rst @@ -0,0 +1,171 @@ +Retrieving simulation data +========================== + +Trackers expose both ordinary Python-returning methods and zero-host-copy-style +methods that fill caller-owned CUDA memory. + +Host access +----------- + +Use host methods for inspection, logging, and small result sets: + +.. code-block:: python + + solver.DoDynamicsThenSync(0.01) + position = tracker.Pos() # first tracked owner + positions = tracker.Positions() # every owner in this tracker + velocities = tracker.Velocities() + +Most single-object methods accept an optional owner offset. Bulk methods return +all owners covered by the tracker. Quaternion values use public ordering +``(x, y, z, w)``. + +Direct CUDA access +------------------ + +The ``*ToDevice`` methods synchronously fill a raw CUDA pointer represented by +a Python integer. This avoids first materializing the result as a Python list. +The allocation remains entirely owned by the caller. + +This CuPy example retrieves all tracked positions: + +.. literalinclude:: examples/cupy_device_retrieval.py + :language: python + :linenos: + +The direct interface is intentionally low level. Every call must satisfy all +of these requirements: + +* ``pointer`` is a valid, writable CUDA-accessible pointer on ``device``; +* the allocation remains alive until the synchronous call returns; +* ``capacity`` is a number of result elements, not bytes; +* dtype and row layout match the method; +* the current simulation state has been synchronized before it is read. + +Supplying the wrong pointer, device, dtype, layout, or capacity can cause a +native CUDA error or memory corruption. Prefer host access unless eliminating +the host transfer materially helps the application. + +Why the pointer argument is an integer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Python bindings represent a native CUDA address with C++ +``std::uintptr_t``. Pybind11 accepts an ordinary Python ``int`` for that +argument, so callers pass the numeric address exported by their array library: + +.. code-block:: python + + tracker.PositionsToDevice( + int(cuda_array_pointer), + owner_count, + cuda_device_id, + ) + +The integer is only an address; passing it does not transfer ownership, retain +the allocation, infer its size or dtype, or validate which CUDA device owns +it. The Python array that owns the address must remain alive through the call. +A Python object such as a NumPy array, Warp array, CuPy array, or PyTorch tensor +must not itself be supplied where the integer pointer is expected. + +Common pointer accessors are ``warp_array.ptr``, ``cupy_array.data.ptr``, and +``torch_tensor.data_ptr()``. Only CUDA-accessible allocations are valid for the +device-retrieval methods; an integer derived from ordinary NumPy host storage +is not valid. + +Tracked-owner methods +--------------------- + +The following table gives the destination representation. A ``float3`` result +maps to a C-contiguous ``(capacity, 3)`` array of ``float32``; ``float4`` maps +to ``(capacity, 4)``. + +.. list-table:: + :header-rows: 1 + + * - Method + - Destination element + - Meaning + * - ``PositionsToDevice`` + - ``float3`` + - Global position + * - ``VelocitiesToDevice`` + - ``float3`` + - Global linear velocity + * - ``AngularVelocitiesLocalToDevice`` + - ``float3`` + - Local angular velocity + * - ``AngularVelocitiesGlobalToDevice`` + - ``float3`` + - Global angular velocity + * - ``OrientationQuaternionsToDevice`` + - ``float4`` + - Quaternion ``(x, y, z, w)`` + * - ``FamiliesToDevice`` + - ``uint32`` + - Family number + * - ``MassesToDevice`` + - ``float32`` + - Mass + * - ``MOIsToDevice`` + - ``float3`` + - Principal moments of inertia + * - ``ContactAccelerationsToDevice`` + - ``float3`` + - Global contact acceleration + * - ``ContactAngularAccelerationsLocalToDevice`` + - ``float3`` + - Local contact angular acceleration + * - ``ContactAngularAccelerationsGlobalToDevice`` + - ``float3`` + - Global contact angular acceleration + * - ``OwnerWildcardValuesToDevice`` + - ``float32`` + - Named owner wildcard + +The required owner capacity is the number of owners covered by the tracker. +In the common case where a tracker was created from a clump batch, this is the +number of clumps added in that batch. + +Contact-pair methods +-------------------- + +Python exposes three all-owner compacting methods: + +* ``GetContactForcesForAllToDevice(points, forces, capacity, device)`` +* ``GetContactForcesAndLocalTorqueForAllToDevice(points, forces, torques, capacity, device)`` +* ``GetContactForcesAndGlobalTorqueForAllToDevice(points, forces, torques, capacity, device)`` + +Every destination is a C-contiguous ``(capacity, 3)`` ``float32`` allocation. +The methods return the number of valid compacted contact pairs; only rows +before that count contain results. Capacity must cover the simulation's total +recorded contact count, not merely the final number involving this tracker. +Contact retrieval also requires force recording to remain enabled. + +Destination device +------------------ + +``device`` names the logical CUDA device that owns the destination allocation; +it does not have to be inferred from the pointer. With CuPy, enter +``with cp.cuda.Device(device):`` before allocating, pass ``array.data.ptr``, +and keep the array in scope through the call. DEME handles retrieval when the +destination differs from a worker device, subject to the CUDA capabilities of +the system. + +NVIDIA Warp +----------- + +A Warp CUDA array exposes its base allocation address through the integer +``array.ptr`` property. Use packed Warp dtypes that match the native CUDA +element type: ``wp.vec3`` for ``float3``, ``wp.vec4`` for ``float4``, +``wp.float32`` for ``float``, and ``wp.uint32`` for ``unsigned int``. + +.. literalinclude:: examples/warp_device_retrieval.py + :language: python + :linenos: + +Pass the CUDA logical ordinal used in the Warp device string as the DEME +``device`` argument. The array must be contiguous and must remain alive until +the synchronous DEME call returns. Synchronizing the Warp device immediately +after allocation is the conservative interoperability choice because Warp may +allocate from a stream-ordered CUDA memory pool while DEME performs the copy on +its own stream. diff --git a/docs/python/device-selection.rst b/docs/python/device-selection.rst new file mode 100644 index 00000000..fd0f6bf0 --- /dev/null +++ b/docs/python/device-selection.rst @@ -0,0 +1,56 @@ +CUDA device selection +===================== + +DEME runs a dynamic worker (``dT``) and a kinematic/contact-detection worker +(``kT``). Their logical CUDA devices are selected when ``DEMSolver`` is +constructed. + +Count-based selection +--------------------- + +.. code-block:: python + + import deme + + solver = deme.DEMSolver(1) # dT and kT both use logical device 0 + +``DEMSolver()`` is equivalent to ``DEMSolver(2)``. When at least two devices +are visible, it assigns dT to device 0 and kT to device 1. If only one is +visible, DEME warns and assigns both workers to device 0. Only counts 1 and 2 +are accepted. + +Explicit logical device IDs +--------------------------- + +Use a list to choose devices rather than relying on the first visible devices: + +.. literalinclude:: examples/device_selection.py + :language: python + :linenos: + +A one-element list assigns both workers to that device. In a two-element list, +the first ID is assigned to dT and the second to kT. Repeated IDs are valid, so +``DEMSolver([2, 2])`` places both workers on logical device 2. + +``GetGPUDeviceIDs()`` returns ``[dT_device, kT_device]`` and is useful for +logging or choosing a destination device for direct data retrieval. + +Logical versus physical IDs +--------------------------- + +IDs are CUDA logical device IDs as seen by this process. Environment-level +masking and reordering such as ``CUDA_VISIBLE_DEVICES`` therefore affect the +numbering. For example: + +.. code-block:: console + + CUDA_VISIBLE_DEVICES=3,1 python my_simulation.py + +Inside that process, logical device 0 denotes physical device 3 and logical +device 1 denotes physical device 1. An explicit ID must be non-negative and +smaller than the number of visible devices; invalid IDs fail during +construction. + +The constructor allocates CUDA resources. Select visibility before starting +Python, and construct the solver only after the intended CUDA environment is +in place. diff --git a/docs/python/examples/cupy_device_retrieval.py b/docs/python/examples/cupy_device_retrieval.py new file mode 100644 index 00000000..b37c2a0b --- /dev/null +++ b/docs/python/examples/cupy_device_retrieval.py @@ -0,0 +1,30 @@ +"""Retrieve tracked DEME positions directly into a CuPy allocation.""" + +import cupy as cp +import deme + + +solver = deme.DEMSolver([0]) +material = solver.LoadMaterial( + {"E": 1.0e7, "nu": 0.3, "CoR": 0.5, "mu": 0.4, "Crr": 0.0} +) +solver.InstructBoxDomainDimension(1.0, 1.0, 1.0) +sphere_type = solver.LoadSphereType(0.01, 0.025, material) +initial_positions = [[0.0, 0.0, 0.2], [0.0, 0.0, 0.4]] +sphere_batch = solver.AddClumps(sphere_type, initial_positions) +tracker = solver.Track(sphere_batch) +solver.SetInitTimeStep(1.0e-5) +solver.Initialize() +solver.DoDynamicsThenSync(0.001) + +destination_device = 0 +owner_count = len(initial_positions) +with cp.cuda.Device(destination_device): + # A float3 result requires a C-contiguous (owner_count, 3) float32 array. + positions = cp.empty((owner_count, 3), dtype=cp.float32) + tracker.PositionsToDevice( + positions.data.ptr, + owner_count, # Capacity is float3 elements, not bytes or scalar floats. + destination_device, + ) + print(positions) diff --git a/docs/python/examples/device_selection.py b/docs/python/examples/device_selection.py new file mode 100644 index 00000000..01ffa19b --- /dev/null +++ b/docs/python/examples/device_selection.py @@ -0,0 +1,9 @@ +"""Select DEME worker devices explicitly.""" + +import deme + + +# The first ID selects the dynamic worker; the second selects the +# kinematic/contact-detection worker. +solver = deme.DEMSolver([2, 3]) +print("DEME worker devices [dT, kT]:", solver.GetGPUDeviceIDs()) diff --git a/docs/python/examples/sphere_drop.py b/docs/python/examples/sphere_drop.py new file mode 100644 index 00000000..642569df --- /dev/null +++ b/docs/python/examples/sphere_drop.py @@ -0,0 +1,37 @@ +"""Run a minimal one-sphere DEME simulation and print its final position.""" + +import deme + + +# A one-element device list places both DEME workers on logical CUDA device 0. +solver = deme.DEMSolver([0]) + +material = solver.LoadMaterial( + { + "E": 1.0e7, + "nu": 0.3, + "CoR": 0.5, + "mu": 0.4, + "Crr": 0.0, + } +) + +# Explicit spans make the global origin and floor location unambiguous. +solver.InstructBoxDomainDimension( + (-0.5, 0.5), + (-0.5, 0.5), + (0.0, 1.0), +) +solver.InstructBoxDomainBoundingBC("top_open", material) + +sphere_type = solver.LoadSphereType(0.01, 0.025, material) +sphere_batch = solver.AddClumps(sphere_type, [[0.0, 0.0, 0.5]]) +sphere_tracker = solver.Track(sphere_batch) + +solver.SetGravitationalAcceleration([0.0, 0.0, -9.81]) +solver.SetInitTimeStep(1.0e-5) +solver.Initialize() + +# Synchronize before querying tracked state on the host. +solver.DoDynamicsThenSync(0.01) +print("Sphere position:", sphere_tracker.Pos()) diff --git a/docs/python/examples/warp_device_retrieval.py b/docs/python/examples/warp_device_retrieval.py new file mode 100644 index 00000000..0a9a5c25 --- /dev/null +++ b/docs/python/examples/warp_device_retrieval.py @@ -0,0 +1,20 @@ +"""Retrieve DEME positions into a caller-owned NVIDIA Warp array.""" + +import warp as wp + + +def retrieve_positions(tracker, owner_count: int, device_id: int = 0) -> wp.array: + """Return tracked positions as a contiguous ``wp.vec3`` CUDA array.""" + device = f"cuda:{device_id}" + positions = wp.empty(owner_count, dtype=wp.vec3, device=device) + + # Warp may use stream-ordered allocation. Make the allocation visible + # before DEME accesses it from DEME's own CUDA stream. + wp.synchronize_device(device) + + tracker.PositionsToDevice( + int(positions.ptr), # pybind11 converts this Python int to std::uintptr_t. + owner_count, # Capacity is float3/wp.vec3 elements, not bytes. + device_id, + ) + return positions diff --git a/docs/python/index.rst b/docs/python/index.rst new file mode 100644 index 00000000..e57b7e27 --- /dev/null +++ b/docs/python/index.rst @@ -0,0 +1,61 @@ +Python interface +================ + +The Python package exposes DEM-Engine's solver, setup objects, trackers, and +CUDA-aware data access. Start with the quickstart, then use the focused guides +for device placement and result retrieval. + +.. toctree:: + :maxdepth: 2 + + quickstart + solver-lifecycle + device-selection + data-access + api-overview + reference + +Import and compatibility +------------------------ + +Use the lowercase package in new code: + +.. code-block:: python + + import deme + +``import DEME`` re-exports the same API for backward compatibility. The +capitalized spelling should not be used in new examples. + +Version +------- + +The runtime version is available as: + +.. parsed-literal:: + + assert deme.__version__ == "|release|" + +API conventions +--------------- + +The current bindings intentionally retain the C++ method names, such as +``SetGravitationalAcceleration`` and ``DoDynamics``. Pythonic snake-case aliases +and type stubs are planned, but are not yet part of the stable interface. + +CUDA vector types exposed by the API are generally accepted as three- or +four-element Python sequences and returned as tuples. Quaternion ordering is +``(x, y, z, w)``. + +Objects returned by solver setup methods frequently own or reference native +simulation state. Keep the solver alive for as long as trackers, inspectors, or +other solver-owned handles are in use. + +Reference status +---------------- + +The `Python API overview `__ inventories the main Python objects and points to the +task-oriented guides. The extension also contains runtime pybind11 docstrings; +use ``help(deme.DEMSolver)`` or ``help(deme.Tracker)`` in an installed +environment. The generated `C++ API reference <../cpp-api/index.rst>`__ remains the exhaustive +low-level reference for APIs shared with C++. diff --git a/docs/python/quickstart.rst b/docs/python/quickstart.rst new file mode 100644 index 00000000..96fe64a6 --- /dev/null +++ b/docs/python/quickstart.rst @@ -0,0 +1,61 @@ +Python quickstart +================= + +Install DEME in an environment with a compatible NVIDIA driver and CUDA +runtime, then verify the package and version: + +.. code-block:: console + + python -c "import deme; print(deme.__version__)" + +The following complete example creates a material, a bounded domain, and one +spherical clump, then advances the simulation: + +.. literalinclude:: examples/sphere_drop.py + :language: python + :linenos: + +Run it with: + +.. code-block:: console + + python docs/python/examples/sphere_drop.py + +The solver constructor initializes CUDA worker resources, so even importing +successfully is not sufficient to run a simulation without a visible, +supported NVIDIA GPU. See `CUDA device selection `__ when the process can see more +than one GPU. + +The example uses ``DoDynamicsThenSync`` because the position is read +immediately afterward. For longer simulations, asynchronous ``DoDynamics`` +calls can overlap host work; synchronize before reading results or exiting. + +More Python demos +----------------- + +The repository's ``python/demos`` directory includes ports of +``DEMdemo_SingleSphereCollide``, ``DEMdemo_BallDrop``, and +``DEMdemo_Centrifuge``. They demonstrate cohesive contact, runtime particle +insertion, a meshed projectile, and a rotating analytical drum: + +.. code-block:: console + + python python/demos/single_sphere_collide.py --smoke-test + python python/demos/ball_drop.py --smoke-test + python python/demos/centrifuge.py --smoke-test + +Run from a checkout with the Python package installed. Each script supports +``--help``, ``--device``, ``--duration``, and ``--output-dir``. See +``python/demos/README.md`` for full runs, output visualization, and differences +from the larger C++ examples. Smoke runs still require a GPU and first-time +kernel compilation. + +Where to go next +---------------- + +* `Solver lifecycle `__ explains which operations belong before and after + ``Initialize()``. +* `Retrieving simulation data `__ covers host-returning tracker methods and direct retrieval + into CUDA arrays. +* `Python API overview `__ maps common tasks to the Python objects and methods that + implement them. diff --git a/docs/python/reference.rst b/docs/python/reference.rst new file mode 100644 index 00000000..087dddf6 --- /dev/null +++ b/docs/python/reference.rst @@ -0,0 +1,5435 @@ +Python API reference: ``deme`` +============================== + +.. This file is generated by docs/generate_python_reference.py. +.. Do not edit it directly; improve the pybind11 docstring and regenerate. + +This reference is generated from the signatures and docstrings exposed by +the installed ``deme`` extension. + +Module functions +---------------- + +.. py:function:: DEMBoxGridSampler + + .. code-block:: text + + DEMBoxGridSampler(BoxCenter: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], HalfDims: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], GridSizeX: typing.SupportsFloat | typing.SupportsIndex, GridSizeY: typing.SupportsFloat | typing.SupportsIndex = -1.0, GridSizeZ: typing.SupportsFloat | typing.SupportsIndex = -1.0) -> list[float3] + + Return regularly spaced points inside a box. Negative Y or Z spacing reuses the X spacing. + + +.. py:function:: DEMBoxHCPSampler + + .. code-block:: text + + DEMBoxHCPSampler(BoxCenter: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], HalfDims: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], separation: typing.SupportsFloat | typing.SupportsIndex) -> list[float3] + + Return hexagonally close-packed points inside a box. + + +.. py:function:: DEMCylSurfSampler + + .. code-block:: text + + DEMCylSurfSampler(CylCenter: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], CylAxis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], CylRad: typing.SupportsFloat | typing.SupportsIndex, CylHeight: typing.SupportsFloat | typing.SupportsIndex, ParticleRad: typing.SupportsFloat | typing.SupportsIndex, spacing: typing.SupportsFloat | typing.SupportsIndex = 1.2) -> list[list[float]] + + Return points on a cylindrical surface. ``spacing`` scales the nominal particle-diameter separation. + + +.. py:function:: FrameTransformGlobalToLocal + + .. code-block:: text + + FrameTransformGlobalToLocal(arg0: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[float] + + Translating the inverse of the provided vec then applying a local inverse rotation of the provided rot_Q, then return the result. + + +.. py:function:: FrameTransformLocalToGlobal + + .. code-block:: text + + FrameTransformLocalToGlobal(arg0: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[float] + + Apply a local rotation then a translation, then return the result. + + +.. py:function:: GetDEMEDataFile + + .. code-block:: text + + GetDEMEDataFile(relative_path: str) -> str + + Resolve a path relative to DEME's installed data directory. + + +Classes +------- + +DEMSolver +--------- + +.. py:class:: DEMSolver + + .. code-block:: text + + Own a DEM simulation and its dynamic and kinematic CUDA workers. + + Configure materials, geometry, particles, solver options, and trackers before + calling ``Initialize``. Device placement is fixed at construction. Keep this + object alive while using any solver-owned material, template, tracker, or + inspector handle. + +.. py:method:: DEMSolver.__init__ + + .. code-block:: text + + __init__(*args, **kwargs) + Overloaded function. + + 1. __init__(self: deme._deme.DEMSolver, nGPUs: typing.SupportsInt | typing.SupportsIndex = 2) -> None + + + Construct a solver using one or two logical CUDA devices. + + With one GPU, both workers use device 0. With two requested GPUs and at least + two visible devices, the dynamic worker uses device 0 and the kinematic worker + uses device 1. If only one device is visible, both workers use device 0 and a + warning is emitted. Only 1 and 2 are valid values. + + + 2. __init__(self: deme._deme.DEMSolver, device_ids: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None + + + Construct a solver on explicit logical CUDA device IDs. + + One ID places both workers on that device. For two IDs, the first selects the + dynamic worker and the second selects the kinematic worker. Repeated IDs are + valid. IDs use the numbering visible to this process, including the effects of + ``CUDA_VISIBLE_DEVICES``. + + +.. py:method:: DEMSolver.AddBCPlane + + .. code-block:: text + + AddBCPlane(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: deme::DEMMaterial) -> deme::DEMExternObj + + Add an analytical plane to the simulation. + + +.. py:method:: DEMSolver.AddClumps + + .. code-block:: text + + AddClumps(*args, **kwargs) + Overloaded function. + + 1. AddClumps(self: deme._deme.DEMSolver, arg0: deme::DEMClumpBatch) -> deme::DEMClumpBatch + + Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial location means the location of the clumps' CoM coordinates in the global frame. + + 2. AddClumps(self: deme._deme.DEMSolver, arg0: deme::DEMClumpTemplate, arg1: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> deme::DEMClumpBatch + + Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial location means the location of the clumps' CoM coordinates in the global frame. + + 3. AddClumps(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[deme::DEMClumpTemplate], arg1: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> deme::DEMClumpBatch + + Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial location means the location of the clumps' CoM coordinates in the global frame. + + +.. py:method:: DEMSolver.AddCombinedFromTemplate + + .. code-block:: text + + AddCombinedFromTemplate(*args, **kwargs) + Overloaded function. + + 1. AddCombinedFromTemplate(self: deme._deme.DEMSolver, combined_template: deme::DEMCombinedTemplate, init_pos: collections.abc.Sequence[float3], init_oriQ: collections.abc.Sequence[float4] = []) -> deme::DEMCombinedInstances + + Instantiate a combined template at one or more user-specified global poses (batch). + + 2. AddCombinedFromTemplate(self: deme._deme.DEMSolver, combined_template: deme::DEMCombinedTemplate, init_pos: float3 = (0.0, 0.0, 0.0), init_oriQ: float4 = (0.0, 0.0, 0.0, 1.0)) -> deme::DEMCombinedInstances + + Instantiate a combined template at a single user-specified global pose. + + +.. py:method:: DEMSolver.AddExternalObject + + .. code-block:: text + + AddExternalObject(self: deme._deme.DEMSolver) -> deme::DEMExternObj + + Add an analytical object to the simulation system. + + +.. py:method:: DEMSolver.AddFamilyPrescribedAcc + + .. code-block:: text + + AddFamilyPrescribedAcc(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, pre: str = 'none') -> None + + The entities in this family will always experienced an extra acceleration defined using this method. + + +.. py:method:: DEMSolver.AddFamilyPrescribedAngAcc + + .. code-block:: text + + AddFamilyPrescribedAngAcc(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, pre: str = 'none') -> None + + The entities in this family will always experienced an extra angular acceleration defined using this method. + + +.. py:method:: DEMSolver.AddKernelInclude + + .. code-block:: text + + AddKernelInclude(self: deme._deme.DEMSolver, arg0: str) -> None + + Add a library that the kernels will be compiled with (so that the user can use the provided methods in their customized code, like force model). + + +.. py:method:: DEMSolver.AddMeshFromTemplate + + .. code-block:: text + + AddMeshFromTemplate(self: deme._deme.DEMSolver, mesh_template: deme::DEMMesh, init_pos: float3 = (0.0, 0.0, 0.0)) -> deme::DEMMesh + + Instantiate a mesh from a cached template at the requested initial position. + + +.. py:method:: DEMSolver.AddOwnerNextStepAcc + + .. code-block:: text + + AddOwnerNextStepAcc(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float3]) -> None + + Add an extra acceleration to a owner for the next time step. + + +.. py:method:: DEMSolver.AddOwnerNextStepAngAcc + + .. code-block:: text + + AddOwnerNextStepAngAcc(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float3]) -> None + + Add an extra angular acceleration to a owner for the next time step. + + +.. py:method:: DEMSolver.AddShellMesh + + .. code-block:: text + + AddShellMesh(self: deme._deme.DEMSolver, mesh: deme::DEMMesh, shell_thickness: typing.SupportsFloat | typing.SupportsIndex) -> deme::DEMMesh + + Load a finite-thickness shell mesh from a user-constructed mesh object. + + +.. py:method:: DEMSolver.AddWavefrontMeshObject + + .. code-block:: text + + AddWavefrontMeshObject(*args, **kwargs) + Overloaded function. + + 1. AddWavefrontMeshObject(self: deme._deme.DEMSolver, filename: str, mat: deme::DEMMaterial, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a mesh-represented object. + + 2. AddWavefrontMeshObject(self: deme._deme.DEMSolver, arg0: deme::DEMMesh) -> deme::DEMMesh + + Load a mesh-represented object. + + 3. AddWavefrontMeshObject(self: deme._deme.DEMSolver, filename: str, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a mesh-represented object. + + +.. py:method:: DEMSolver.AddWavefrontShellObject + + .. code-block:: text + + AddWavefrontShellObject(*args, **kwargs) + Overloaded function. + + 1. AddWavefrontShellObject(self: deme._deme.DEMSolver, filename: str, mat: deme::DEMMaterial, shell_thickness: typing.SupportsFloat | typing.SupportsIndex, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a finite-thickness shell mesh from a mesh file. + + 2. AddWavefrontShellObject(self: deme._deme.DEMSolver, filename: str, shell_thickness: typing.SupportsFloat | typing.SupportsIndex, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a finite-thickness shell mesh from a mesh file without assigning material immediately. + + +.. py:method:: DEMSolver.ChangeClumpFamily + + .. code-block:: text + + ChangeClumpFamily(self: deme._deme.DEMSolver, fam_num: typing.SupportsInt | typing.SupportsIndex, X: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex] = (-1000000000000000.0, 1000000000000000.0), Y: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex] = (-1000000000000000.0, 1000000000000000.0), Z: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex] = (-1000000000000000.0, 1000000000000000.0), orig_fam: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex] = set()) -> int + + Change the family number for the clumps in a box region to the specified value. + + +.. py:method:: DEMSolver.ChangeFamily + + .. code-block:: text + + ChangeFamily(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None + + Change all entities with family number ID_from to have a new number ID_to, when the condition defined by the string is satisfied by the entities in question. This should be called before initialization, and will be baked into the solver, so the conditions will be checked and changes applied every time step. + + +.. py:method:: DEMSolver.ChangeFamilyWhen + + .. code-block:: text + + ChangeFamilyWhen(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: str) -> None + + Change all entities with family number ID_from to have a new number ID_to, when the condition defined by the string is satisfied by the entities in question. This should be called before initialization, and will be baked into the solver, so the conditions will be checked and changes applied every time step. + + +.. py:method:: DEMSolver.ClearCache + + .. code-block:: text + + ClearCache(self: deme._deme.DEMSolver) -> None + + Remove host-side cached vectors (so you can re-define them, and then re-initialize system). + + +.. py:method:: DEMSolver.ClearThreadCollaborationStats + + .. code-block:: text + + ClearThreadCollaborationStats(self: deme._deme.DEMSolver) -> None + + Reset the collaboration stats between dT and kT back to the initial value (0). You should call this if you want to start over and re-inspect the stats of the new run; otherwise, it is generally not needed, you can go ahead and destroy DEMSolver. + + +.. py:method:: DEMSolver.ClearTimingStats + + .. code-block:: text + + ClearTimingStats(self: deme._deme.DEMSolver) -> None + + Reset the recordings of the wall time and percentages of wall time spend on various solver tasks. + + +.. py:method:: DEMSolver.CorrectFamilyAngVel + + .. code-block:: text + + CorrectFamilyAngVel(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, pre: str = 'none') -> None + + The entities in this family will always experience an added angular-velocity correction defined using this method. At the same time, they are still subject to the `simulation physics'. + + +.. py:method:: DEMSolver.CorrectFamilyLinVel + + .. code-block:: text + + CorrectFamilyLinVel(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, pre: str = 'none') -> None + + The entities in this family will always experience an added linear-velocity correction defined using this method. At the same time, they are still subject to the `simulation physics'. + + +.. py:method:: DEMSolver.CorrectFamilyPosition + + .. code-block:: text + + CorrectFamilyPosition(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, pre: str = 'none') -> None + + The entities in this family will always experience an added positional correction defined using this method. At the same time, they are still subject to the `simulation physics'. + + +.. py:method:: DEMSolver.CorrectFamilyQuaternion + + .. code-block:: text + + CorrectFamilyQuaternion(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, q_formula: str) -> None + + The entities in this family will always experience an added quaternion correction defined using this method. At the same time, they are still subject to the `simulation physics'. + + +.. py:method:: DEMSolver.CreateInspector + + .. code-block:: text + + CreateInspector(*args, **kwargs) + Overloaded function. + + 1. CreateInspector(self: deme._deme.DEMSolver, quantity: str = 'clump_max_z') -> deme._deme.DEMInspector + + Create a inspector object that can help query some statistical info of the clumps in the simulation. + + 2. CreateInspector(self: deme._deme.DEMSolver, arg0: str, arg1: str) -> deme._deme.DEMInspector + + Create a inspector object that can help query some statistical info of the clumps in the simulation. + + +.. py:method:: DEMSolver.DefineContactForceModel + + .. code-block:: text + + DefineContactForceModel(self: deme._deme.DEMSolver, arg0: str) -> deme._deme.DEMForceModel + + Define a custom contact force model by a string. Returns a pointer to the force model in use. + + +.. py:method:: DEMSolver.DisableAdaptiveBinSize + + .. code-block:: text + + DisableAdaptiveBinSize(self: deme._deme.DEMSolver) -> None + + Disable the use of adaptive bin size (always use initial size) + + +.. py:method:: DEMSolver.DisableAdaptiveUpdateFreq + + .. code-block:: text + + DisableAdaptiveUpdateFreq(self: deme._deme.DEMSolver) -> None + + Disable the use of adaptive max update step count (always use initial update frequency) + + +.. py:method:: DEMSolver.DisableAllMeshWearModels + + .. code-block:: text + + DisableAllMeshWearModels(self: deme._deme.DEMSolver) -> None + + Disable all active mesh wear models. + + +.. py:method:: DEMSolver.DisableContactBetweenFamilies + + .. code-block:: text + + DisableContactBetweenFamilies(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None + + Instruct the solver that the 2 input families should not have contacts (a.k.a. ignored, if such a pair is encountered in contact detection). These 2 families can be the same (which means no contact within members of that family). + + +.. py:method:: DEMSolver.DisableFamilyOutput + + .. code-block:: text + + DisableFamilyOutput(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Prevent entites associated with this family to be outputted to files. + + +.. py:method:: DEMSolver.DisableJitifyClumpTemplates + + .. code-block:: text + + DisableJitifyClumpTemplates(self: deme._deme.DEMSolver) -> None + + Disable jitification of clump templates (use flattened arrays instead). + + +.. py:method:: DEMSolver.DisableJitifyMassProperties + + .. code-block:: text + + DisableJitifyMassProperties(self: deme._deme.DEMSolver) -> None + + Disable jitification of mass properties (use flattened arrays instead). + + +.. py:method:: DEMSolver.DisableMeshWearModel + + .. code-block:: text + + DisableMeshWearModel(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Disable mesh wear model for one owner. + + +.. py:method:: DEMSolver.DisableTrianglePVTracking + + .. code-block:: text + + DisableTrianglePVTracking(self: deme._deme.DEMSolver) -> None + + Disable per-triangle P/V/PxV tracking and clear tracking state. + + +.. py:method:: DEMSolver.DoDynamics + + .. code-block:: text + + DoDynamics(self: deme._deme.DEMSolver, duration: typing.SupportsFloat | typing.SupportsIndex) -> None + + Advance by ``duration`` seconds without forcing the dynamic and kinematic workers to synchronize at the call boundary. This supports short calls and co-simulation; synchronize before immediately reading state. + + +.. py:method:: DEMSolver.DoDynamicsThenSync + + .. code-block:: text + + DoDynamicsThenSync(self: deme._deme.DEMSolver, duration: typing.SupportsFloat | typing.SupportsIndex) -> None + + Advance by ``duration`` seconds, then synchronize the dynamic and kinematic workers. Use this before immediately querying or modifying runtime state. + + +.. py:method:: DEMSolver.DoStepDynamics + + .. code-block:: text + + DoStepDynamics(self: deme._deme.DEMSolver) -> None + + Advance one current timestep without forcing a final dynamic/kinematic worker synchronization. + + +.. py:method:: DEMSolver.Duplicate + + .. code-block:: text + + Duplicate(*args, **kwargs) + Overloaded function. + + 1. Duplicate(self: deme._deme.DEMSolver, arg0: deme::DEMMaterial) -> deme::DEMMaterial + + Duplicate a material that is loaded into the system. + + 2. Duplicate(self: deme._deme.DEMSolver, arg0: deme::DEMClumpTemplate) -> deme::DEMClumpTemplate + + Duplicate a clump template that is loaded into the system. + + 3. Duplicate(self: deme._deme.DEMSolver, arg0: deme::DEMClumpBatch) -> deme::DEMClumpBatch + + Duplicate a batch of clumps that is loaded into the system. + + +.. py:method:: DEMSolver.EnableContactBetweenFamilies + + .. code-block:: text + + EnableContactBetweenFamilies(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None + + Re-enable contact between 2 families after the system is initialized. + + +.. py:method:: DEMSolver.EnableContactWildcardOutput + + .. code-block:: text + + EnableContactWildcardOutput(self: deme._deme.DEMSolver, enable: bool = True) -> None + + Enable or disable contact wildcard output. + + +.. py:method:: DEMSolver.EnableMeshPatchColorOutput + + .. code-block:: text + + EnableMeshPatchColorOutput(self: deme._deme.DEMSolver, enable: bool = True) -> None + + Enable or disable patch color metadata in PLY mesh output. + + +.. py:method:: DEMSolver.EnableMeshWearModel + + .. code-block:: text + + EnableMeshWearModel(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, wear_rate: typing.SupportsFloat | typing.SupportsIndex, update_interval: typing.SupportsFloat | typing.SupportsIndex, start_time: typing.SupportsFloat | typing.SupportsIndex = 0.0, end_time: typing.SupportsFloat | typing.SupportsIndex = -1.0, normal_sign: typing.SupportsFloat | typing.SupportsIndex = -1.0) -> None + + Enable a mesh wear model driven by per-triangle P*V. + + +.. py:method:: DEMSolver.EnableOwnerWildcardOutput + + .. code-block:: text + + EnableOwnerWildcardOutput(self: deme._deme.DEMSolver, enable: bool = True) -> None + + Enable or disable owner wildcard output. + + +.. py:method:: DEMSolver.EnsureKernelErrMsgLineNum + + .. code-block:: text + + EnsureKernelErrMsgLineNum(self: deme._deme.DEMSolver, flag: bool = True) -> None + + If true, each jitification string substitution will do a one-liner to one-liner replacement, so that if the kernel compilation fails, the error meessage line number will reflex the actual spot where that happens (instead of some random number). + + +.. py:method:: DEMSolver.FlushMeshWearModels + + .. code-block:: text + + FlushMeshWearModels(self: deme._deme.DEMSolver) -> None + + Force-apply any pending wear deformation immediately. + + +.. py:method:: DEMSolver.GetAllOwnerWildcardValue + + .. code-block:: text + + GetAllOwnerWildcardValue(self: deme._deme.DEMSolver, arg0: str) -> list[float] + + Get the owner wildcard's values of all entities. + + +.. py:method:: DEMSolver.GetAvgSphContacts + + .. code-block:: text + + GetAvgSphContacts(self: deme._deme.DEMSolver) -> float + + Get the current number of contacts each sphere has + + +.. py:method:: DEMSolver.GetBinNum + + .. code-block:: text + + GetBinNum(self: deme._deme.DEMSolver) -> int + + Get the current number of bins (for contact detection). Must be called from synchronized stance. + + +.. py:method:: DEMSolver.GetBinSize + + .. code-block:: text + + GetBinSize(self: deme._deme.DEMSolver) -> float + + Get the current bin (for contact detection) size. Must be called from synchronized stance. + + +.. py:method:: DEMSolver.GetCachedMesh + + .. code-block:: text + + GetCachedMesh(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> deme::DEMMesh + + Get a handle for the mesh this tracker is tracking. + + +.. py:method:: DEMSolver.GetClumpContacts + + .. code-block:: text + + GetClumpContacts(*args, **kwargs) + Overloaded function. + + 1. GetClumpContacts(self: deme._deme.DEMSolver) -> list[tuple[int, int]] + + Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. + + 2. GetClumpContacts(self: deme._deme.DEMSolver, arg0: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex]) -> list[tuple[int, int]] + + Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to include only these families in the output. + + 3. GetClumpContacts(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[tuple[typing.SupportsInt | typing.SupportsIndex, typing.SupportsInt | typing.SupportsIndex]]) -> list[tuple[int, int]] + + Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. + + +.. py:method:: DEMSolver.GetClumpPositionsHandover + + .. code-block:: text + + GetClumpPositionsHandover(self: deme._deme.DEMSolver) -> list[float3] + + Get all clump-owner center positions ordered by owner ID. + + +.. py:method:: DEMSolver.GetCombinedInstanceInfo + + .. code-block:: text + + GetCombinedInstanceInfo(self: deme._deme.DEMSolver, combined_instance_id: typing.SupportsInt | typing.SupportsIndex) -> tuple[bool, int, list[int], list[float3], list[float4]] + + Query combined-instance metadata; returns (ok, master_owner_id, member_owner_ids, member_rel_pos, member_rel_oriQ). + + +.. py:method:: DEMSolver.GetContactDetailedInfo + + .. code-block:: text + + GetContactDetailedInfo(self: deme._deme.DEMSolver, force_thres: typing.SupportsFloat | typing.SupportsIndex = -1.0) -> deme._deme.ContactInfoContainer + + Get all contact pairs' detailed information (actual content based on the setting with SetContactOutputContent; default are owner IDs, contact point location, contact force, and associated wildcard values) in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. + + +.. py:method:: DEMSolver.GetContactForceModel + + .. code-block:: text + + GetContactForceModel(self: deme._deme.DEMSolver) -> deme._deme.DEMForceModel + + Get the current force model + + +.. py:method:: DEMSolver.GetContacts + + .. code-block:: text + + GetContacts(*args, **kwargs) + Overloaded function. + + 1. GetContacts(self: deme._deme.DEMSolver) -> list[tuple[int, int]] + + Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. + + 2. GetContacts(self: deme._deme.DEMSolver, arg0: collections.abc.Set[typing.SupportsInt | typing.SupportsIndex]) -> list[tuple[int, int]] + + Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to include only these families in the output. + + 3. GetContacts(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[tuple[typing.SupportsInt | typing.SupportsIndex, typing.SupportsInt | typing.SupportsIndex]]) -> list[tuple[int, int]] + + Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential contacts (not necessarily confirmed contacts), meaning they are similar to what WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. + + +.. py:method:: DEMSolver.GetCudaDebugSync + + .. code-block:: text + + GetCudaDebugSync(self: deme._deme.DEMSolver) -> bool + + Return whether process-wide CUDA debug synchronization is enabled. + + +.. py:method:: DEMSolver.GetDeviceMemUsageDynamic + + .. code-block:: text + + GetDeviceMemUsageDynamic(self: deme._deme.DEMSolver) -> int + + Get the device memory usage (in bytes) on dT. + + +.. py:method:: DEMSolver.GetDeviceMemUsageKinematic + + .. code-block:: text + + GetDeviceMemUsageKinematic(self: deme._deme.DEMSolver) -> int + + Get the device memory usage (in bytes) on kT. + + +.. py:method:: DEMSolver.GetFamilyOwnerWildcardValue + + .. code-block:: text + + GetFamilyOwnerWildcardValue(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: str) -> list[float] + + Get the owner wildcard's values of all entities in family N. + + +.. py:method:: DEMSolver.GetGPUDeviceIDs + + .. code-block:: text + + GetGPUDeviceIDs(self: deme._deme.DEMSolver) -> list[int] + + Return ``[dynamic_worker_device, kinematic_worker_device]`` using process-visible logical CUDA IDs. + + +.. py:method:: DEMSolver.GetGPUTimersEnabled + + .. code-block:: text + + GetGPUTimersEnabled(self: deme._deme.DEMSolver) -> bool + + Return whether event-based GPU timing is enabled. + + +.. py:method:: DEMSolver.GetHostMemUsageDynamic + + .. code-block:: text + + GetHostMemUsageDynamic(self: deme._deme.DEMSolver) -> int + + Get the host memory usage (in bytes) on dT. + + +.. py:method:: DEMSolver.GetHostMemUsageKinematic + + .. code-block:: text + + GetHostMemUsageKinematic(self: deme._deme.DEMSolver) -> int + + Get the host memory usage (in bytes) on kT. + + +.. py:method:: DEMSolver.GetInitStatus + + .. code-block:: text + + GetInitStatus(self: deme._deme.DEMSolver) -> bool + + Return whether this simulation system is initialized. + + +.. py:method:: DEMSolver.GetJitifyOptions + + .. code-block:: text + + GetJitifyOptions(self: deme._deme.DEMSolver) -> list[str] + + Get current jitification options. It is needed by some of this simulation system's friend classes. + + +.. py:method:: DEMSolver.GetJitStringSubs + + .. code-block:: text + + GetJitStringSubs(self: deme._deme.DEMSolver) -> dict[str, str] + + Get the jitification string substitution laundary list. It is needed by some of this simulation system's friend classes. + + +.. py:method:: DEMSolver.GetMeshNodesGlobal + + .. code-block:: text + + GetMeshNodesGlobal(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> list[float3] + + Get the current locations of all the nodes in the mesh being tracked. + + +.. py:method:: DEMSolver.GetNumClumps + + .. code-block:: text + + GetNumClumps(self: deme._deme.DEMSolver) -> int + + Return the number of clumps that are currently in the simulation. Must be used after initialization. + + +.. py:method:: DEMSolver.GetNumCombinedInstances + + .. code-block:: text + + GetNumCombinedInstances(self: deme._deme.DEMSolver) -> int + + Return the number of combined instances currently cached. + + +.. py:method:: DEMSolver.GetNumContacts + + .. code-block:: text + + GetNumContacts(self: deme._deme.DEMSolver) -> int + + Get the number of kT-reported potential contact pairs. + + +.. py:method:: DEMSolver.GetNumOwners + + .. code-block:: text + + GetNumOwners(self: deme._deme.DEMSolver) -> int + + Return the total number of owners (clumps + meshes + analytical objects) that are currently in the simulation. Must be used after initialization. + + +.. py:method:: DEMSolver.GetOwnerAcc + + .. code-block:: text + + GetOwnerAcc(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get the acceleration of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerAngAcc + + .. code-block:: text + + GetOwnerAngAcc(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get the angular acceleration of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerAngVel + + .. code-block:: text + + GetOwnerAngVel(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get angular velocity of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerContactClumps + + .. code-block:: text + + GetOwnerContactClumps(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> list[int] + + Get the clumps that are in contact with this owner as a vector. + + +.. py:method:: DEMSolver.GetOwnerContactForces + + .. code-block:: text + + GetOwnerContactForces(*args, **kwargs) + Overloaded function. + + 1. GetOwnerContactForces(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], arg1: collections.abc.Sequence[float3], arg2: collections.abc.Sequence[float3]) -> int + + Get all contact forces that concern a list of owners. + + 2. GetOwnerContactForces(self: deme._deme.DEMSolver, ownerIDs: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], torque_in_local: bool = False) -> int + + Get all contact forces and torque that concern a list of owners. + + +.. py:method:: DEMSolver.GetOwnerFamily + + .. code-block:: text + + GetOwnerFamily(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[int] + + Get the family number of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerMass + + .. code-block:: text + + GetOwnerMass(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float] + + Get the mass of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerMOI + + .. code-block:: text + + GetOwnerMOI(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get the moment of inertia (in principal axis frame) of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerOriQ + + .. code-block:: text + + GetOwnerOriQ(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float4] + + Get quaternion of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerPosition + + .. code-block:: text + + GetOwnerPosition(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get position of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerVelocity + + .. code-block:: text + + GetOwnerVelocity(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float3] + + Get velocity of n consecutive owners. + + +.. py:method:: DEMSolver.GetOwnerWildcardValue + + .. code-block:: text + + GetOwnerWildcardValue(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, name: str, n: typing.SupportsInt | typing.SupportsIndex = 1) -> list[float] + + Get the owner wildcard's values of some owners. + + +.. py:method:: DEMSolver.GetSimTime + + .. code-block:: text + + GetSimTime(self: deme._deme.DEMSolver) -> float + + Get the simulation time passed since the start of simulation. + + +.. py:method:: DEMSolver.GetTimeStepSize + + .. code-block:: text + + GetTimeStepSize(self: deme._deme.DEMSolver) -> float + + Get the current time step size in simulation. + + +.. py:method:: DEMSolver.GetTrackedOwnerTrianglePV + + .. code-block:: text + + GetTrackedOwnerTrianglePV(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, reset_window: bool = True) -> tuple[bool, list[float], list[float], list[float]] + + Get frame-window averaged per-triangle P, V and P*V for one tracked owner. + + +.. py:method:: DEMSolver.GetUpdateFreq + + .. code-block:: text + + GetUpdateFreq(self: deme._deme.DEMSolver) -> float + + Get the current update frequency used by the solver. + + +.. py:method:: DEMSolver.GetWhetherForceCollectInKernel + + .. code-block:: text + + GetWhetherForceCollectInKernel(self: deme._deme.DEMSolver) -> bool + + Return whether the solver is currently reducing force in the force calculation kernel. + + +.. py:method:: DEMSolver.Initialize + + .. code-block:: text + + Initialize(self: deme._deme.DEMSolver, dry_run: bool = False) -> None + + Finalize cached setup data, allocate runtime state, and prepare JIT CUDA kernels. Call once after materials, geometry, particles, and solver options have been configured. ``dry_run=True`` performs initialization without starting normal dynamics. + + +.. py:method:: DEMSolver.InstructBoxDomainBoundingBC + + .. code-block:: text + + InstructBoxDomainBoundingBC(self: deme._deme.DEMSolver, arg0: str, arg1: deme::DEMMaterial) -> None + + Instruct if and how we should add boundaries to the simulation world upon initialization. Choose between `none', `all' (add 6 boundary planes) and `top_open' (add 5 boundary planes and leave the z-directon top open). Also specifies the material that should be assigned to those bounding boundaries. + + +.. py:method:: DEMSolver.InstructBoxDomainDimension + + .. code-block:: text + + InstructBoxDomainDimension(*args, **kwargs) + Overloaded function. + + 1. InstructBoxDomainDimension(self: deme._deme.DEMSolver, x: typing.SupportsFloat | typing.SupportsIndex, y: typing.SupportsFloat | typing.SupportsIndex, z: typing.SupportsFloat | typing.SupportsIndex, dir_exact: str = 'none') -> None + + Set the Box Domain Dimension + + 2. InstructBoxDomainDimension(self: deme._deme.DEMSolver, x: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex], y: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex], z: tuple[typing.SupportsFloat | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex], dir_exact: str = 'none') -> None + + Set the span of the Box Domain + + +.. py:method:: DEMSolver.InstructNumOwners + + .. code-block:: text + + InstructNumOwners(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Explicitly instruct the sizes for the arrays at initialization time. This is useful when the number of owners tends to change (especially gradually increase) frequently in the simulation. + + +.. py:method:: DEMSolver.LoadClumpType + + .. code-block:: text + + LoadClumpType(*args, **kwargs) + Overloaded function. + + 1. LoadClumpType(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex, arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: str, arg3: deme::DEMMaterial) -> deme::DEMClumpTemplate + + Load a clump type into the API-level cache + + 2. LoadClumpType(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex, arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg3: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]], arg4: deme::DEMMaterial) -> deme::DEMClumpTemplate + + Load a clump type into the API-level cache + + 3. LoadClumpType(self: deme._deme.DEMSolver, arg0: deme::DEMClumpTemplate) -> deme::DEMClumpTemplate + + Load a clump type into the API-level cache + + 4. LoadClumpType(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex, arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: str, arg3: collections.abc.Sequence[deme::DEMMaterial]) -> deme::DEMClumpTemplate + + Load a clump type into the API-level cache + + 5. LoadClumpType(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex, arg1: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], arg2: str, arg3: deme::DEMMaterial) -> deme::DEMClumpTemplate + + Load a clump type into the API-level cache + + +.. py:method:: DEMSolver.LoadCombinedClumpType + + .. code-block:: text + + LoadCombinedClumpType(self: deme._deme.DEMSolver, component_templates: collections.abc.Sequence[deme::DEMClumpTemplate], component_rel_pos: collections.abc.Sequence[float3], component_rel_oriQ: collections.abc.Sequence[float4] = [], master_component: typing.SupportsInt | typing.SupportsIndex = 0) -> deme::DEMCombinedTemplate + + Load a rigid combined-clump template with fixed member-relative transforms. + + +.. py:method:: DEMSolver.LoadCombinedMeshType + + .. code-block:: text + + LoadCombinedMeshType(self: deme._deme.DEMSolver, component_templates: collections.abc.Sequence[deme::DEMMesh], component_rel_pos: collections.abc.Sequence[float3], component_rel_oriQ: collections.abc.Sequence[float4] = [], master_component: typing.SupportsInt | typing.SupportsIndex = 0) -> deme::DEMCombinedTemplate + + Load a rigid combined-mesh template with fixed member-relative transforms. + + +.. py:method:: DEMSolver.LoadMaterial + + .. code-block:: text + + LoadMaterial(*args, **kwargs) + Overloaded function. + + 1. LoadMaterial(self: deme._deme.DEMSolver, arg0: collections.abc.Mapping[str, typing.SupportsFloat | typing.SupportsIndex]) -> deme::DEMMaterial + + Load materials properties (Young's modulus, Poisson's ratio...) into the system. + + 2. LoadMaterial(self: deme._deme.DEMSolver, arg0: deme::DEMMaterial) -> deme::DEMMaterial + + Load materials properties into the system. + + +.. py:method:: DEMSolver.LoadMeshType + + .. code-block:: text + + LoadMeshType(*args, **kwargs) + Overloaded function. + + 1. LoadMeshType(self: deme._deme.DEMSolver, filename: str, mat: deme::DEMMaterial, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a mesh template into cache so it can be instantiated repeatedly. + + 2. LoadMeshType(self: deme._deme.DEMSolver, filename: str, load_normals: bool = True, load_uv: bool = False) -> deme::DEMMesh + + Load a mesh template into cache (without explicitly assigning material here). + + 3. LoadMeshType(self: deme._deme.DEMSolver, mesh: deme::DEMMesh) -> deme::DEMMesh + + Load a user-constructed mesh object as a reusable template. + + +.. py:method:: DEMSolver.LoadSphereType + + .. code-block:: text + + LoadSphereType(*args, **kwargs) + Overloaded function. + + 1. LoadSphereType(self: deme._deme.DEMSolver, mass: typing.SupportsFloat | typing.SupportsIndex, radius: typing.SupportsFloat | typing.SupportsIndex, material: deme::DEMMaterial) -> deme::DEMClumpTemplate + + Load a reusable one-sphere clump template, deriving its spherical moment of inertia. + + 2. LoadSphereType(self: deme._deme.DEMSolver, mass: typing.SupportsFloat | typing.SupportsIndex, moi: typing.SupportsFloat | typing.SupportsIndex, radius: typing.SupportsFloat | typing.SupportsIndex, material: deme::DEMMaterial) -> deme::DEMClumpTemplate + + Load a reusable one-sphere clump template with an explicitly supplied scalar moment of inertia. + + +.. py:method:: DEMSolver.MarkFamilyPersistentContact + + .. code-block:: text + + MarkFamilyPersistentContact(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None + + Make it so that if for any currently-existing contact, if its two contact geometries are in family N1 and N2 respectively, this contact will never be removed. + + +.. py:method:: DEMSolver.MarkFamilyPersistentContactBoth + + .. code-block:: text + + MarkFamilyPersistentContactBoth(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Make it so that for any currently-existing contact, if both of its contact geometries are in family N, then this contact will never be removed. + + +.. py:method:: DEMSolver.MarkFamilyPersistentContactEither + + .. code-block:: text + + MarkFamilyPersistentContactEither(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Make it so that for any currently-existing contact, if one of its contact geometries is in family N, then this contact will never be removed. + + +.. py:method:: DEMSolver.MarkPersistentContact + + .. code-block:: text + + MarkPersistentContact(self: deme._deme.DEMSolver) -> None + + Make it so that all currently-existing contacts in this simulation will never be removed. + + +.. py:method:: DEMSolver.PrintKinematicScratchSpaceUsage + + .. code-block:: text + + PrintKinematicScratchSpaceUsage(self: deme._deme.DEMSolver) -> None + + Print kT's scratch space usage. This is a debug method. + + +.. py:method:: DEMSolver.PurgeFamily + + .. code-block:: text + + PurgeFamily(self: deme._deme.DEMSolver, family_num: typing.SupportsInt | typing.SupportsIndex) -> None + + Remove all entities in a family from runtime arrays to reclaim memory. + + +.. py:method:: DEMSolver.ReadContactForceModel + + .. code-block:: text + + ReadContactForceModel(self: deme._deme.DEMSolver, arg0: str) -> deme._deme.DEMForceModel + + Read user custom contact force model from a file (which by default should reside in kernel/DEMUserScripts). Returns a pointer to the force model in use. + + +.. py:method:: DEMSolver.ReleaseFlattenedArrays + + .. code-block:: text + + ReleaseFlattenedArrays(self: deme._deme.DEMSolver) -> None + + Release setup-time flattened arrays used for preprocessing and worker transfer. + + +.. py:method:: DEMSolver.RemoveFamilyPersistentContact + + .. code-block:: text + + RemoveFamilyPersistentContact(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None + + Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContact. + + +.. py:method:: DEMSolver.RemoveFamilyPersistentContactBoth + + .. code-block:: text + + RemoveFamilyPersistentContactBoth(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactBoth. + + +.. py:method:: DEMSolver.RemoveFamilyPersistentContactEither + + .. code-block:: text + + RemoveFamilyPersistentContactEither(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactEither. + + +.. py:method:: DEMSolver.RemoveKernelInclude + + .. code-block:: text + + RemoveKernelInclude(self: deme._deme.DEMSolver) -> None + + Remove all extra libraries that the kernels `include' in their headers. + + +.. py:method:: DEMSolver.RemovePersistentContact + + .. code-block:: text + + RemovePersistentContact(self: deme._deme.DEMSolver) -> None + + Cancel contact persistence qualification. Work like the inverse of MarkPersistentContact. + + +.. py:method:: DEMSolver.RequestContactUpdate + + .. code-block:: text + + RequestContactUpdate(self: deme._deme.DEMSolver) -> None + + Request an immediate contact detection update. + + +.. py:method:: DEMSolver.SetAdaptiveBinSizeAcc + + .. code-block:: text + + SetAdaptiveBinSizeAcc(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set how fast kT changes the direction of bin size adjustmemt when there's a more beneficial direction + + +.. py:method:: DEMSolver.SetAdaptiveBinSizeDelaySteps + + .. code-block:: text + + SetAdaptiveBinSizeDelaySteps(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Adjust how frequent kT updates the bin size + + +.. py:method:: DEMSolver.SetAdaptiveBinSizeLowerProactivity + + .. code-block:: text + + SetAdaptiveBinSizeLowerProactivity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set how proactive the solver is in avoiding the bin being too small (leading to too many bins in domain). + + +.. py:method:: DEMSolver.SetAdaptiveBinSizeMaxRate + + .. code-block:: text + + SetAdaptiveBinSizeMaxRate(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the max rate that the bin size can change in one adjustment + + +.. py:method:: DEMSolver.SetAdaptiveBinSizeUpperProactivity + + .. code-block:: text + + SetAdaptiveBinSizeUpperProactivity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a bin) + + +.. py:method:: DEMSolver.SetAdaptiveTimeStepType + + .. code-block:: text + + SetAdaptiveTimeStepType(self: deme._deme.DEMSolver, arg0: str) -> None + + Set the strategy for auto-adapting time step size. Currently, hertz_const computes one fixed setup-time timestep; max_vel and int_diff are accepted but not implemented. + + +.. py:method:: DEMSolver.SetAllowIntraCombinedOwnerContacts + + .. code-block:: text + + SetAllowIntraCombinedOwnerContacts(self: deme._deme.DEMSolver, allow: bool = True) -> None + + Allow or suppress contacts among owners belonging to the same combined owner group. + + +.. py:method:: DEMSolver.SetCDMaxUpdateFreq + + .. code-block:: text + + SetCDMaxUpdateFreq(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the upper bound of kT update frequency (when it is adjusted automatically). + + +.. py:method:: DEMSolver.SetCDNumStepsMaxDriftAheadOfAvg + + .. code-block:: text + + SetCDNumStepsMaxDriftAheadOfAvg(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the number of steps dT configures its max drift more than average drift steps. + + +.. py:method:: DEMSolver.SetCDNumStepsMaxDriftHistorySize + + .. code-block:: text + + SetCDNumStepsMaxDriftHistorySize(self: deme._deme.DEMSolver, n: typing.SupportsInt | typing.SupportsIndex) -> None + + Set how many past kinematic-worker updates calibrate the maximum future drift limit. The default is recommended for normal use. + + +.. py:method:: DEMSolver.SetCDNumStepsMaxDriftMultipleOfAvg + + .. code-block:: text + + SetCDNumStepsMaxDriftMultipleOfAvg(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the multiplier which dT configures its max drift to be w.r.t. the average drift steps. + + +.. py:method:: DEMSolver.SetCDUpdateFreq + + .. code-block:: text + + SetCDUpdateFreq(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the number of dT steps before it waits for a contact-pair info update from kT. + + +.. py:method:: DEMSolver.SetCollectAccRightAfterForceCalc + + .. code-block:: text + + SetCollectAccRightAfterForceCalc(self: deme._deme.DEMSolver, flag: bool = True) -> None + + Reduce contact forces to accelerations right after calculating them, in the same kernel. This may give some performance boost if you have only polydisperse spheres, no clumps. + + +.. py:method:: DEMSolver.SetContactOutputContent + + .. code-block:: text + + SetContactOutputContent(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[str]) -> None + + Specify the information that needs to go into the contact pair output files. + + +.. py:method:: DEMSolver.SetContactOutputFormat + + .. code-block:: text + + SetContactOutputFormat(self: deme._deme.DEMSolver, arg0: str) -> None + + Specify the file format of contact pairs. + + +.. py:method:: DEMSolver.SetContactWildcards + + .. code-block:: text + + SetContactWildcards(self: deme._deme.DEMSolver, arg0: collections.abc.Set[str]) -> None + + Set the names for the extra quantities that will be associated with each contact pair. + + +.. py:method:: DEMSolver.SetContactWildcardValue + + .. code-block:: text + + SetContactWildcardValue(self: deme._deme.DEMSolver, arg0: str, arg1: typing.SupportsFloat | typing.SupportsIndex) -> None + + Change the value of contact wildcards to val. Apply to all simulation bodies that are present. + + +.. py:method:: DEMSolver.SetCudaDebugSync + + .. code-block:: text + + SetCudaDebugSync(self: deme._deme.DEMSolver, enable: bool = True) -> None + + Set the process-wide switch that synchronizes after each CUDA operation that enqueues stream work. Disable for normal asynchronous performance. + + +.. py:method:: DEMSolver.SetErrorOutAngularVelocity + + .. code-block:: text + + SetErrorOutAngularVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the angular velocity which when exceeded, the solver errors out. A huge number can be used to discourage this error type. Defaulted to 1e4. + + +.. py:method:: DEMSolver.SetErrorOutAvgContacts + + .. code-block:: text + + SetErrorOutAvgContacts(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the average number of contacts a sphere has, before the solver errors out. A huge number can be used to discourage this error type. Defaulted to 100 + + +.. py:method:: DEMSolver.SetErrorOutVelocity + + .. code-block:: text + + SetErrorOutVelocity(*args, **kwargs) + Overloaded function. + + 1. SetErrorOutVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage this error type. Defaulted to 1e3. + + 2. SetErrorOutVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage this error type. Defaulted to 5e4. + + +.. py:method:: DEMSolver.SetExpandFactor + + .. code-block:: text + + SetExpandFactor(self: deme._deme.DEMSolver, beta: typing.SupportsFloat | typing.SupportsIndex, fix: bool = True) -> None + + (Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are expanded for the purpose of contact detection (safe, and creates false positives). If fix is set to true, then this expand factor does not change even if the user uses variable time step size. + + +.. py:method:: DEMSolver.SetExpandSafetyAdder + + .. code-block:: text + + SetExpandSafetyAdder(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving the thickness of the contact `safety' margin + + +.. py:method:: DEMSolver.SetExpandSafetyMultiplier + + .. code-block:: text + + SetExpandSafetyMultiplier(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the contact `safety' margin. + + +.. py:method:: DEMSolver.SetExpandSafetyType + + .. code-block:: text + + SetExpandSafetyType(self: deme._deme.DEMSolver, arg0: str) -> None + + A string. If 'auto': the solver automatically derives. + + +.. py:method:: DEMSolver.SetFamilyClumpMaterial + + .. code-block:: text + + SetFamilyClumpMaterial(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: deme::DEMMaterial) -> None + + Set all clumps in this family to have this material. + + +.. py:method:: DEMSolver.SetFamilyContactWildcardValue + + .. code-block:: text + + SetFamilyContactWildcardValue(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: str, arg3: typing.SupportsFloat | typing.SupportsIndex) -> None + + Change the value of contact wildcards to val if one of the contact geometry is in family N1, and the other is in N2. + + +.. py:method:: DEMSolver.SetFamilyContactWildcardValueBoth + + .. code-block:: text + + SetFamilyContactWildcardValueBoth(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: str, arg2: typing.SupportsFloat | typing.SupportsIndex) -> None + + Change the value of contact wildcards to val if both of the contact geometries are in family N. + + +.. py:method:: DEMSolver.SetFamilyContactWildcardValueEither + + .. code-block:: text + + SetFamilyContactWildcardValueEither(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: str, arg2: typing.SupportsFloat | typing.SupportsIndex) -> None + + Change the value of contact wildcards to val if either of the contact geometries is in family N. + + +.. py:method:: DEMSolver.SetFamilyExtraMargin + + .. code-block:: text + + SetFamilyExtraMargin(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsFloat | typing.SupportsIndex) -> None + + Add an extra contact margin to entities in a family so they are registered as potential contact pairs earlier. + + +.. py:method:: DEMSolver.SetFamilyFixed + + .. code-block:: text + + SetFamilyFixed(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Mark all entities in this family to be fixed. + + +.. py:method:: DEMSolver.SetFamilyMeshMaterial + + .. code-block:: text + + SetFamilyMeshMaterial(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: deme::DEMMaterial) -> None + + Set all meshes in this family to have this material. + + +.. py:method:: DEMSolver.SetFamilyOwnerWildcardValue + + .. code-block:: text + + SetFamilyOwnerWildcardValue(*args, **kwargs) + Overloaded function. + + 1. SetFamilyOwnerWildcardValue(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: str, arg2: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Modify the owner wildcard's values of all entities in family N. + + 2. SetFamilyOwnerWildcardValue(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: str, arg2: typing.SupportsFloat | typing.SupportsIndex) -> None + + Modify the owner wildcard's values of all entities in family N. + + +.. py:method:: DEMSolver.SetFamilyPrescribedAngVel + + .. code-block:: text + + SetFamilyPrescribedAngVel(*args, **kwargs) + Overloaded function. + + 1. SetFamilyPrescribedAngVel(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, velX: str, velY: str, velZ: str, dictate: bool = True, pre: str = 'none') -> None + + Set the prescribed angular velocity to all entities in a family. If dictate is set to true, then this family will not be influenced by the force exerted from other simulation entites (both linear and rotational motions). If false, only specified components (that is, not specified with 'none') will not be influenced by the force exerted from other simulation entites. + + 2. SetFamilyPrescribedAngVel(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the angular velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedAngVelX + + .. code-block:: text + + SetFamilyPrescribedAngVelX(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the X component of the angular velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedAngVelY + + .. code-block:: text + + SetFamilyPrescribedAngVelY(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the X component of the angular velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedAngVelZ + + .. code-block:: text + + SetFamilyPrescribedAngVelZ(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the X component of the angular velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedLinVel + + .. code-block:: text + + SetFamilyPrescribedLinVel(*args, **kwargs) + Overloaded function. + + 1. SetFamilyPrescribedLinVel(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, velX: str, velY: str, velZ: str, dictate: bool = True, pre: str = 'none') -> None + + Set the prescribed linear velocity to all entities in a family. If dictate is set to true, then this family will not be influenced by the force exerted from other simulation entites (both linear and rotational motions). If false, only specified components (that is, not specified with 'none') will not be influenced by the force exerted from other simulation entites. + + 2. SetFamilyPrescribedLinVel(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the linear velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedLinVelX + + .. code-block:: text + + SetFamilyPrescribedLinVelX(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the X component of the linear velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedLinVelY + + .. code-block:: text + + SetFamilyPrescribedLinVelY(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the Y component of the linear velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedLinVelZ + + .. code-block:: text + + SetFamilyPrescribedLinVelZ(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the Z component of the linear velocities of all entites in this family always keep `as is', and not influenced by the force exerted from other simulation entites. + + +.. py:method:: DEMSolver.SetFamilyPrescribedPosition + + .. code-block:: text + + SetFamilyPrescribedPosition(*args, **kwargs) + Overloaded function. + + 1. SetFamilyPrescribedPosition(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, X: str, Y: str, Z: str, dictate: bool = True, pre: str = 'none') -> None + + Keep the positions of all entites in this family to remain exactly the user-specified values. + + 2. SetFamilyPrescribedPosition(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Keep the positions of all entites in this family to remain as is. + + +.. py:method:: DEMSolver.SetFamilyPrescribedPositionX + + .. code-block:: text + + SetFamilyPrescribedPositionX(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the X component of the linear positions of all entites in this family always keep `as is'. + + +.. py:method:: DEMSolver.SetFamilyPrescribedPositionY + + .. code-block:: text + + SetFamilyPrescribedPositionY(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the Y component of the linear positions of all entites in this family always keep `as is'. + + +.. py:method:: DEMSolver.SetFamilyPrescribedPositionZ + + .. code-block:: text + + SetFamilyPrescribedPositionZ(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the Z component of the linear positions of all entites in this family always keep `as is'. + + +.. py:method:: DEMSolver.SetFamilyPrescribedQuaternion + + .. code-block:: text + + SetFamilyPrescribedQuaternion(*args, **kwargs) + Overloaded function. + + 1. SetFamilyPrescribedQuaternion(self: deme._deme.DEMSolver, ID: typing.SupportsInt | typing.SupportsIndex, q_formula: str, dictate: bool = True) -> None + + Keep the orientation quaternions of all entites in this family to remain exactly the user-specified values. + + 2. SetFamilyPrescribedQuaternion(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Let the orientation quaternions of all entites in this family always keep `as is'. + + +.. py:method:: DEMSolver.SetForceCalcThreadsPerBlock + + .. code-block:: text + + SetForceCalcThreadsPerBlock(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the number of threads per block in force calculation (default 256). + + +.. py:method:: DEMSolver.SetGPUTimersEnabled + + .. code-block:: text + + SetGPUTimersEnabled(self: deme._deme.DEMSolver, enabled: bool) -> None + + Enable or disable event-based GPU timing. + + +.. py:method:: DEMSolver.SetGravitationalAcceleration + + .. code-block:: text + + SetGravitationalAcceleration(self: deme._deme.DEMSolver, acc: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the global gravitational acceleration as ``[x, y, z]`` in length/time^2 units. + + +.. py:method:: DEMSolver.SetInitBinNumTarget + + .. code-block:: text + + SetInitBinNumTarget(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the target number of bins (for contact detection) at the start of the simulation upon initialization. + + +.. py:method:: DEMSolver.SetInitBinSize + + .. code-block:: text + + SetInitBinSize(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Explicitly instruct the bin size (for contact detection) that the solver should use. + + +.. py:method:: DEMSolver.SetInitBinSizeAsMultipleOfSmallestSphere + + .. code-block:: text + + SetInitBinSizeAsMultipleOfSmallestSphere(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Explicitly instruct the bin size (for contact detection) that the solver should use, as a multiple of the radius of the smallest sphere in simulation. + + +.. py:method:: DEMSolver.SetInitTimeStep + + .. code-block:: text + + SetInitTimeStep(self: deme._deme.DEMSolver, ts: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the initial timestep in seconds before ``Initialize``. A constant-step simulation uses it throughout; an adaptive strategy may update it later. + + +.. py:method:: DEMSolver.SetIntegrator + + .. code-block:: text + + SetIntegrator(*args, **kwargs) + Overloaded function. + + 1. SetIntegrator(self: deme._deme.DEMSolver, arg0: str) -> None + + Set the time integrator for this simulator. + + 2. SetIntegrator(self: deme._deme.DEMSolver, arg0: deme::TIME_INTEGRATOR) -> None + + Set the time integrator for this simulator. + + +.. py:method:: DEMSolver.SetJitifyClumpTemplates + + .. code-block:: text + + SetJitifyClumpTemplates(self: deme._deme.DEMSolver, use: bool = True) -> None + + Instruct the solver to rearrange and consolidate clump templates information, then jitify it into GPU kernels (if set to true), rather than using flattened sphere component configuration arrays whose entries are associated with individual spheres. + + +.. py:method:: DEMSolver.SetJitifyMassProperties + + .. code-block:: text + + SetJitifyMassProperties(self: deme._deme.DEMSolver, use: bool = True) -> None + + Instruct the solver to rearrange and consolidate mass property information (for all owner types), then jitify it into GPU kernels (if set to true), rather than using flattened mass property arrays whose entries are associated with individual owners. + + +.. py:method:: DEMSolver.SetJitifyOptions + + .. code-block:: text + + SetJitifyOptions(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[str]) -> None + + Set the jitification options. It is only needed by advanced users. + + +.. py:method:: DEMSolver.SetKernelInclude + + .. code-block:: text + + SetKernelInclude(self: deme._deme.DEMSolver, arg0: str) -> None + + Set the kernels' headers' extra include lines. Useful for customization. + + +.. py:method:: DEMSolver.SetMaterialPropertyPair + + .. code-block:: text + + SetMaterialPropertyPair(self: deme._deme.DEMSolver, arg0: str, arg1: deme::DEMMaterial, arg2: deme::DEMMaterial, arg3: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the value for a material property that by nature involves a pair of a materials (e.g. friction coefficient). + + +.. py:method:: DEMSolver.SetMaxSphereInBin + + .. code-block:: text + + SetMaxSphereInBin(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Used to force the solver to error out when there are too many spheres in a bin. A huge number can be used to discourage this error type + + +.. py:method:: DEMSolver.SetMaxTriangleInBin + + .. code-block:: text + + SetMaxTriangleInBin(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex) -> None + + Used to force the solver to error out when there are too many spheres in a bin. A huge number can be used to discourage this error type + + +.. py:method:: DEMSolver.SetMaxTriTriPenetration + + .. code-block:: text + + SetMaxTriTriPenetration(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the maximum allowed triangle-triangle penetration used as the margin added in kT contact detection. This value caps the penetration margin added to prevent excessively large values. + + +.. py:method:: DEMSolver.SetMaxVelocity + + .. code-block:: text + + SetMaxVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the maximum expected particle velocity. The solver will not use a velocity larger than this for determining the margin thickness, and velocity larger than this will be considered a system anomaly. + + +.. py:method:: DEMSolver.SetMeshOutputFormat + + .. code-block:: text + + SetMeshOutputFormat(self: deme._deme.DEMSolver, arg0: str) -> None + + Specify the output file format of meshes. + + +.. py:method:: DEMSolver.SetMeshParticlesLowPoly + + .. code-block:: text + + SetMeshParticlesLowPoly(self: deme._deme.DEMSolver, use: bool = True) -> None + + Declare that all meshed particles have a low polygon count (e.g. box, tetrahedron). When enabled, the per-triangle maxTriTriPenetration array is neither computed, transferred to kT, nor used to inflate contact-detection margins, saving compute time. Toggle this on only when you are confident that no triangle from one mesh will ever be completely submerged inside another mesh. + + +.. py:method:: DEMSolver.SetMeshUniversalContact + + .. code-block:: text + + SetMeshUniversalContact(self: deme._deme.DEMSolver, use: bool = True) -> None + + Set whether mesh-mesh contacts should be universally detected. Set to false to speedup simulation if meshes are not expected to have contacts. Default is false. + + +.. py:method:: DEMSolver.SetNoForceRecord + + .. code-block:: text + + SetNoForceRecord(self: deme._deme.DEMSolver, flag: bool = True) -> None + + Instruct the solver that there is no need to record the contact force (and contact point location etc.) in an array. + + +.. py:method:: DEMSolver.SetOutputContent + + .. code-block:: text + + SetOutputContent(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[str]) -> None + + Specify the information that needs to go into the clump or sphere output files. + + +.. py:method:: DEMSolver.SetOutputFormat + + .. code-block:: text + + SetOutputFormat(self: deme._deme.DEMSolver, arg0: str) -> None + + Choose sphere and clump output file format. + + +.. py:method:: DEMSolver.SetOwnerAngVel + + .. code-block:: text + + SetOwnerAngVel(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float3]) -> None + + Set angular velocity of consecutive owners starting from ownerID, based on input angular velocity vector. N (the size of the input vector) elements will be modified. + + +.. py:method:: DEMSolver.SetOwnerFamily + + .. code-block:: text + + SetOwnerFamily(self: deme._deme.DEMSolver, ownerID: typing.SupportsInt | typing.SupportsIndex, fam: typing.SupportsInt | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> None + + Set the family number of consecutive owners. + + +.. py:method:: DEMSolver.SetOwnerOriQ + + .. code-block:: text + + SetOwnerOriQ(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float4]) -> None + + Set quaternion of consecutive owners starting from ownerID, based on input quaternion vector. N (the size of the input vector) elements will be modified. + + +.. py:method:: DEMSolver.SetOwnerPosition + + .. code-block:: text + + SetOwnerPosition(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float3]) -> None + + Set position of consecutive owners starting from ownerID, based on input position vector. N (the size of the input vector) elements will be modified. + + +.. py:method:: DEMSolver.SetOwnerVelocity + + .. code-block:: text + + SetOwnerVelocity(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[float3]) -> None + + Set velocity of consecutive owners starting from ownerID, based on input velocity vector. N (the size of the input vector) elements will be modified. + + +.. py:method:: DEMSolver.SetOwnerWildcards + + .. code-block:: text + + SetOwnerWildcards(self: deme._deme.DEMSolver, arg0: collections.abc.Set[str]) -> None + + Set the names for the extra quantities that will be associated with each owner. + + +.. py:method:: DEMSolver.SetOwnerWildcardValue + + .. code-block:: text + + SetOwnerWildcardValue(*args, **kwargs) + Overloaded function. + + 1. SetOwnerWildcardValue(self: deme._deme.DEMSolver, ownerIDs: typing.SupportsInt | typing.SupportsIndex, name: str, vals: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the wildcard values of some owners using a list. + + 2. SetOwnerWildcardValue(self: deme._deme.DEMSolver, ownerIDs: typing.SupportsInt | typing.SupportsIndex, name: str, val: typing.SupportsFloat | typing.SupportsIndex, n: typing.SupportsInt | typing.SupportsIndex = 1) -> None + + Set the wildcard values of some owners using a list. + + +.. py:method:: DEMSolver.SetPersistentContact + + .. code-block:: text + + SetPersistentContact(self: deme._deme.DEMSolver, use: bool = True) -> None + + Set whether the solver should expect the user to mark certain contacts as persistent across kT updates. Set this to true if you later will call MarkPersistentContact series of methods. + + +.. py:method:: DEMSolver.SetSimplePatchCombination + + .. code-block:: text + + SetSimplePatchCombination(self: deme._deme.DEMSolver, use: bool = True) -> None + + Use simple patch-ID based triangle contact combination. Disable to opt into connected-component flooding. + + +.. py:method:: DEMSolver.SetSimTime + + .. code-block:: text + + SetSimTime(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Get the simulation time passed since the start of simulation. + + +.. py:method:: DEMSolver.SetSortContactPairs + + .. code-block:: text + + SetSortContactPairs(self: deme._deme.DEMSolver, arg0: bool) -> None + + Instruct the solver if contact pair arrays should be sorted (based on the types of contacts) before usage. + + +.. py:method:: DEMSolver.SetStablePatchIslandIDs + + .. code-block:: text + + SetStablePatchIslandIDs(self: deme._deme.DEMSolver, use: bool = True) -> None + + Stabilize flooded patch-island IDs across contact-detection steps. + + +.. py:method:: DEMSolver.SetTrianglePVTrackingOwners + + .. code-block:: text + + SetTrianglePVTrackingOwners(self: deme._deme.DEMSolver, arg0: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None + + Enable per-triangle P/V/PxV tracking for the specified mesh owners. + + +.. py:method:: DEMSolver.SetTriNodeRelPos + + .. code-block:: text + + SetTriNodeRelPos(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: collections.abc.Sequence[float3]) -> None + + Rewrite the relative positions of the flattened triangle soup. + + +.. py:method:: DEMSolver.SetTriTriContactRejectionRatio + + .. code-block:: text + + SetTriTriContactRejectionRatio(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the ratio threshold for rejecting suspicious tri-tri contacts. A contact is rejected when the penetration depth exceeds this fraction of the contact-point-to-mesh-center distance for either mesh involved. A negative value disables the guard entirely. + + +.. py:method:: DEMSolver.SetTriTriPenetration + + .. code-block:: text + + SetTriTriPenetration(self: deme._deme.DEMSolver, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None + + Manually set the current triangle-triangle penetration value in dT. This allows the user to directly control the maxTriTriPenetration value which will ONLY be used in the NEXT contact detection run in kT. + + +.. py:method:: DEMSolver.SetUseAngularVelocityMargin + + .. code-block:: text + + SetUseAngularVelocityMargin(self: deme._deme.DEMSolver, arg0: bool) -> None + + Control whether angular velocity contributes to the contact detection margin. + + +.. py:method:: DEMSolver.SetVerbosity + + .. code-block:: text + + SetVerbosity(self: deme._deme.DEMSolver, arg0: str) -> None + + Set the verbosity level of the solver. Select from 'QUIET', 'ERROR', 'WARNING', 'INFO', 'METRIC' or 'DEBUG'. Recommend 'INFO'. + + +.. py:method:: DEMSolver.ShowAnomalies + + .. code-block:: text + + ShowAnomalies(self: deme._deme.DEMSolver) -> None + + Show potential anomalies that may have been there in the simulation, then clear the anomaly log. + + +.. py:method:: DEMSolver.ShowMemStats + + .. code-block:: text + + ShowMemStats(self: deme._deme.DEMSolver) -> None + + Print the current memory usage in pretty format. + + +.. py:method:: DEMSolver.ShowThreadCollaborationStats + + .. code-block:: text + + ShowThreadCollaborationStats(self: deme._deme.DEMSolver) -> None + + Show the collaboration stats between dT and kT. This is more useful for tweaking the number of time steps that dT should be allowed to be in advance of kT. + + +.. py:method:: DEMSolver.ShowTimingStats + + .. code-block:: text + + ShowTimingStats(self: deme._deme.DEMSolver) -> None + + Show the wall time and percentages of wall time spend on various solver tasks. + + +.. py:method:: DEMSolver.SyncMemoryTransfer + + .. code-block:: text + + SyncMemoryTransfer(self: deme._deme.DEMSolver) -> None + + If the user used async-ed version of a tracker's get/set methods (to get a speed boost in many piecemeal accesses of a long array), this method should be called to mark the end of to-host transactions. + + +.. py:method:: DEMSolver.Track + + .. code-block:: text + + Track(*args, **kwargs) + Overloaded function. + + 1. Track(self: deme._deme.DEMSolver, arg0: deme._deme.DEMInitializer) -> deme._deme.Tracker + + Create a DEMTracker to allow direct control/modification/query to this external object/batch of clumps/triangle mesh object. + + 2. Track(self: deme._deme.DEMSolver, combined_inst: deme::DEMCombinedInstances) -> deme._deme.Tracker + + Create a single tracker that tracks all member owners in a combined-instances batch. + + +.. py:method:: DEMSolver.UpdateClumps + + .. code-block:: text + + UpdateClumps(self: deme._deme.DEMSolver) -> None + + Transfer newly loaded clumps to the GPU-side in mid-simulation. + + +.. py:method:: DEMSolver.UpdateSimParams + + .. code-block:: text + + UpdateSimParams(self: deme._deme.DEMSolver) -> None + + Transfer the cached sim params to the workers. Used for sim environment modification after system initialization. + + +.. py:method:: DEMSolver.UpdateStepSize + + .. code-block:: text + + UpdateStepSize(self: deme._deme.DEMSolver, ts: typing.SupportsFloat | typing.SupportsIndex = -1.0) -> None + + Update the time step size. Used after system initialization. + + +.. py:method:: DEMSolver.UpdateTriNodeRelPos + + .. code-block:: text + + UpdateTriNodeRelPos(self: deme._deme.DEMSolver, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex, arg2: collections.abc.Sequence[float3]) -> None + + Update the relative positions of the flattened triangle soup. + + +.. py:method:: DEMSolver.UseAdaptiveBinSize + + .. code-block:: text + + UseAdaptiveBinSize(self: deme._deme.DEMSolver, use: bool = True) -> None + + Enable or disable the use of adaptive bin size (by default it is on) + + +.. py:method:: DEMSolver.UseAdaptiveUpdateFreq + + .. code-block:: text + + UseAdaptiveUpdateFreq(self: deme._deme.DEMSolver, use: bool = True) -> None + + Enable or disable the use of adaptive max update step count (by default it is on) + + +.. py:method:: DEMSolver.UseFrictionalHertzianModel + + .. code-block:: text + + UseFrictionalHertzianModel(self: deme._deme.DEMSolver) -> deme._deme.DEMForceModel + + Instruct the solver to use frictonal (history-based) Hertzian contact force model. + + +.. py:method:: DEMSolver.UseFrictionlessHertzianModel + + .. code-block:: text + + UseFrictionlessHertzianModel(self: deme._deme.DEMSolver) -> deme._deme.DEMForceModel + + Instruct the solver to use frictonless Hertzian contact force model + + +.. py:method:: DEMSolver.UseHertzConstTimeStep + + .. code-block:: text + + UseHertzConstTimeStep(self: deme._deme.DEMSolver) -> None + + Use the setup-time Hertzian constant timestep estimate. + + +.. py:method:: DEMSolver.WaitForPendingOutput + + .. code-block:: text + + WaitForPendingOutput(self: deme._deme.DEMSolver) -> None + + Wait for any in-flight async output to finish. + + +.. py:method:: DEMSolver.WriteClumpFile + + .. code-block:: text + + WriteClumpFile(self: deme._deme.DEMSolver, outfilename: str, accuracy: typing.SupportsInt | typing.SupportsIndex = 10) -> None + + Write the current status of clumps to a file. + + +.. py:method:: DEMSolver.WriteContactFile + + .. code-block:: text + + WriteContactFile(self: deme._deme.DEMSolver, outfilename: str, force_thres: typing.SupportsFloat | typing.SupportsIndex = 1e-30) -> None + + Write all contact pairs to a file. Forces smaller than threshold will not be outputted. + + +.. py:method:: DEMSolver.WriteContactFileIncludingPotentialPairs + + .. code-block:: text + + WriteContactFileIncludingPotentialPairs(self: deme._deme.DEMSolver, outfilename: str) -> None + + Write all contact pairs kT-supplied to a file, thus including the potential ones (those are not yet in contact, or recently used to be in contact). + + +.. py:method:: DEMSolver.WriteMeshFile + + .. code-block:: text + + WriteMeshFile(self: deme._deme.DEMSolver, arg0: str) -> None + + Write the current status of all meshes to a file. + + +.. py:method:: DEMSolver.WriteSphereFile + + .. code-block:: text + + WriteSphereFile(self: deme._deme.DEMSolver, arg0: str) -> None + + Writes the current status of clumps (but decomposed as spheres) file. + + + +Tracker +------- + +.. py:class:: Tracker + + .. code-block:: text + + Access and modify owners associated with a tracked batch or object. + + Create trackers with ``DEMSolver.Track`` during setup. Most state queries and + updates require the solver to be initialized and synchronized. ``offset`` + selects an owner within the tracked collection, starting at zero. Keep the + parent solver alive while using a tracker. + +.. py:method:: Tracker.__init__ + + .. code-block:: text + + __init__(self: deme._deme.Tracker, solver: deme::DEMSolver) -> None + + Create a tracker associated with ``solver``. Prefer ``DEMSolver.Track`` so the tracked owner range is configured correctly. + + +.. py:method:: Tracker.AddAcc + + .. code-block:: text + + AddAcc(*args, **kwargs) + Overloaded function. + + 1. AddAcc(self: deme._deme.Tracker, acc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Add an extra acc to the tracked body, for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. + + 2. AddAcc(self: deme._deme.Tracker, acc: collections.abc.Sequence[float3]) -> None + + Add an extra acc to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external force, then using family prescription is the better method. + + +.. py:method:: Tracker.AddAngAcc + + .. code-block:: text + + AddAngAcc(*args, **kwargs) + Overloaded function. + + 1. AddAngAcc(self: deme._deme.Tracker, angAcc: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Add an extra angular acceleration to the tracked body, for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. + + 2. AddAngAcc(self: deme._deme.Tracker, angAcc: collections.abc.Sequence[float3]) -> None + + Add an extra angular acceleration to consecutive tracked objects, (only) for the next time step. Note if the user intends to add a persistent external torque, then using family prescription is the better method. + + +.. py:method:: Tracker.AngularVelocitiesGlobal + + .. code-block:: text + + AngularVelocitiesGlobal(self: deme._deme.Tracker) -> list[list[float]] + + Return global-frame angular velocities for all tracked owners. + + +.. py:method:: Tracker.AngularVelocitiesGlobalToDevice + + .. code-block:: text + + AngularVelocitiesGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked global-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.AngularVelocitiesLocal + + .. code-block:: text + + AngularVelocitiesLocal(self: deme._deme.Tracker) -> list[list[float]] + + Return local principal-axis-frame angular velocities for all tracked owners. + + +.. py:method:: Tracker.AngularVelocitiesLocalToDevice + + .. code-block:: text + + AngularVelocitiesLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked local-frame angular velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.AngVelGlobal + + .. code-block:: text + + AngVelGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the global-frame angular velocity of the owner at ``offset``. + + +.. py:method:: Tracker.AngVelLocal + + .. code-block:: text + + AngVelLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the angular velocity of the owner at ``offset`` in its local principal-axis frame. Rotate it by ``OriQ(offset)`` to obtain the global-frame value. + + +.. py:method:: Tracker.ContactAcc + + .. code-block:: text + + ContactAcc(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the acceleration of this tracked object, that is the result of its contact with other simulation entities. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + + +.. py:method:: Tracker.ContactAccelerations + + .. code-block:: text + + ContactAccelerations(self: deme._deme.Tracker) -> list[list[float]] + + Get the acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in global frame. In most cases, this means excluding the gravitational acceleration. The acceleration is in global frame. + + +.. py:method:: Tracker.ContactAccelerationsToDevice + + .. code-block:: text + + ContactAccelerationsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked global-frame contact accelerations to caller-owned CUDA ``float3`` storage. Gravity and other non-contact acceleration are excluded. + + +.. py:method:: Tracker.ContactAngAccGlobal + + .. code-block:: text + + ContactAngAccGlobal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's global frame. + + +.. py:method:: Tracker.ContactAngAccLocal + + .. code-block:: text + + ContactAngAccLocal(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the a portion of the angular acceleration of this tracked object, that is the result of its contact with other simulation entities. The acceleration is in this object's local frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsGlobal + + .. code-block:: text + + ContactAngularAccelerationsGlobal(self: deme._deme.Tracker) -> list[list[float]] + + Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's global frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsGlobalToDevice + + .. code-block:: text + + ContactAngularAccelerationsGlobalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked global-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + + +.. py:method:: Tracker.ContactAngularAccelerationsLocal + + .. code-block:: text + + ContactAngularAccelerationsLocal(self: deme._deme.Tracker) -> list[list[float]] + + Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of their contact with other simulation entities. The acceleration is in this object's local frame. + + +.. py:method:: Tracker.ContactAngularAccelerationsLocalToDevice + + .. code-block:: text + + ContactAngularAccelerationsLocalToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked local-frame contact angular accelerations to caller-owned CUDA ``float3`` storage. + + +.. py:method:: Tracker.FamiliesToDevice + + .. code-block:: text + + FamiliesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked family numbers to caller-owned CUDA ``uint32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.GetContactClumps + + .. code-block:: text + + GetContactClumps(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[int] + + Get the clumps that are in contact with this tracked owner as a vector. + + +.. py:method:: Tracker.GetContactForces + + .. code-block:: text + + GetContactForces(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorque + + .. code-block:: text + + GetContactForcesAndGlobalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces and global torques that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAll + + .. code-block:: text + + GetContactForcesAndGlobalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int + + Get all contact forces and global torques that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndGlobalTorqueForAllToDevice + + .. code-block:: text + + GetContactForcesAndGlobalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + Synchronously compact contact points, forces, and global-frame extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorque + + .. code-block:: text + + GetContactForcesAndLocalTorque(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3], offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get all contact forces and local torques that concern this tracked object. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAll + + .. code-block:: text + + GetContactForcesAndLocalTorqueForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3], torques: collections.abc.Sequence[float3]) -> int + + Get all contact forces and local torques that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesAndLocalTorqueForAllToDevice + + .. code-block:: text + + GetContactForcesAndLocalTorqueForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, torques: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + Synchronously compact contact points, forces, and owner-local extra torques into caller-owned CUDA ``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number of valid rows. + + +.. py:method:: Tracker.GetContactForcesForAll + + .. code-block:: text + + GetContactForcesForAll(self: deme._deme.Tracker, points: collections.abc.Sequence[float3], forces: collections.abc.Sequence[float3]) -> int + + Get all contact forces that concern all objects tracked by this tracker. Returns number of force pairs. + + +.. py:method:: Tracker.GetContactForcesForAllToDevice + + .. code-block:: text + + GetContactForcesForAllToDevice(self: deme._deme.Tracker, points: typing.SupportsInt | typing.SupportsIndex, forces: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> int + + + Synchronously compact contact points and forces for all tracked owners into CUDA ``float3`` arrays. + + Each pointer must address ``capacity`` elements on logical CUDA ``device``. + Capacity must cover the simulation's total recorded-contact count. The return + value is the number of valid compacted rows. Contact force recording must be + enabled. + + +.. py:method:: Tracker.GetFamilies + + .. code-block:: text + + GetFamilies(self: deme._deme.Tracker) -> list[int] + + Get the family numbers of all the tracked object. + + +.. py:method:: Tracker.GetFamily + + .. code-block:: text + + GetFamily(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Get the family number of the tracked object. + + +.. py:method:: Tracker.GetMesh + + .. code-block:: text + + GetMesh(self: deme._deme.Tracker) -> deme::DEMMesh + + Get a handle for the mesh this tracker is tracking. + + +.. py:method:: Tracker.GetMeshNodesGlobal + + .. code-block:: text + + GetMeshNodesGlobal(self: deme._deme.Tracker) -> list[float3] + + Get the current locations of all the nodes in the mesh being tracked. + + +.. py:method:: Tracker.GetOwnerID + + .. code-block:: text + + GetOwnerID(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> int + + Return the simulation-wide owner ID at ``offset`` within this tracker. + + +.. py:method:: Tracker.GetOwnerIDs + + .. code-block:: text + + GetOwnerIDs(self: deme._deme.Tracker) -> list[int] + + Return simulation-wide owner IDs for every owner covered by this tracker. + + +.. py:method:: Tracker.GetOwnerWildcardValue + + .. code-block:: text + + GetOwnerWildcardValue(self: deme._deme.Tracker, name: str, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float + + Get the owner's wildcard value. + + +.. py:method:: Tracker.GetOwnerWildcardValues + + .. code-block:: text + + GetOwnerWildcardValues(self: deme._deme.Tracker, name: str) -> list[float] + + Get the owner wildcard values for all the owners entities tracked by this tracker. + + +.. py:method:: Tracker.Mass + + .. code-block:: text + + Mass(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> float + + Get the mass of the tracked object. + + +.. py:method:: Tracker.Masses + + .. code-block:: text + + Masses(self: deme._deme.Tracker) -> list[float] + + Get the masses of all the tracked objects. + + +.. py:method:: Tracker.MassesToDevice + + .. code-block:: text + + MassesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked masses to caller-owned CUDA ``float32`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.MOI + + .. code-block:: text + + MOI(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Get the moment of inertia (in principal axis frame) of the tracked object. + + +.. py:method:: Tracker.MOIs + + .. code-block:: text + + MOIs(self: deme._deme.Tracker) -> list[list[float]] + + Get the moment of inertia (in principal axis frame) of all the tracked objects. + + +.. py:method:: Tracker.MOIsToDevice + + .. code-block:: text + + MOIsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked principal moments of inertia to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + +.. py:method:: Tracker.OrientationQuaternions + + .. code-block:: text + + OrientationQuaternions(self: deme._deme.Tracker) -> list[list[float]] + + Return local-to-global orientation quaternions for all tracked owners. Each quaternion uses Python ordering ``(x, y, z, w)``. + + +.. py:method:: Tracker.OrientationQuaternionsToDevice + + .. code-block:: text + + OrientationQuaternionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked local-to-global quaternions to caller-owned CUDA ``float4`` storage in ``(x, y, z, w)`` order. ``capacity`` is in elements. + + +.. py:method:: Tracker.OriQ + + .. code-block:: text + + OriQ(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the quaternion rotating the owner-local frame to the global frame. Python ordering is ``(x, y, z, w)`` (Chrono ``(e1, e2, e3, e0)``). + + +.. py:method:: Tracker.OwnerWildcardValuesToDevice + + .. code-block:: text + + OwnerWildcardValuesToDevice(self: deme._deme.Tracker, name: str, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy the named ``float32`` owner wildcard for every tracked owner to caller-owned CUDA storage. ``capacity`` is in owner elements. + + +.. py:method:: Tracker.Pos + + .. code-block:: text + + Pos(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the global-frame center-of-mass position of the tracked owner at ``offset``. + + +.. py:method:: Tracker.Positions + + .. code-block:: text + + Positions(self: deme._deme.Tracker) -> list[list[float]] + + Return global-frame center-of-mass positions for all owners covered by this tracker. + + +.. py:method:: Tracker.PositionsToDevice + + .. code-block:: text + + PositionsToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + + Synchronously copy all tracked global positions to caller-owned CUDA memory. + + ``pointer`` is an integer address to writable, CUDA-accessible ``float3`` + storage on logical CUDA ``device``. ``capacity`` is measured in ``float3`` + elements and must cover every owner in this tracker. The caller owns the + allocation and must keep it alive through the call. + + +.. py:method:: Tracker.SetAngVel + + .. code-block:: text + + SetAngVel(*args, **kwargs) + Overloaded function. + + 1. SetAngVel(self: deme._deme.Tracker, angVel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the angular velocity of this tracked object in its own local coordinate system. + + 2. SetAngVel(self: deme._deme.Tracker, angVel: collections.abc.Sequence[float3]) -> None + + Set the angular velocity of consecutive tracked objects in their own local coordinate systems. + + +.. py:method:: Tracker.SetFamily + + .. code-block:: text + + SetFamily(*args, **kwargs) + Overloaded function. + + 1. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex) -> None + + Change the family numbers of all the entities tracked by this tracker. + + 2. SetFamily(self: deme._deme.Tracker, fam_num: typing.SupportsInt | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex) -> None + + Change the family number of one entities tracked by this tracker. + + +.. py:method:: Tracker.SetOriQ + + .. code-block:: text + + SetOriQ(*args, **kwargs) + Overloaded function. + + 1. SetOriQ(self: deme._deme.Tracker, oriQ: float4, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the quaternion which represents the orientation of this tracked object's coordinate system. + + 2. SetOriQ(self: deme._deme.Tracker, oriQ: collections.abc.Sequence[float4]) -> None + + Set the quaternion which represents the orientation of consecutive tracked objects' coordinate systems. + + +.. py:method:: Tracker.SetOwnerWildcardValue + + .. code-block:: text + + SetOwnerWildcardValue(self: deme._deme.Tracker, name: str, wc: typing.SupportsFloat | typing.SupportsIndex, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set a wildcard value of the owner this tracker is tracking. + + +.. py:method:: Tracker.SetOwnerWildcardValues + + .. code-block:: text + + SetOwnerWildcardValues(self: deme._deme.Tracker, name: str, wc: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set a wildcard value of the owner this tracker is tracking. + + +.. py:method:: Tracker.SetPos + + .. code-block:: text + + SetPos(*args, **kwargs) + Overloaded function. + + 1. SetPos(self: deme._deme.Tracker, pos: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the position of this tracked object. + + 2. SetPos(self: deme._deme.Tracker, pos: collections.abc.Sequence[float3]) -> None + + Set the positions of consecutive tracked objects. + + +.. py:method:: Tracker.SetVel + + .. code-block:: text + + SetVel(*args, **kwargs) + Overloaded function. + + 1. SetVel(self: deme._deme.Tracker, vel: float3, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> None + + Set the velocity of this tracked object in global frame. + + 2. SetVel(self: deme._deme.Tracker, vel: collections.abc.Sequence[float3]) -> None + + Set the velocity of consecutive tracked objects in global frame. + + +.. py:method:: Tracker.UpdateMesh + + .. code-block:: text + + UpdateMesh(self: deme._deme.Tracker, new_nodes: collections.abc.Sequence[float3]) -> None + + Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes. + + +.. py:method:: Tracker.UpdateMeshByIncrement + + .. code-block:: text + + UpdateMeshByIncrement(self: deme._deme.Tracker, deformation: collections.abc.Sequence[float3]) -> None + + Change the coordinates of each mesh node by the given amount. + + +.. py:method:: Tracker.Vel + + .. code-block:: text + + Vel(self: deme._deme.Tracker, offset: typing.SupportsInt | typing.SupportsIndex = 0) -> list[float] + + Return the global-frame linear velocity of the owner at ``offset``. + + +.. py:method:: Tracker.Velocities + + .. code-block:: text + + Velocities(self: deme._deme.Tracker) -> list[list[float]] + + Return global-frame linear velocities for all tracked owners. + + +.. py:method:: Tracker.VelocitiesToDevice + + .. code-block:: text + + VelocitiesToDevice(self: deme._deme.Tracker, pointer: typing.SupportsInt | typing.SupportsIndex, capacity: typing.SupportsInt | typing.SupportsIndex, device: typing.SupportsInt | typing.SupportsIndex) -> None + + Synchronously copy all tracked global linear velocities to caller-owned CUDA ``float3`` storage. ``capacity`` is in elements; ``device`` is the allocation's logical CUDA device. + + + +CNT_OUTPUT_CONTENT +------------------ + +.. py:class:: CNT_OUTPUT_CONTENT + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:method:: CNT_OUTPUT_CONTENT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.CNT_OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_TYPE + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.CNT_WILDCARD + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.COMPONENT + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.FORCE + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.GEO_ID + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.NICKNAME + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.NORMAL + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.OWNER + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.POINT + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + +.. py:attribute:: CNT_OUTPUT_CONTENT.TORQUE + + .. code-block:: text + + Members: + + CNT_TYPE + + FORCE + + POINT + + COMPONENT + + NORMAL + + TORQUE + + CNT_WILDCARD + + OWNER + + GEO_ID + + NICKNAME + + +ContactInfoContainer +-------------------- + +.. py:class:: ContactInfoContainer + + .. code-block:: text + + Named arrays describing contacts selected by ``DEMSolver.SetContactOutputContent``. + +.. py:method:: ContactInfoContainer.__init__ + + .. code-block:: text + + __init__(self: deme._deme.ContactInfoContainer, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: collections.abc.Sequence[tuple[str, str]]) -> None + + +.. py:method:: ContactInfoContainer.GetAGeo + + .. code-block:: text + + GetAGeo(self: deme._deme.ContactInfoContainer) -> list[int] + + Get AGeo number from the container. + + +.. py:method:: ContactInfoContainer.GetAOwner + + .. code-block:: text + + GetAOwner(self: deme._deme.ContactInfoContainer) -> list[int] + + Get AOwner number from the container. + + +.. py:method:: ContactInfoContainer.GetAOwnerFamily + + .. code-block:: text + + GetAOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] + + Get AOwnerFamily number from the container. + + +.. py:method:: ContactInfoContainer.GetBGeo + + .. code-block:: text + + GetBGeo(self: deme._deme.ContactInfoContainer) -> list[int] + + Get BGeo number from the container. + + +.. py:method:: ContactInfoContainer.GetBOwner + + .. code-block:: text + + GetBOwner(self: deme._deme.ContactInfoContainer) -> list[int] + + Get BOwner number from the container. + + +.. py:method:: ContactInfoContainer.GetBOwnerFamily + + .. code-block:: text + + GetBOwnerFamily(self: deme._deme.ContactInfoContainer) -> list[int] + + Get BOwnerFamily number from the container. + + +.. py:method:: ContactInfoContainer.GetContactType + + .. code-block:: text + + GetContactType(self: deme._deme.ContactInfoContainer) -> list[str] + + Get contact type as strings from the container. + + +.. py:method:: ContactInfoContainer.GetForce + + .. code-block:: text + + GetForce(self: deme._deme.ContactInfoContainer) -> list[float3] + + Get force as vectors from the container. + + +.. py:method:: ContactInfoContainer.GetNormal + + .. code-block:: text + + GetNormal(self: deme._deme.ContactInfoContainer) -> list[float3] + + Get contact normal as vectors from the container. + + +.. py:method:: ContactInfoContainer.GetPoint + + .. code-block:: text + + GetPoint(self: deme._deme.ContactInfoContainer) -> list[float3] + + Get contact points as vectors from the container. + + +.. py:method:: ContactInfoContainer.GetTorque + + .. code-block:: text + + GetTorque(self: deme._deme.ContactInfoContainer) -> list[float3] + + Get torque as vectors from the container. + + + +DataContainer +------------- + +.. py:class:: DataContainer + + .. code-block:: text + + String-keyed container used to return named arrays from DEME. + +.. py:method:: DataContainer.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DataContainer) -> None + + Create an empty data container. + + +.. py:method:: DataContainer.Get + + .. code-block:: text + + Get(self: deme._deme.DataContainer, key: str) -> list[float] + + Return the stored float array named ``key`` by reference. This is primarily used for wildcard output. + + + +DEMClumpBatch +------------- + +.. py:class:: DEMClumpBatch + + .. code-block:: text + + Cached setup data for a batch of clump instances. + +.. py:method:: DEMClumpBatch.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMClumpBatch, count: typing.SupportsInt | typing.SupportsIndex) -> None + + Create setup storage for ``count`` clumps. + + +.. py:method:: DEMClumpBatch.AddExistingContactWildcard + + .. code-block:: text + + AddExistingContactWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Add one named restart contact-wildcard array after ``SetExistingContacts``. + + +.. py:method:: DEMClumpBatch.AddOwnerWildcard + + .. code-block:: text + + AddOwnerWildcard(*args, **kwargs) + Overloaded function. + + 1. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, values: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Add one initial owner wildcard with one value per clump. + + 2. AddOwnerWildcard(self: deme._deme.DEMClumpBatch, name: str, value: typing.SupportsFloat | typing.SupportsIndex) -> None + + Add one initial owner wildcard with the same value for every clump. + + +.. py:method:: DEMClumpBatch.GetNumClumps + + .. code-block:: text + + GetNumClumps(self: deme._deme.DEMClumpBatch) -> int + + Return the number of clumps in this batch. + + +.. py:method:: DEMClumpBatch.GetNumContacts + + .. code-block:: text + + GetNumContacts(self: deme._deme.DEMClumpBatch) -> int + + Return the number of pre-existing contacts supplied for restart initialization. + + +.. py:method:: DEMClumpBatch.GetNumSpheres + + .. code-block:: text + + GetNumSpheres(self: deme._deme.DEMClumpBatch) -> int + + Return the total number of component spheres represented by this batch. + + +.. py:method:: DEMClumpBatch.SetExistingContacts + + .. code-block:: text + + SetExistingContacts(self: deme._deme.DEMClumpBatch, pairs: collections.abc.Sequence[tuple[typing.SupportsInt | typing.SupportsIndex, typing.SupportsInt | typing.SupportsIndex]]) -> None + + Supply sphere-sphere contact owner-ID pairs relative to this batch when restoring a simulation. + + +.. py:method:: DEMClumpBatch.SetExistingContactWildcards + + .. code-block:: text + + SetExistingContactWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + + Supply named restart wildcard arrays after ``SetExistingContacts``. Every array must match the pre-existing contact count. + + +.. py:method:: DEMClumpBatch.SetFamilies + + .. code-block:: text + + SetFamilies(*args, **kwargs) + Overloaded function. + + 1. SetFamilies(self: deme._deme.DEMClumpBatch, families: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None + + Set one family number per clump. Family behavior is configured on the solver. + + 2. SetFamilies(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the same family number for every clump. + + +.. py:method:: DEMClumpBatch.SetFamily + + .. code-block:: text + + SetFamily(self: deme._deme.DEMClumpBatch, family: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the same family number for every clump. + + +.. py:method:: DEMClumpBatch.SetOwnerWildcards + + .. code-block:: text + + SetOwnerWildcards(self: deme._deme.DEMClumpBatch, wildcards: collections.abc.Mapping[str, collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + + Set named initial owner-wildcard arrays. Every array must match the clump count. + + +.. py:method:: DEMClumpBatch.SetType + + .. code-block:: text + + SetType(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None + + Assign the same clump template to every instance. + + +.. py:method:: DEMClumpBatch.SetTypes + + .. code-block:: text + + SetTypes(*args, **kwargs) + Overloaded function. + + 1. SetTypes(self: deme._deme.DEMClumpBatch, types: collections.abc.Sequence[deme._deme.DEMClumpTemplate]) -> None + + Assign one clump template per instance. The sequence length must equal the batch size. + + 2. SetTypes(self: deme._deme.DEMClumpBatch, type: deme._deme.DEMClumpTemplate) -> None + + Assign the same clump template to every instance. + + +.. py:method:: DEMClumpBatch.SetVel + + .. code-block:: text + + SetVel(*args, **kwargs) + Overloaded function. + + 1. SetVel(self: deme._deme.DEMClumpBatch, velocities: collections.abc.Sequence[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]]) -> None + + Set one global initial linear velocity vector per clump. + + 2. SetVel(self: deme._deme.DEMClumpBatch, velocity: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the same global initial linear velocity for every clump. + + + +DEMClumpTemplate +---------------- + +.. py:class:: DEMClumpTemplate + + .. code-block:: text + + Reusable rigid clump topology composed of one or more sphere components. + +.. py:method:: DEMClumpTemplate.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMClumpTemplate) -> None + + Create an empty clump template. + + +.. py:method:: DEMClumpTemplate.AssignName + + .. code-block:: text + + AssignName(self: deme._deme.DEMClumpTemplate, name: str) -> None + + Assign the clump type name written to output files. + + +.. py:method:: DEMClumpTemplate.InformCentroidPrincipal + + .. code-block:: text + + InformCentroidPrincipal(self: deme._deme.DEMClumpTemplate, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Re-express component positions about the supplied center of mass and principal orientation. + + +.. py:method:: DEMClumpTemplate.Mass + + .. code-block:: text + + Mass(self: deme._deme.DEMClumpTemplate) -> float + + Return the clump mass. + + +.. py:method:: DEMClumpTemplate.MOI + + .. code-block:: text + + MOI(self: deme._deme.DEMClumpTemplate) -> float3 + + Return principal moments of inertia in the clump-local principal frame. + + +.. py:method:: DEMClumpTemplate.Move + + .. code-block:: text + + Move(self: deme._deme.DEMClumpTemplate, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Apply a translation and quaternion rotation to the component coordinates. + + +.. py:method:: DEMClumpTemplate.ReadComponentFromFile + + .. code-block:: text + + ReadComponentFromFile(self: deme._deme.DEMClumpTemplate, filename: str, x_id: str = 'x', y_id: str = 'y', z_id: str = 'z', r_id: str = 'r') -> int + + Load sphere-component positions and radii from a delimited file. + + +.. py:method:: DEMClumpTemplate.Scale + + .. code-block:: text + + Scale(self: deme._deme.DEMClumpTemplate, factor: typing.SupportsFloat | typing.SupportsIndex) -> None + + Uniformly scale component radii and relative positions. + + +.. py:method:: DEMClumpTemplate.SetMass + + .. code-block:: text + + SetMass(self: deme._deme.DEMClumpTemplate, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the clump mass. + + +.. py:method:: DEMClumpTemplate.SetMaterial + + .. code-block:: text + + SetMaterial(*args, **kwargs) + Overloaded function. + + 1. SetMaterial(self: deme._deme.DEMClumpTemplate, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None + + Assign one material per sphere component. The sequence length must equal the component count. + + 2. SetMaterial(self: deme._deme.DEMClumpTemplate, material: deme._deme.DEMMaterial) -> None + + Assign the same material to every sphere component. + + +.. py:method:: DEMClumpTemplate.SetMOI + + .. code-block:: text + + SetMOI(self: deme._deme.DEMClumpTemplate, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the three principal moments of inertia in the clump-local frame. + + +.. py:method:: DEMClumpTemplate.SetVolume + + .. code-block:: text + + SetVolume(self: deme._deme.DEMClumpTemplate, volume: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the clump volume, which is required for void-ratio queries. + + + +DEMCombinedInstances +-------------------- + +.. py:class:: DEMCombinedInstances + + .. code-block:: text + + A batch of instantiated combined owners and their member-owner metadata. + +.. py:method:: DEMCombinedInstances.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMCombinedInstances) -> None + + Create an empty combined-instance batch. + + +.. py:method:: DEMCombinedInstances.AddOwnerWildcard + + .. code-block:: text + + AddOwnerWildcard(*args, **kwargs) + Overloaded function. + + 1. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, vals: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Add an owner wildcard to all member owners with per-owner values. + + 2. AddOwnerWildcard(self: deme._deme.DEMCombinedInstances, name: str, val: typing.SupportsFloat | typing.SupportsIndex) -> None + + Add an owner wildcard to all member owners with a uniform value. + + +.. py:method:: DEMCombinedInstances.GetNumOwners + + .. code-block:: text + + GetNumOwners(self: deme._deme.DEMCombinedInstances) -> int + + Get total number of member owners in this combined batch. + + + +DEMCombinedTemplate +------------------- + +.. py:class:: DEMCombinedTemplate + + .. code-block:: text + + Reusable rigid grouping of clump or mesh templates with fixed member-relative transforms. + +.. py:method:: DEMCombinedTemplate.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMCombinedTemplate) -> None + + Create an empty combined-owner template. + + + +DEMERuntimeDataHelper +--------------------- + +.. py:class:: DEMERuntimeDataHelper + + .. code-block:: text + + No class description is currently available. + +.. py:method:: DEMERuntimeDataHelper.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMERuntimeDataHelper) -> None + + +.. py:method:: DEMERuntimeDataHelper.SetPathPrefix + + .. code-block:: text + + Convert a function to be a static method. + + A static method does not receive an implicit first argument. + To declare a static method, use this idiom: + + class C: + @staticmethod + def f(arg1, arg2, argN): + ... + + It can be called either on the class (e.g. C.f()) or on an instance + (e.g. C().f()). Both the class and the instance are ignored, and + neither is passed implicitly as the first argument to the method. + + Static methods in Python are similar to those found in Java or C++. + For a more advanced concept, see the classmethod builtin. + + + +DEMExternObj +------------ + +.. py:class:: DEMExternObj + + .. code-block:: text + + Analytical external owner composed of planes, cylinders, and related primitives. + +.. py:method:: DEMExternObj.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMExternObj) -> None + + Create an empty analytical external object. + + +.. py:method:: DEMExternObj.AddCylinder + + .. code-block:: text + + AddCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], axis: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None + + Add an infinite cylinder along a user-specified object-local axis. + + +.. py:method:: DEMExternObj.AddPlane + + .. code-block:: text + + AddPlane(self: deme._deme.DEMExternObj, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], material: deme._deme.DEMMaterial) -> None + + Add an infinite plane specified by a point and normal in the object's local frame. + + +.. py:method:: DEMExternObj.AddZCylinder + + .. code-block:: text + + AddZCylinder(self: deme._deme.DEMExternObj, pos: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], rad: typing.SupportsFloat | typing.SupportsIndex, material: deme._deme.DEMMaterial, normal: bool = False) -> None + + Add an infinite cylinder aligned with the object-local Z axis. + + +.. py:attribute:: DEMExternObj.entity_params + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.family_code + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.init_oriQ + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.init_pos + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.load_order + + .. code-block:: text + + Bound attribute. + +.. py:method:: DEMExternObj.Mass + + .. code-block:: text + + Mass(self: deme._deme.DEMExternObj) -> float + + Return the object's mass. + + +.. py:attribute:: DEMExternObj.mass + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.materials + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMExternObj.MOI + + .. code-block:: text + + Bound attribute. + +.. py:method:: DEMExternObj.SetFamily + + .. code-block:: text + + SetFamily(self: deme._deme.DEMExternObj, family: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the contact family number before initialization. + + +.. py:method:: DEMExternObj.SetInitPos + + .. code-block:: text + + SetInitPos(self: deme._deme.DEMExternObj, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the initial global center-of-mass position before initialization. + + +.. py:method:: DEMExternObj.SetInitQuat + + .. code-block:: text + + SetInitQuat(self: deme._deme.DEMExternObj, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the initial local-to-global orientation quaternion ``(x, y, z, w)`` before initialization. + + +.. py:method:: DEMExternObj.SetMass + + .. code-block:: text + + SetMass(self: deme._deme.DEMExternObj, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the object mass. + + +.. py:method:: DEMExternObj.SetMOI + + .. code-block:: text + + SetMOI(self: deme._deme.DEMExternObj, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the three principal moments of inertia in the object-local frame. + + +.. py:attribute:: DEMExternObj.types + + .. code-block:: text + + Bound attribute. + + +DEMForceModel +------------- + +.. py:class:: DEMForceModel + + .. code-block:: text + + No class description is currently available. + +.. py:method:: DEMForceModel.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMForceModel, model: deme::FORCE_MODEL) -> None + + Create a force-model definition of the selected built-in type. + + +.. py:method:: DEMForceModel.DefineCustomModel + + .. code-block:: text + + DefineCustomModel(self: deme._deme.DEMForceModel, arg0: str) -> None + + Define user-custom force model with a string which is your force calculation code. + + +.. py:method:: DEMForceModel.DefineCustomModelPrerequisites + + .. code-block:: text + + DefineCustomModelPrerequisites(self: deme._deme.DEMForceModel, arg0: str) -> None + + Define user-custom force model's utility __device__ functions with a string. + + +.. py:method:: DEMForceModel.ReadCustomModelFile + + .. code-block:: text + + ReadCustomModelFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int + + Read user-custom force model from a file (which by default should reside in kernel/DEMUserScripts), which contains your force calculation code. Returns 0 if read successfully, otherwise 1. + + +.. py:method:: DEMForceModel.ReadCustomModelPrerequisitesFile + + .. code-block:: text + + ReadCustomModelPrerequisitesFile(self: deme._deme.DEMForceModel, arg0: std::filesystem::__cxx11::path) -> int + + Read user-custom force model's utility __device__ functions from a file (which by default should reside in kernel/DEMUserScripts). Returns 0 if read successfully, otherwise 1. + + +.. py:method:: DEMForceModel.SetForceModelType + + .. code-block:: text + + SetForceModelType(self: deme._deme.DEMForceModel, arg0: deme::FORCE_MODEL) -> None + + Set the contact force model type + + +.. py:method:: DEMForceModel.SetMustHaveMatProp + + .. code-block:: text + + SetMustHaveMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + + Specifiy the material properties that this force model will use + + +.. py:method:: DEMForceModel.SetMustPairwiseMatProp + + .. code-block:: text + + SetMustPairwiseMatProp(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + + Specifiy the material properties that are pair-wise (instead of being associated with each individual material). + + +.. py:method:: DEMForceModel.SetPerContactWildcards + + .. code-block:: text + + SetPerContactWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + + Set the names for the extra quantities that will be associated with each contact pair. For example, history-based models should have 3 float arrays to store contact history. Only float is supported. Note the initial value of all contact wildcard arrays is automatically 0 + + +.. py:method:: DEMForceModel.SetPerOwnerWildcards + + .. code-block:: text + + SetPerOwnerWildcards(self: deme._deme.DEMForceModel, arg0: collections.abc.Set[str]) -> None + + Set the names for the extra quantities that will be associated with each owner. For example, you can use this to associate a cohesion parameter to each particle. Only float is supported. + + + +DEMInitializer +-------------- + +.. py:class:: DEMInitializer + + .. code-block:: text + + No class description is currently available. + +.. py:method:: DEMInitializer.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMInitializer) -> None + + + +DEMInspector +------------ + +.. py:class:: DEMInspector + + .. code-block:: text + + Evaluate a configured aggregate or spatial quantity from initialized simulation state. + +.. py:method:: DEMInspector.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMInspector, arg0: deme::DEMSolver, arg1: deme::DEMDynamicThread, arg2: str) -> None + + +.. py:method:: DEMInspector.GetValue + + .. code-block:: text + + GetValue(self: deme._deme.DEMInspector) -> float + + Synchronize as needed and return the inspector's current scalar value. + + + +DEMMaterial +----------- + +.. py:class:: DEMMaterial + + .. code-block:: text + + Material property name/value pairs used by a contact force model. + +.. py:method:: DEMMaterial.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMMaterial, properties: collections.abc.Mapping[str, typing.SupportsFloat | typing.SupportsIndex]) -> None + + Create material properties. Normally use ``DEMSolver.LoadMaterial`` so the material is registered. + + +.. py:attribute:: DEMMaterial.load_order + + .. code-block:: text + + Bound attribute. + +.. py:attribute:: DEMMaterial.mat_prop + + .. code-block:: text + + Bound attribute. + + +DEMMesh +------- + +.. py:class:: DEMMesh + + .. code-block:: text + + Connected triangle-mesh template or external mesh owner. + +.. py:method:: DEMMesh.__init__ + + .. code-block:: text + + __init__(*args, **kwargs) + Overloaded function. + + 1. __init__(self: deme._deme.DEMMesh) -> None + + Create an empty mesh. + + 2. __init__(self: deme._deme.DEMMesh, filename: str) -> None + + Load a mesh from a Wavefront OBJ file. + + 3. __init__(self: deme._deme.DEMMesh, filename: str, material: deme._deme.DEMMaterial) -> None + + Load a Wavefront OBJ mesh and assign one material to every triangle. + + +.. py:method:: DEMMesh.ArePatchesExplicitlySet + + .. code-block:: text + + ArePatchesExplicitlySet(self: deme._deme.DEMMesh) -> bool + + Return whether patch IDs were explicitly supplied or computed. + + +.. py:method:: DEMMesh.Clear + + .. code-block:: text + + Clear(self: deme._deme.DEMMesh) -> None + + Remove all mesh geometry and cached attributes. + + +.. py:method:: DEMMesh.ClearWildcards + + .. code-block:: text + + ClearWildcards(self: deme._deme.DEMMesh) -> None + + Remove cached triangle wildcard values. + + +.. py:method:: DEMMesh.DisableShell + + .. code-block:: text + + DisableShell(self: deme._deme.DEMMesh) -> None + + Disable finite-thickness shell mode. + + +.. py:method:: DEMMesh.GetCoordsVertices + + .. code-block:: text + + GetCoordsVertices(self: deme._deme.DEMMesh) -> list[list[float]] + + Return mesh vertex coordinates as ``[[x, y, z], ...]``. + + +.. py:method:: DEMMesh.GetIndicesVertexes + + .. code-block:: text + + GetIndicesVertexes(self: deme._deme.DEMMesh) -> list[list[int]] + + Return triangle vertex-index triplets. + + +.. py:method:: DEMMesh.GetNumNodes + + .. code-block:: text + + GetNumNodes(self: deme._deme.DEMMesh) -> int + + Get the number of nodes in the mesh + + +.. py:method:: DEMMesh.GetNumPatches + + .. code-block:: text + + GetNumPatches(self: deme._deme.DEMMesh) -> int + + Get the number of mesh patches. + + +.. py:method:: DEMMesh.GetNumTriangles + + .. code-block:: text + + GetNumTriangles(self: deme._deme.DEMMesh) -> int + + Get the number of triangles already added to this mesh + + +.. py:method:: DEMMesh.GetPatchIDs + + .. code-block:: text + + GetPatchIDs(self: deme._deme.DEMMesh) -> list[int] + + Get one patch ID per triangle. + + +.. py:method:: DEMMesh.GetShellThickness + + .. code-block:: text + + GetShellThickness(self: deme._deme.DEMMesh) -> float + + Return the full shell thickness. + + +.. py:method:: DEMMesh.GetTriangle + + .. code-block:: text + + GetTriangle(self: deme._deme.DEMMesh, arg0: typing.SupportsInt | typing.SupportsIndex) -> deme::DEMTriangle + + Access the n-th triangle in mesh + + +.. py:method:: DEMMesh.InformCentroidPrincipal + + .. code-block:: text + + InformCentroidPrincipal(self: deme._deme.DEMMesh, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], orientation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Re-express mesh vertices about the supplied center of mass and principal orientation. + + +.. py:method:: DEMMesh.IsShell + + .. code-block:: text + + IsShell(self: deme._deme.DEMMesh) -> bool + + Return whether this mesh is configured as a shell. + + +.. py:method:: DEMMesh.LoadWavefrontMesh + + .. code-block:: text + + LoadWavefrontMesh(self: deme._deme.DEMMesh, input_file: str, load_normals: bool = True, load_uv: bool = False) -> bool + + Load a triangle mesh saved as a Wavefront .obj file + + +.. py:method:: DEMMesh.Mass + + .. code-block:: text + + Mass(self: deme._deme.DEMMesh) -> float + + Return the mesh owner's mass. + + +.. py:method:: DEMMesh.Mirror + + .. code-block:: text + + Mirror(self: deme._deme.DEMMesh, point: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Mirror vertices about a plane specified by a point and normal. + + +.. py:method:: DEMMesh.MOI + + .. code-block:: text + + MOI(self: deme._deme.DEMMesh) -> float3 + + Return principal moments of inertia in the mesh-local frame. + + +.. py:method:: DEMMesh.Move + + .. code-block:: text + + Move(self: deme._deme.DEMMesh, translation: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Apply a translation and quaternion rotation to mesh vertices. + + +.. py:method:: DEMMesh.Scale + + .. code-block:: text + + Scale(*args, **kwargs) + Overloaded function. + + 1. Scale(self: deme._deme.DEMMesh, factor: typing.SupportsFloat | typing.SupportsIndex) -> None + + Uniformly scale mesh vertices. + + 2. Scale(self: deme._deme.DEMMesh, factors: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Scale mesh vertices independently along X, Y, and Z. + + +.. py:method:: DEMMesh.SetFamily + + .. code-block:: text + + SetFamily(self: deme._deme.DEMMesh, family: typing.SupportsInt | typing.SupportsIndex) -> None + + Set the contact family number before initialization. + + +.. py:method:: DEMMesh.SetInitPos + + .. code-block:: text + + SetInitPos(self: deme._deme.DEMMesh, position: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the initial global center-of-mass position. + + +.. py:method:: DEMMesh.SetInitQuat + + .. code-block:: text + + SetInitQuat(self: deme._deme.DEMMesh, quaternion: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the initial local-to-global orientation quaternion ``(x, y, z, w)``. + + +.. py:method:: DEMMesh.SetMass + + .. code-block:: text + + SetMass(self: deme._deme.DEMMesh, mass: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the mesh owner's mass. + + +.. py:method:: DEMMesh.SetMaterial + + .. code-block:: text + + SetMaterial(*args, **kwargs) + Overloaded function. + + 1. SetMaterial(self: deme._deme.DEMMesh, material: deme._deme.DEMMaterial) -> None + + Assign the same material to every mesh triangle. + + 2. SetMaterial(self: deme._deme.DEMMesh, materials: collections.abc.Sequence[deme._deme.DEMMaterial]) -> None + + Assign one material per triangle. The sequence length must equal the triangle count. + + +.. py:method:: DEMMesh.SetMOI + + .. code-block:: text + + SetMOI(self: deme._deme.DEMMesh, MOI: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None + + Set the three principal moments of inertia in the mesh-local frame. + + +.. py:method:: DEMMesh.SetPatchIDs + + .. code-block:: text + + SetPatchIDs(self: deme._deme.DEMMesh, patch_ids: collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex]) -> None + + Set one patch ID per triangle. + + +.. py:method:: DEMMesh.SetShellThickness + + .. code-block:: text + + SetShellThickness(self: deme._deme.DEMMesh, thickness: typing.SupportsFloat | typing.SupportsIndex) -> None + + Treat this mesh as a finite-thickness shell. + + +.. py:method:: DEMMesh.SplitIntoConvexPatches + + .. code-block:: text + + SplitIntoConvexPatches(self: deme._deme.DEMMesh, hard_angle_deg: typing.SupportsFloat | typing.SupportsIndex = 30.0) -> int + + Split the mesh into connected angle-threshold patches. + + +.. py:method:: DEMMesh.UseNormals + + .. code-block:: text + + UseNormals(self: deme._deme.DEMMesh, use: bool = True) -> None + + Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle so that the normals derived from right-hand-rule are the same as the normals in the mesh file + + +.. py:method:: DEMMesh.WriteWavefront + + .. code-block:: text + + WriteWavefront(self: str, output_file: collections.abc.Sequence[deme._deme.DEMMesh]) -> None + + Write the mesh geometry to a Wavefront OBJ file. + + + +DEMTrackedObj +------------- + +.. py:class:: DEMTrackedObj + + .. code-block:: text + + No class description is currently available. + +.. py:method:: DEMTrackedObj.__init__ + + .. code-block:: text + + __init__(self: deme._deme.DEMTrackedObj, arg0: deme._deme.DEMTrackedObj) -> None + + + +FORCE_MODEL +----------- + +.. py:class:: FORCE_MODEL + + .. code-block:: text + + Members: + + HERTZIAN + + HERTZIAN_FRICTIONLES + + CUSTOM + +.. py:method:: FORCE_MODEL.__init__ + + .. code-block:: text + + __init__(self: deme._deme.FORCE_MODEL, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: FORCE_MODEL.CUSTOM + + .. code-block:: text + + Members: + + HERTZIAN + + HERTZIAN_FRICTIONLES + + CUSTOM + +.. py:attribute:: FORCE_MODEL.HERTZIAN + + .. code-block:: text + + Members: + + HERTZIAN + + HERTZIAN_FRICTIONLES + + CUSTOM + +.. py:attribute:: FORCE_MODEL.HERTZIAN_FRICTIONLES + + .. code-block:: text + + Members: + + HERTZIAN + + HERTZIAN_FRICTIONLES + + CUSTOM + + +GridSampler +----------- + +.. py:class:: GridSampler + + .. code-block:: text + + Regular-grid volumetric point sampler. + +.. py:method:: GridSampler.__init__ + + .. code-block:: text + + __init__(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Create a regular-grid sampler with uniform point spacing. + + +.. py:method:: GridSampler.GetSeparation + + .. code-block:: text + + GetSeparation(self: deme._deme.GridSampler) -> float + + Return the current grid spacing. + + +.. py:method:: GridSampler.SampleBox + + .. code-block:: text + + SampleBox(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + + Return points sampled from the specified box volume. + + +.. py:method:: GridSampler.SampleCylinderX + + .. code-block:: text + + SampleCylinderX(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from an X-aligned cylindrical volume. + + +.. py:method:: GridSampler.SampleCylinderY + + .. code-block:: text + + SampleCylinderY(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Y-aligned cylindrical volume. + + +.. py:method:: GridSampler.SampleCylinderZ + + .. code-block:: text + + SampleCylinderZ(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Z-aligned cylindrical volume. + + +.. py:method:: GridSampler.SampleSphere + + .. code-block:: text + + SampleSphere(self: deme._deme.GridSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from the specified spherical volume. + + +.. py:method:: GridSampler.SetSeparation + + .. code-block:: text + + SetSeparation(self: deme._deme.GridSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the point spacing used by subsequent samples. + + + +HCPSampler +---------- + +.. py:class:: HCPSampler + + .. code-block:: text + + Hexagonally close-packed volumetric point sampler. + +.. py:method:: HCPSampler.__init__ + + .. code-block:: text + + __init__(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Create an HCP sampler with the requested point separation. + + +.. py:method:: HCPSampler.GetSeparation + + .. code-block:: text + + GetSeparation(self: deme._deme.HCPSampler) -> float + + Return the current point separation. + + +.. py:method:: HCPSampler.SampleBox + + .. code-block:: text + + SampleBox(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + + Return points sampled from the specified box volume. + + +.. py:method:: HCPSampler.SampleCylinderX + + .. code-block:: text + + SampleCylinderX(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from an X-aligned cylindrical volume. + + +.. py:method:: HCPSampler.SampleCylinderY + + .. code-block:: text + + SampleCylinderY(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Y-aligned cylindrical volume. + + +.. py:method:: HCPSampler.SampleCylinderZ + + .. code-block:: text + + SampleCylinderZ(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Z-aligned cylindrical volume. + + +.. py:method:: HCPSampler.SampleSphere + + .. code-block:: text + + SampleSphere(self: deme._deme.HCPSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from the specified spherical volume. + + +.. py:method:: HCPSampler.SetSeparation + + .. code-block:: text + + SetSeparation(self: deme._deme.HCPSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the point separation used by subsequent samples. + + + +MESH_FORMAT +----------- + +.. py:class:: MESH_FORMAT + + .. code-block:: text + + Members: + + VTK + + OBJ + + STL + + PLY + +.. py:method:: MESH_FORMAT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.MESH_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: MESH_FORMAT.OBJ + + .. code-block:: text + + Members: + + VTK + + OBJ + + STL + + PLY + +.. py:attribute:: MESH_FORMAT.PLY + + .. code-block:: text + + Members: + + VTK + + OBJ + + STL + + PLY + +.. py:attribute:: MESH_FORMAT.STL + + .. code-block:: text + + Members: + + VTK + + OBJ + + STL + + PLY + +.. py:attribute:: MESH_FORMAT.VTK + + .. code-block:: text + + Members: + + VTK + + OBJ + + STL + + PLY + + +OUTPUT_CONTENT +-------------- + +.. py:class:: OUTPUT_CONTENT + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:method:: OUTPUT_CONTENT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OUTPUT_CONTENT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OUTPUT_CONTENT.ABS_ACC + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.ABSV + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.ACC + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.ANG_ACC + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.ANG_VEL + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.FAMILY + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.MAT + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.OWNER_WILDCARD + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.QUAT + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.VEL + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + +.. py:attribute:: OUTPUT_CONTENT.XYZ + + .. code-block:: text + + Members: + + XYZ + + QUAT + + ABSV + + VEL + + ANG_VEL + + ABS_ACC + + ACC + + ANG_ACC + + FAMILY + + MAT + + OWNER_WILDCARD + + +OUTPUT_FORMAT +------------- + +.. py:class:: OUTPUT_FORMAT + + .. code-block:: text + + Members: + + CSV + + BINARY + +.. py:method:: OUTPUT_FORMAT.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OUTPUT_FORMAT, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OUTPUT_FORMAT.BINARY + + .. code-block:: text + + Members: + + CSV + + BINARY + +.. py:attribute:: OUTPUT_FORMAT.CSV + + .. code-block:: text + + Members: + + CSV + + BINARY + + +OWNER_TYPE +---------- + +.. py:class:: OWNER_TYPE + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:method:: OWNER_TYPE.__init__ + + .. code-block:: text + + __init__(self: deme._deme.OWNER_TYPE, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: OWNER_TYPE.ANALYTICAL + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:attribute:: OWNER_TYPE.CLUMP + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + +.. py:attribute:: OWNER_TYPE.MESH + + .. code-block:: text + + Members: + + CLUMP + + ANALYTICAL + + MESH + + +PDSampler +--------- + +.. py:class:: PDSampler + + .. code-block:: text + + Poisson-disk volumetric sampler enforcing a minimum point separation. + +.. py:method:: PDSampler.__init__ + + .. code-block:: text + + __init__(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Create a sampler with the requested minimum separation. + + +.. py:method:: PDSampler.GetSeparation + + .. code-block:: text + + GetSeparation(self: deme._deme.PDSampler) -> float + + Return the current minimum separation. + + +.. py:method:: PDSampler.SampleBox + + .. code-block:: text + + SampleBox(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], halfDim: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> list[list[float]] + + Return points sampled from the specified box volume. + + +.. py:method:: PDSampler.SampleCylinderX + + .. code-block:: text + + SampleCylinderX(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from an X-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleCylinderY + + .. code-block:: text + + SampleCylinderY(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Y-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleCylinderZ + + .. code-block:: text + + SampleCylinderZ(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex, halfHeight: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from a Z-aligned cylindrical volume. + + +.. py:method:: PDSampler.SampleSphere + + .. code-block:: text + + SampleSphere(self: deme._deme.PDSampler, center: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], radius: typing.SupportsFloat | typing.SupportsIndex) -> list[list[float]] + + Return points sampled from the specified spherical volume. + + +.. py:method:: PDSampler.SetRandomEngineSeed + + .. code-block:: text + + SetRandomEngineSeed(self: deme._deme.PDSampler, seed: typing.SupportsInt | typing.SupportsIndex) -> None + + Seed the random engine so Poisson-disk samples can be reproduced. + + +.. py:method:: PDSampler.SetSeparation + + .. code-block:: text + + SetSeparation(self: deme._deme.PDSampler, separation: typing.SupportsFloat | typing.SupportsIndex) -> None + + Set the minimum separation used by subsequent samples. + + + +SPATIAL_DIR +----------- + +.. py:class:: SPATIAL_DIR + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:method:: SPATIAL_DIR.__init__ + + .. code-block:: text + + __init__(self: deme._deme.SPATIAL_DIR, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: SPATIAL_DIR.NONE + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.X + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.Y + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + +.. py:attribute:: SPATIAL_DIR.Z + + .. code-block:: text + + Members: + + X + + Y + + Z + + NONE + + +TIME_INTEGRATOR +--------------- + +.. py:class:: TIME_INTEGRATOR + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:method:: TIME_INTEGRATOR.__init__ + + .. code-block:: text + + __init__(self: deme._deme.TIME_INTEGRATOR, value: typing.SupportsInt | typing.SupportsIndex) -> None + + +.. py:attribute:: TIME_INTEGRATOR.CENTERED_DIFFERENCE + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.CHUNG + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.EXTENDED_TAYLOR + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG + +.. py:attribute:: TIME_INTEGRATOR.FORWARD_EULER + + .. code-block:: text + + Members: + + FORWARD_EULER + + CENTERED_DIFFERENCE + + EXTENDED_TAYLOR + + CHUNG diff --git a/docs/python/solver-lifecycle.rst b/docs/python/solver-lifecycle.rst new file mode 100644 index 00000000..e7bac6d4 --- /dev/null +++ b/docs/python/solver-lifecycle.rst @@ -0,0 +1,73 @@ +Solver lifecycle +================ + +A Python simulation follows four phases. Keeping this boundary explicit avoids +rebuilding setup data during time stepping and prevents access to state that +does not exist yet. + +1. Construct the solver +----------------------- + +Choose worker device placement when constructing ``DEMSolver``. Device +placement cannot be changed later. See `CUDA device selection `__. + +2. Describe the system +---------------------- + +Before initialization: + +* load materials and clump or mesh templates; +* set the domain and boundary conditions; +* add clumps, meshes, or external objects; +* configure gravity, the force model, timestep, output, and family behavior; +* create trackers for batches or objects whose state will be queried. + +Setup methods return native-backed handles. Retain the solver for at least as +long as any of these handles. + +3. Initialize +------------- + +Call ``Initialize()`` once after setup: + +.. code-block:: python + + solver.SetInitTimeStep(1.0e-5) + solver.Initialize() + +Initialization resolves cached topology and prepares runtime/JIT CUDA data. +Methods documented as post-initialization operations must not be used earlier. + +Python processes use the same persistent Jitify header cache as C++ programs. +Set ``DEME_PERSISTENT_JITIFY_CACHE`` in the shell that launches Python, or set +``os.environ["DEME_PERSISTENT_JITIFY_CACHE"]`` before importing ``deme`` and +calling ``Initialize()``. Use an explicit, user-owned filename; unset the +variable to retain normal non-persistent Jitify behavior. + +To compare cold and warm initialization in separate Python processes, run +``python/demos/jitify_cache_timing.py`` from the repository root twice with the +same cache filename. Remove the cache before the first run so that it measures +header discovery rather than reuse. + +4. Advance, synchronize, and retrieve +-------------------------------------- + +``DoDynamics(duration)`` advances for a duration and may return before all +worker activity needed by a subsequent host query is synchronized. +``DoDynamicsThenSync(duration)`` is the convenient choice when the next action +reads or modifies simulation state. ``DoStepDynamics()`` advances one timestep. + +Trackers provide scalar and bulk host access: + +.. code-block:: python + + solver.DoDynamicsThenSync(0.01) + first_position = tracker.Pos() + all_positions = tracker.Positions() + +They can also write directly to caller-owned CUDA allocations; see +`Retrieving simulation data `__. + +Output methods may queue work. Call ``WaitForPendingOutput()`` before relying +on all requested files being complete. The solver destructor also waits for +pending output, but explicit synchronization makes program behavior clearer. diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..ca7e06ee --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,24 @@ +Quickstart +========== + +Python +------ + +The `Python quickstart `__ contains a complete runnable sphere simulation. +The Python guide also covers the setup/runtime boundary, multi-GPU device +selection, tracker access, and direct retrieval into CUDA arrays. + +C++ +--- + +Include the public API and construct a solver: + +.. code-block:: cpp + + #include "DEM/API.h" + + deme::DEMSolver solver; + solver.InstructBoxDomainDimension(1.0f, 1.0f, 1.0f); + solver.SetGravitationalAcceleration(make_float3(0.0f, 0.0f, -9.81f)); + +See `C++ API reference `__ for the generated API reference. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..a99a2c1d --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=7.0 +breathe>=4.35 +furo>=2024.1.29 diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst new file mode 100644 index 00000000..f56ae492 --- /dev/null +++ b/docs/troubleshooting.rst @@ -0,0 +1,93 @@ +Troubleshooting +=============== + +For numeric contact, geometry, and owner codes seen in logs or a debugger, +see `Internal type codes for debugging `__. + +CUDA or PTX mismatch +-------------------- + +Errors such as ``CUDA_ERROR_UNSUPPORTED_PTX_VERSION`` usually indicate that the +installed NVIDIA driver cannot execute code produced by the selected CUDA +Toolkit. Verify the driver/toolkit compatibility and rebuild with a supported +toolkit. + +Runtime JIT compilation cannot find headers +------------------------------------------- + +For binary wheels, install ``"deme[cuda12]"`` to supply matching libraries and +headers. A complete pip CUDA installation takes precedence over system CUDA +discovery inside the Python extension only. It does not export CUDA paths to +child processes. If another GPU package has already loaded a different NVRTC +version, use compatible dependencies in a fresh Python process or separate +environment. + +DEME compiles kernels at runtime. Confirm that the CUDA Toolkit headers and the +``share/DEME/kernel`` and ``include`` resources installed with DEME are +available. The CUDA header major and minor version must match the loaded NVRTC +library. If several CUDA Toolkits are installed, set ``CUDA_HOME`` to the one +providing that NVRTC version, for example: + +.. code-block:: console + + export CUDA_HOME=/usr/local/cuda-12.8 + +Errors in CUDA or CURAND headers involving undefined internal identifiers often +mean that an unversioned ``/usr/local/cuda`` link selected headers from a newer +Toolkit than the loaded ``libnvrtc``. Current DEME releases reject that mismatch +and report both the required NVRTC version and how to select matching headers. + +Import works but initialization fails +------------------------------------- + +``import deme`` verifies only that the extension and its immediate shared +libraries can load. A meaningful installation test must construct a solver and +run a small simulation through ``Initialize()`` so NVRTC, kernel resources, and +the GPU driver are exercised. + +Conda ``GLIBCXX`` errors +------------------------ + +Build with compilers compatible with the target Conda environment. A wheel +built against a newer system ``libstdc++`` may import on the build host but fail +inside another environment. + +Stale runtime kernels +--------------------- + +After changing kernel or force-model sources, use a clean build or ensure the +runtime kernel assets have been refreshed. Old copied text sources can make the +runtime behavior disagree with the compiled host code. + +Persistent Jitify startup cache +------------------------------- + +DEME uses Jitify and NVRTC to discover CUDA headers and compile runtime kernels during solver initialization. Workloads +that repeatedly start fresh DEME processes can persist the discovered header sources and avoid repeating much of that +startup work. Set ``DEME_PERSISTENT_JITIFY_CACHE`` to a writable cache-file path: + +.. code-block:: console + + export DEME_PERSISTENT_JITIFY_CACHE="$HOME/.cache/deme/jitify_header_cache.bin" + mkdir -p "$(dirname "$DEME_PERSISTENT_JITIFY_CACHE")" + + +Values such as ``1``, ``true``, ``on``, and ``yes`` select an automatic temporary path. Values such as ``0``, ``false``, ``off``, and +``no``, as well as an unset variable, disable the persistent cache. The cache is opt-in because an automatic shared +temporary path can be unsafe on multi-user systems, concurrent writers can contend, and header contents can change +without their paths or CUDA version changing. Prefer an explicit, user-owned path and remove the file after changing +CUDA toolchains or headers. An unreadable, incompatible, or unwritable cache falls back to normal Jitify discovery. + + +Custom force models +------------------- + +If a custom force kernel fails to launch, try a smaller block size such as +``SetForceCalcThreadsPerBlock(128)`` during setup. Large kernels can exceed +register limits. Check CUDA errors before treating missing contacts or a +subsequent too-many-geometries-in-bin error as a geometry problem. + +For JIT errors involving material arrays or missing wildcard variables, +compare your model declarations with ``DEMdemo_Electrostatic``. Declare pairwise +material properties with ``SetMustPairwiseMatProp`` and contact wildcards with +``SetPerContactWildcards`` before initialization. diff --git a/docs/visualization.rst b/docs/visualization.rst new file mode 100644 index 00000000..3f87853f --- /dev/null +++ b/docs/visualization.rst @@ -0,0 +1,278 @@ +Interactive visualization +========================= + +``DEMVisualizer`` is a native OpenGL 3.3 simulation inspector with a Dear ImGui +sidebar and raylib window/input support. It is enabled in ordinary C++ builds +and Python wheels by default. Existing visualizer calls remain supported. +The viewer does not advance simulation time: each call to ``Render`` +synchronously captures and displays the solver state at that moment. + +Recommended scope +----------------- + +Use the interactive viewer for **proof-of-concept (PoC) work and small-scale +simulations**: checking geometry placement, inspecting motion, and exploring +solver settings. It is not recommended as an always-on viewer for large +production runs or throughput benchmarks. For those workloads, run without +interactive rendering, write output at a suitable interval, and inspect it +later in ParaView. There is no universal particle-count cutoff: mesh complexity, +sphere detail, hardware, and display frequency all affect the cost. + +Sphere components use a shared instanced mesh. Triangle geometry stays in +persistent indexed buffers in owner-local coordinates. Each display frame +uploads owner poses and colors, rather than reconstructing component positions +and triangle vertices on the CPU. Geometry uploads occur after initialization, +``Update()``, mesh deformation (including wear), or a sphere-detail change. + +Writing a responsive visualization loop +--------------------------------------- + +``Render()`` handles mouse and keyboard input as well as drawing. The target FPS +is a frame-rate limit, not a background event loop: it cannot make the window +respond while your application is inside a blocking solver call or file write. +A simulation-time interval such as ``DoDynamicsThenSync(1.0 / 60.0)`` can take +seconds of wall time to compute; it does not imply a 60 Hz display. + +Use short dynamics advances and a **wall-clock** display deadline, independently +of simulation-time file-output intervals. The examples below advance one solver +timestep per call. They check the deadline between calls, keep drawing while +paused, and consume ``ShouldStep()`` exactly once per intended advance. A paused +single-step therefore advances one timestep. Check for window closure again +after rendering so closing the viewer does not advance another step. + +Schedule the next deadline **after rendering finishes**. The renderer's FPS +limiter may sleep for a whole frame; scheduling from the start can make every +subsequent timestep immediately trigger another render, leaving little time +for physics. A 16 ms work interval below is a starting point, not a guaranteed +60 FPS rate. Increase it to favor simulation throughput. Keep your physical +timestep unchanged when tuning the display frequency. + +C++ usage +~~~~~~~~~ + +After configuring and initializing ``solver``: + +.. code-block:: cpp + + #include + #include "DEM/API.h" + #include "DEM/utils/DEMVisualizer.h" + + deme::DEMVisualizer visualizer(solver); + visualizer.SetTargetFPS(60); + visualizer.Initialize(); + + using Clock = std::chrono::steady_clock; + auto next_display = Clock::now(); + const auto work_interval = std::chrono::milliseconds(16); + // Match the solver's internal float timestep; a slightly larger double + // duration could request an extra step. This example uses a fixed timestep. + const float dt = static_cast(solver.GetTimeStepSize()); + + while (visualizer.Run()) { + if (visualizer.IsPaused() || Clock::now() >= next_display) { + visualizer.Render(); + next_display = Clock::now() + work_interval; + } + if (!visualizer.Run()) + break; + if (visualizer.ShouldStep()) + solver.DoDynamics(dt); + } + solver.DoDynamicsThenSync(0.0); // Join both workers at the end. + visualizer.Close(); + +Python usage +~~~~~~~~~~~~ + +After configuring and initializing ``solver``: + +.. code-block:: python + + import time + import numpy as np + import deme + + visualizer = deme.DEMVisualizer(solver) + visualizer.SetTargetFPS(60) + visualizer.Initialize() + + next_display = time.monotonic() + work_interval = 0.016 + # Round to the internal float timestep before passing a Python float. + # This example uses a fixed timestep. + dt = float(np.float32(solver.GetTimeStepSize())) + + try: + while visualizer.Run(): + if visualizer.IsPaused() or time.monotonic() >= next_display: + visualizer.Render() + next_display = time.monotonic() + work_interval + if not visualizer.Run(): + break + if visualizer.ShouldStep(): + solver.DoDynamics(dt) + finally: + solver.DoDynamicsThenSync(0.0) + visualizer.Close() + +``DoDynamics`` returns after the dynamic worker finishes the requested advance; +these loops capture its state on the same application thread afterwards. They +do not need to reset both workers with ``DoDynamicsThenSync`` at every display +refresh. Do not run ``Render`` concurrently with dynamics from another thread. +For a finite simulation, also add your simulation-end condition to the loop. +If you change the solver timestep at runtime, refresh ``dt`` accordingly. + +Spheres and triangles are both rendered by default. They can be controlled +independently before or during the visualization loop: + +.. code-block:: python + + visualizer.SetRenderSpheres(False) + visualizer.SetRenderTriangles(True) + +Performance tradeoffs +--------------------- + +Persistent geometry reduces rebuilding work, but the viewer is not free: + +* Each ``Render`` synchronously transfers owner state to the CPU, prepares + colors, and uploads frame data to OpenGL. Speed coloring and inspecting a + selected owner also request velocity data. Paused redraws still capture state. +* Sphere detail and mesh facet counts affect drawing cost and GPU memory use. + Geometry changes, including mesh deformation and wear, trigger geometry + uploads. Hiding geometry reduces drawing work but does not eliminate the + frame capture for all owners. +* Frequent calls across the solver boundary, particularly from Python, add + overhead. Drawing, state transfers, and the FPS limiter share the application's + time with simulation. More responsive interaction can mean a substantially + slower overall run, even with few particles. +* A long individual solver step, expensive output operation, or large frame + transfer can still stall input. The wall-clock deadline is checked only when + control returns to the application; rendering is not asynchronous. + +Measure representative runs with and without calls to ``Render`` using the +same physics and output settings. Compare wall time per simulated second, +not just the viewer's displayed FPS. In one development run, +``SingleSphereCollide`` took about 287 seconds with output-frame-only rendering +and 521 seconds with frequent interactive updates (about 1.8 times as long). +That illustrates the tradeoff, not a portable benchmark or expected slowdown +for other scenes or machines. + +For PoC work, start with a small scene, reduce sphere detail, and lengthen the +work interval if throughput matters more than camera responsiveness. Pausing +is useful for inspection, but still incurs redraw costs. For large or long +runs, omit the viewer loop and use file output and postprocessing instead. +Removing the viewer at build time is optional; see the headless build option +below. Moving rendering to a background thread is not a supported shortcut. + +Inspection controls +------------------- + +* Left drag: orbit around the camera target. Middle drag: pan. Wheel: zoom. +* ``F``: frame visible geometry. ``Shift+F``: frame the selected owner. +* Right click: select a sphere component or mesh triangle using depth-tested + geometry IDs. The sidebar reports its owner, family, position, orientation, + and linear velocity. Hidden geometry cannot be selected. +* ``Space``: pause/resume. ``.``: request one application-defined dynamics advance while + paused (one solver timestep in the loops above). **The application must honor** ``ShouldStep()`` **as shown above.** + It consumes one pending step request; call it once per intended advance. +* ``F12`` or the screenshot button: save ``deme-screenshot.png`` in the current + directory. ``RequestScreenshot(path)`` saves the next rendered frame elsewhere. +* ``Esc``: close the window. The three existing visualizer demos continue their + simulation/output after closing the window. + +The sidebar controls sphere/mesh visibility, wireframe, the XY reference grid, +sphere detail, per-family visibility/colors, and coloring by owner height or +linear speed. Scalar modes include an automatic or manually specified range +and a color legend. Height uses the owner center, not individual component +centers. Values are in the simulation's length/time units (the labels assume SI). +Analytical boundaries currently remain outside this viewer's geometry interface; +use analytical VTK output for those surfaces. + +These controls also have C++/Python entry points: ``SetPaused``, ``RequestStep``, +``SetFamilyVisible``, ``SetColorMode``, ``FrameAll``, ``FrameSelected``, and +``PickAt``. Python color modes are ``deme.VisualizerColorMode.FAMILY``, ``HEIGHT``, +and ``SPEED``; C++ uses ``deme::DEMVisualizerColorMode``. ``PickAt`` uses logical +window coordinates and the last rendered frame. ``GetSelectedOwner`` returns +``NULL_BODYID`` (the maximum 32-bit unsigned value) when nothing is selected; +``GetSelectedGeometryID`` returns ``SIZE_MAX`` in that case. Selected geometry +indices are global sphere or triangle indices, distinguished by +``IsSelectedSphere()``. + +Camera settings supplied before initialization are preserved; otherwise the +first frame automatically fits the scene. The camera, frame buffers, and GUI +must be used on the thread owning the window. Only one native viewer window +may be open in a process. Pause does not block ``Render``; keep rendering to +handle input while paused. The existing visualizer demos illustrate display +updates between solver calls, independently of file-output intervals. For +custom applications, use the loop pattern above and check ``ShouldStep()`` +before every advance, including advances between display deadlines. + +Scene/frame interface +--------------------- + +``GetVisualizationScene()`` returns local sphere offsets/radii, local triangle +vertices, owner mappings, and a geometry revision. Cache it until the revision +in ``GetVisualizationFrame(frame, include_velocities=false)`` changes. +``DEMVisualizationFrame`` contains reusable vectors indexed by owner ID: +positions, quaternions (x, y, z, w), families, and optional velocities. Python +uses ``deme.VisualizationFrame()`` and the same solver methods. Python field +access converts vectors to Python values; the native viewer uses the reusable +C++ buffers directly. + +Capture scene/frame data only at a synchronized solver boundary. Runtime family +or rigid-pose changes update the frame without invalidating geometry. Runtime +mesh changes through solver/tracker APIs invalidate the scene even when the +facet count is unchanged. ``GetVisualizationSnapshot`` remains available for +consumers needing expanded world-space geometry. + +This implementation uses synchronous host state transfer. CUDA/OpenGL interop, +asynchronous rendering, sphere impostors, contact overlays, and force dragging +are not part of this viewer version. + +Verification +------------ + +``DEMTest_VisualizationScene`` checks local-to-world transforms, combined-owner +members, family/velocity updates, deformation invalidation, and insertion. +``DEMTest_Visualizer`` additionally requires CUDA and a display; it exercises +rendering, occlusion-aware picking, filters, stepping requests, window resize, +and close/reopen. Pass a PNG filename to save a frame for inspection:: + + ./build/bin/DEMTest_Visualizer /tmp/deme-visualizer.png + +The existing ``DEMdemo_SingleSphereCollide``, ``DEMdemo_MeshCollide``, and +``DEMdemo_MeshFalling`` continue to use the viewer and honor pause/single-step. + +Linux build requirements +------------------------ + +Dear ImGui v1.91.9b is vendored with its MIT license; builds require no additional +download for the UI. The source build uses raylib's bundled GLFW backend and requires +the standard X11 and OpenGL development headers. On Debian or Ubuntu: + +.. code-block:: console + + sudo apt-get install libx11-dev libxrandr-dev libxinerama-dev \ + libxcursor-dev libxi-dev libgl1-mesa-dev + +The GitHub wheel workflow installs the equivalent packages in its manylinux +container. At runtime, a graphical display and OpenGL implementation must be +available. WSL users should run under WSLg or configure ``DISPLAY`` to an X +server. + +The visualizer can be omitted for a headless source build with: + +.. code-block:: console + + cmake -S . -B build -DDEME_BUILD_VISUALIZER=OFF + +Postprocessing with ParaView +---------------------------- + +Load output ``.vtk`` mesh files directly. For component-sphere CSV output from +``WriteSphereFile``, apply **Table To Points** using the ``X``, ``Y``, and ``Z`` +columns. Then apply **Glyph**, choose **Sphere**, select ``r`` as the scale +array, set the scale factor to 2 (radii become diameters), and choose +**All Points**. Reduce sphere resolution when visualizing large datasets. diff --git a/pyproject.toml b/pyproject.toml index e8077ae0..d8869827 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "scikit_build_core.build" [project] name = "deme" dynamic = ["version"] -description = "Python bindings for DEM-Engine 2, a GPU-accelerated discrete element method engine" +description = "Python bindings for DEM-Engine - a GPU-accelerated Discrete Element Method simulation engine" readme = "README.md" authors = [ { name = "Ruochun Zhang", email = "ruochunz@gmail.com" }, @@ -29,10 +29,23 @@ classifiers = [ "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Physics", ] -dependencies = ["numpy>=1.19.0"] +dependencies = [ + "numpy>=1.19.0", +] [project.optional-dependencies] -dev = ["pytest>=6.0", "pybind11>=2.11.0"] +# Keep runtime headers, compiler headers, and NVRTC on the wheel build's CUDA minor version. +cuda12 = [ + "nvidia-cuda-runtime-cu12>=12.9,<12.10", + "nvidia-cuda-nvrtc-cu12>=12.9,<12.10", + "nvidia-cuda-nvcc-cu12>=12.9,<12.10", + "nvidia-cuda-cccl-cu12>=12.9,<12.10", + "nvidia-curand-cu12>=10.3.10,<10.4", +] +dev = [ + "pytest>=6.0", + "pybind11>=2.11.0", +] [project.urls] Homepage = "https://github.com/projectchrono/DEM-Engine" @@ -45,6 +58,7 @@ cmake.verbose = true minimum-version = "0.8" build-dir = "build/{wheel_tag}" +# Read the distribution version from the same source used by standalone CMake. [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" input = "VERSION" @@ -52,15 +66,21 @@ regex = '''^(?P\d+\.\d+\.\d+)\s*$''' [tool.scikit-build.cmake.define] DEME_BUILD_PYTHON = "ON" +DEME_BUILD_VISUALIZER = "ON" +# Demos and modular tests are useful in developer builds but are not wheel content. DEME_BUILD_DEMOS = "OFF" -USE_HIP = "OFF" [tool.cibuildwheel] +# DEME contains a CPython extension, so every supported interpreter needs its own wheel. build = "cp3{9,10,11,12,13,14}-manylinux_x86_64" skip = "*-musllinux_*" build-verbosity = 1 [tool.cibuildwheel.linux] +# Build on glibc 2.28 with CUDA 12.9. The cuda12 extra supplies runtime compilation dependencies. +# Keep NVIDIA components external to this wheel; never bundle the build-time driver stub. manylinux-x86_64-image = "quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9:latest" -before-all = "dnf install -y cuda-nvrtc-12-9 cuda-nvrtc-devel-12-9 && dnf clean all" +# The current CUDA 12.9 image can omit NVRTC despite providing nvcc and cudart. DEME links NVRTC for runtime kernel +# compilation, so install both the shared library and its development symlink before any interpreter build starts. +before-all = "dnf install -y cuda-nvrtc-12-9 cuda-nvrtc-devel-12-9 libX11-devel libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel mesa-libGL-devel && dnf clean all" repair-wheel-command = "auditwheel repair --plat manylinux_2_28_x86_64 --exclude libcuda.so.1 --exclude libcudart.so.12 --exclude libnvrtc.so.12 -w {dest_dir} {wheel}" diff --git a/python/demos/README.md b/python/demos/README.md new file mode 100644 index 00000000..fed7f4d3 --- /dev/null +++ b/python/demos/README.md @@ -0,0 +1,90 @@ +# Python demos + +These scripts use `import deme` and the data and kernels shipped with the Python +wheel. Install the package into your active environment: + +```sh +python -m pip install 'deme[cuda12]' +``` + +The `cuda12` extra supplies CUDA 12 libraries and headers +for Linux, including WSL2. A compatible NVIDIA GPU driver is still required. +First-time solver initialization compiles CUDA kernels and can take tens of +minutes for these scenes. Cached runs are much faster; a short smoke duration +does not reduce this compilation work. + +Run these commands from a repository checkout. The scripts resolve simulation +assets through the installed package, so they also work from another working +directory when invoked by their absolute path. No additional Python packages are +needed beyond the DEME installation. + +| Script | C++ source | What it demonstrates | +| --- | --- | --- | +| `single_sphere_collide.py` | `src/demo/DEMdemo_SingleSphereCollide.cpp` | Two spheres, moving and fixed meshes, cohesive contact, pairwise material properties, runtime insertion with `UpdateClumps()`, tracking and contact output | +| `ball_drop.py` | `src/demo/DEMdemo_BallDrop.cpp` | A polydisperse bed, meshed projectile, settling, family-based release, and penetration measurement | +| `centrifuge.py` | `src/demo/DEMdemo_Centrifuge.cpp` | Ellipsoid clumps and equal-mass spheres at three densities, an analytical drum with end caps, prescribed rotation, and contact torque | +| `jitify_cache_timing.py` | — | Kernel compilation cache timing; requires the `DEME_PERSISTENT_JITIFY_CACHE` environment variable | + +```sh +python python/demos/single_sphere_collide.py +python python/demos/ball_drop.py --density 7800 --drop-height 0.05 +python python/demos/centrifuge.py --duration 2 +``` + +The three physics demos run headlessly. Use `--device 0` to select a GPU, +`--duration SECONDS` to change the simulated duration, and `--output-dir PATH` +to choose an output directory. Existing files with the same names are overwritten. +Use `--help` for all options; it works without loading CUDA. + +## Scope of the ports + +- **Single sphere collision:** retains the C++ demo's sphere masses, radii, initial + velocities, mesh positions, cohesive material parameters, and time step. The + second sphere is added after `Initialize()` via `UpdateClumps()` (the current + Python alias for the C++ `Update()` operation). Redundant API regression exercises + and the interactive viewer are omitted to keep the example readable. +- **Ball drop:** runs one impact instead of the C++ parameter sweep. The default + bed is 8 cm wide and initially 4 cm deep, with grain radii 2.125–2.5 mm on a + seeded HCP mixture. The projectile radius (12.7 mm), material parameters, and + time step follow C++. Adjust `--settle-time` as needed; the default 0.5 s is a + demonstration setting, not a guarantee of an equilibrated bed. The reported + penetration is measured at the final requested time, not necessarily at rest. +- **Centrifuge:** keeps the 2 m drum radius, 1 m nominal height, 6 rad/s rotation, + and C++ time step. The default particle scale is 0.05 m instead of 0.01 m to + reduce particle count. End caps share the drum owner, so reported torque is + the total for the cylinder and caps. The initial grid leaves clearance for the + ellipsoids. To use the C++ particle scale and duration: + + ```sh + python python/demos/centrifuge.py --particle-scale 0.01 --duration 20 + ``` + +## Short checks + +```sh +python python/demos/single_sphere_collide.py --smoke-test +python python/demos/ball_drop.py --smoke-test +python python/demos/centrifuge.py --smoke-test +``` + +These options override the duration (and, for the bed/drum, particle size). The +sphere check runs 0.3 s to include the sphere collision. The ball-drop check uses +0.02 s of settling followed by a 0.04 s drop from 1 mm above the bed. The drum +check uses 0.1 m particles and runs 0.03 s, long enough for the lowest +ellipsoids to reach the lower cap. Each script checks a final position or maximum speed for non-finite values. These are setup/runtime checks, not validation of +settled-bed behavior or centrifugal segregation, and still require kernel +compilation on a cold cache. + +## Viewing output + +The collision and ball-drop demos write frames every 0.01 s; the centrifuge +writes every 0.05 s. Frame zero is the initial state for that stage. The final +interval is shortened when the requested duration is not an exact multiple. + +Open the VTK sequences in ParaView. For `spheres_*.vtk`, apply **Glyph**, choose a +sphere, scale by the `r` field, and use a scale factor of 2 if the glyph source +has the default radius of 0.5. Meshes and analytical geometry +are separate VTK sequences. The bed and centrifuge write particle CSV files +with component positions and radii; use a CSV reader and **Table To Points** +followed by sphere glyphs. Contact CSV files contain the requested owner IDs, +forces, points, normals, torques, and history fields. diff --git a/python/demos/ball_drop.py b/python/demos/ball_drop.py new file mode 100644 index 00000000..1dd27591 --- /dev/null +++ b/python/demos/ball_drop.py @@ -0,0 +1,84 @@ +"""Drop a meshed projectile into a small granular bed (DEMdemo_BallDrop.cpp).""" + +import argparse +import math +from pathlib import Path +import random + + +def main(): + """Settle a reproducible bed, release a tracked mesh, and measure penetration.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--duration", type=float, default=0.5, help="Time after releasing the projectile, in seconds") + parser.add_argument("--settle-time", type=float, default=0.5) + parser.add_argument("--drop-height", type=float, default=0.05) + parser.add_argument("--density", type=float, default=7800.0, help="Projectile density in kg/m^3") + parser.add_argument("--output-dir", type=Path, default=Path("DemoOutput_BallDrop")) + parser.add_argument("--smoke-test", action="store_true", help="Use a coarser bed and short settling/drop stages") + args = parser.parse_args() + for name in ("duration", "settle_time", "density", "drop_height"): + value = getattr(args, name) + if not math.isfinite(value) or value <= 0: + parser.error(f"--{name.replace('_', '-')} must be finite and positive") + import deme + + solver = deme.DEMSolver([args.device]) + solver.SetVerbosity("ERROR") + solver.SetOutputFormat("CSV") + material = solver.LoadMaterial({"E": 7e7, "nu": 0.24, "CoR": 0.9, "mu": 0.3, "Crr": 0.0}) + solver.InstructBoxDomainDimension([-0.04, 0.04], [-0.04, 0.04], [0, 0.8]) + solver.InstructBoxDomainBoundingBC("top_open", material) + + # A seeded mixture on an HCP lattice avoids random initial overlaps. + grain_radius = 0.005 if args.smoke_test else 0.0025 + radii = [grain_radius * (0.85 + 0.03 * i) for i in range(6)] + templates = [solver.LoadSphereType(2500 * 4 * math.pi * r**3 / 3, r, material) for r in radii] + positions = deme.DEMBoxHCPSampler([0, 0, 0.02], [0.04 - grain_radius * 1.01] * 2 + + [0.02 - grain_radius * 1.01], 2.01 * grain_radius) + rng = random.Random(4150) + bed = solver.AddClumps([rng.choice(templates) for _ in positions], positions) + bed.SetFamily(0) + top = solver.CreateInspector("clump_max_z") + + radius = 0.0127 + mass = args.density * 4 * math.pi * radius**3 / 3 + projectile = solver.AddWavefrontMeshObject(deme.GetDEMEDataFile("mesh/sphere.obj"), material) + projectile.Scale(radius) + projectile.SetMass(mass) + projectile.SetMOI([0.4 * mass * radius**2] * 3) + projectile.SetInitPos([0, 0, 0.6]) + projectile.SetFamily(2) + solver.SetFamilyFixed(2) + solver.DisableContactBetweenFamilies(0, 2) + tracker = solver.Track(projectile) + solver.SetTimeStepSize(2e-6) + solver.SetGravitationalAcceleration([0, 0, -9.81]) + solver.SetMaxVelocity(30.0) + print("Initializing CUDA kernels (a cold cache can take tens of minutes)...", flush=True) + solver.Initialize() + print("Settling the bed...", flush=True) + solver.DoDynamicsThenSync(0.02 if args.smoke_test else args.settle_time) + + args.output_dir.mkdir(parents=True, exist_ok=True) + solver.WriteClumpFile(str(args.output_dir / "settled_bed.csv")) + bed_top = top.GetValue() + solver.ChangeFamily(2, 0) + tracker.SetPos([0, 0, bed_top + radius + (0.001 if args.smoke_test else args.drop_height)]) + print("Dropping the projectile...", flush=True) + duration = 0.04 if args.smoke_test else args.duration + frame_time = 0.01 + for frame in range(math.ceil(duration / frame_time) + 1): + if frame: + solver.DoDynamicsThenSync(min(frame_time, duration - (frame - 1) * frame_time)) + solver.WriteSphereFile(str(args.output_dir / f"bed_{frame:04d}.csv")) + solver.WriteMeshFile(str(args.output_dir / f"projectile_{frame:04d}.vtk")) + position = tracker.Pos() + if not all(math.isfinite(value) for value in position): + raise RuntimeError("Non-finite projectile position") + print(f"Finished: {len(positions)} grains, penetration={bed_top - (position[2] - radius):.6g} m") + print(f"Output: {args.output_dir.resolve()}") + + +if __name__ == "__main__": + main() diff --git a/python/demos/centrifuge.py b/python/demos/centrifuge.py new file mode 100644 index 00000000..a21cace7 --- /dev/null +++ b/python/demos/centrifuge.py @@ -0,0 +1,91 @@ +"""Rotate a drum containing spheres and ellipsoids (DEMdemo_Centrifuge.cpp).""" + +import argparse +import math +from pathlib import Path + + +def main(): + """Build six grain templates and prescribe rotation of an analytical drum.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--duration", type=float, default=2.0) + parser.add_argument("--particle-scale", type=float, default=0.05, help="Ellipsoid short semi-axis in meters") + parser.add_argument("--output-dir", type=Path, default=Path("DemoOutput_Centrifuge")) + parser.add_argument("--smoke-test", action="store_true", help="Use larger grains and run 0.03 s") + args = parser.parse_args() + if not math.isfinite(args.duration) or args.duration <= 0: + parser.error("--duration must be finite and positive") + if not math.isfinite(args.particle_scale) or not 0 < args.particle_scale <= 0.1: + parser.error("--particle-scale must be in (0, 0.1]") + import deme + + solver = deme.DEMSolver([args.device]) + solver.SetVerbosity("ERROR") + solver.SetOutputFormat("CSV") + solver.SetOutputContent(["FAMILY"]) + sand = solver.LoadMaterial({"E": 1e9, "nu": 0.3, "CoR": 0.6, "mu": 0.5, "Crr": 0.01}) + drum_material = solver.LoadMaterial({"E": 2e9, "nu": 0.3, "CoR": 0.6, "mu": 0.5, "Crr": 0.01}) + solver.SetMaterialPropertyPair("mu", sand, drum_material, 0.5) + scale = 0.1 if args.smoke_test else args.particle_scale + mass = 2600 * 4 * math.pi * 2 / 3 + ellipsoid = solver.LoadClumpType(mass, [mass, mass, 0.4 * mass], + deme.GetDEMEDataFile("clumps/ellipsoid_2_1_1.csv"), sand) + # Scale already updates geometry, mass (s^3), and inertia (s^5). + ellipsoid.Scale(scale) + templates = [] + for density_index in range(3): + grain = solver.Duplicate(ellipsoid) + multiplier = 1.5**density_index + grain.SetMass(ellipsoid.Mass() * multiplier) + grain.SetMOI([value * multiplier for value in ellipsoid.MOI()]) + templates.extend([grain, solver.LoadSphereType(grain.Mass(), 2**(1 / 3) * scale, sand)]) + + drum = solver.AddExternalObject() + drum.AddCylinder([0, 0, 0], [0, 0, 1], 2.0, drum_material) + drum.AddPlane([0, 0, -0.47], [0, 0, 1], drum_material) + drum.AddPlane([0, 0, 0.47], [0, 0, -1], drum_material) + drum.SetMass(1.0) + drum.SetMOI([13 / 12, 13 / 12, 2.0]) + drum.SetFamily(100) + # Keep the drum centered while prescribing its rotation about the cylinder axis. + solver.SetFamilyPrescribedLinVel(100, "0", "0", "0", False) + solver.SetFamilyPrescribedAngVel(100, "0", "0", "6.0") + tracker = solver.Track(drum) + # Keep even the corner grains clear of the cylindrical wall. + half_width = min(2 / 1.5, (2 - 2.01 * scale) / math.sqrt(2)) + # The sphere approximation extends 2.08 * scale along z, slightly beyond + # the nominal ellipsoid semi-axis; 2.1 leaves a small end-cap clearance. + positions = deme.DEMBoxGridSampler([0, 0, 0], [half_width, half_width, 0.47 - 2.1 * scale], + scale * 2**(1 / 3) * 2.1, scale * 2**(1 / 3) * 2.1, scale * 4.2) + grains = solver.AddClumps([templates[i % 6] for i in range(len(positions))], positions) + grains.SetFamilies([(i % 6) // 2 for i in range(len(positions))]) + speed = solver.CreateInspector("clump_max_absv") + solver.InstructBoxDomainDimension(5.0, 5.0, 5.0) + solver.SetTimeStepSize(5e-6) + solver.SetGravitationalAcceleration([0, 0, -9.81]) + solver.SetExpandSafetyType("auto") + # Broad-phase padding must include the analytical surface speed: omega * radius. + solver.SetExpandSafetyAdder(12.0) + print("Initializing CUDA kernels (a cold cache can take tens of minutes)...", flush=True) + solver.Initialize() + print("Running rotating drum...", flush=True) + args.output_dir.mkdir(parents=True, exist_ok=True) + duration = 0.03 if args.smoke_test else args.duration + frame_time = 0.05 + for frame in range(math.ceil(duration / frame_time) + 1): + if frame: + solver.DoDynamicsThenSync(min(frame_time, duration - (frame - 1) * frame_time)) + solver.WriteSphereFile(str(args.output_dir / f"grains_{frame:04d}.csv")) + solver.WriteAnalyticalFile(str(args.output_dir / f"drum_{frame:04d}.vtk")) + max_speed = speed.GetValue() + if not math.isfinite(max_speed): + raise RuntimeError("Non-finite grain speed") + print(f"Finished: {len(positions)} grains, maximum speed={max_speed:.6g} m/s") + torque = tuple(a * b for a, b in zip(tracker.ContactAngAccLocal(), [13 / 12, 13 / 12, 2.0])) + print(f"Drum contact torque (local): {torque}") + print(f"Output: {args.output_dir.resolve()}") + + +if __name__ == "__main__": + main() diff --git a/python/demos/jitify_cache_timing.py b/python/demos/jitify_cache_timing.py new file mode 100644 index 00000000..db7cdac2 --- /dev/null +++ b/python/demos/jitify_cache_timing.py @@ -0,0 +1,41 @@ +"""Time DEME initialization with an explicitly configured persistent Jitify cache.""" + +import os +from pathlib import Path +from time import perf_counter + + +cache_value = os.environ.get("DEME_PERSISTENT_JITIFY_CACHE") +if not cache_value: + raise SystemExit("Set DEME_PERSISTENT_JITIFY_CACHE to a user-owned cache file before running this example") +if cache_value.lower() in {"0", "1", "false", "true", "off", "on", "no", "yes"}: + raise SystemExit("Use an explicit cache filename for a reproducible cold-versus-warm timing comparison") + +# Set the environment before importing DEME so the same script also covers future initialization-order changes. +import deme + + +solver = deme.DEMSolver([0]) +material = solver.LoadMaterial( + { + "E": 1.0e7, + "nu": 0.3, + "CoR": 0.5, + "mu": 0.4, + "Crr": 0.0, + } +) +solver.InstructBoxDomainDimension((-0.5, 0.5), (-0.5, 0.5), (0.0, 1.0)) +solver.InstructBoxDomainBoundingBC("top_open", material) +sphere_type = solver.LoadSphereType(0.01, 0.025, material) +solver.AddClumps(sphere_type, [[0.0, 0.0, 0.5]]) +solver.SetInitTimeStep(1.0e-5) + +start = perf_counter() +solver.Initialize() +elapsed = perf_counter() - start + +cache_path = Path(cache_value).expanduser() +cache_size = cache_path.stat().st_size if cache_path.is_file() else 0 +print(f"Initialize: {elapsed:.3f} s") +print(f"Cache: {cache_path} ({cache_size} bytes)") diff --git a/python/demos/single_sphere_collide.py b/python/demos/single_sphere_collide.py new file mode 100644 index 00000000..5172ab7a --- /dev/null +++ b/python/demos/single_sphere_collide.py @@ -0,0 +1,92 @@ +"""Two colliding spheres over meshes, ported from DEMdemo_SingleSphereCollide.cpp.""" + +import argparse +import math +from pathlib import Path + + +def main(): + """Configure cohesion, insert a second sphere at runtime, and write synchronized frames.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--device", type=int, default=0) + parser.add_argument("--duration", type=float, default=1.0) + parser.add_argument("--output-dir", type=Path, default=Path("DemoOutput_SingleSphereCollide")) + parser.add_argument("--smoke-test", action="store_true", help="Run 0.3 s, including the sphere collision") + args = parser.parse_args() + if not math.isfinite(args.duration) or args.duration <= 0: + parser.error("--duration must be finite and positive") + import deme + + solver = deme.DEMSolver([args.device]) + solver.SetVerbosity("ERROR") + solver.SetOutputFormat("VTK") + solver.SetContactOutputContent(["OWNER", "FORCE", "POINT", "NORMAL", "TORQUE", "CNT_WILDCARD"]) + solver.SetPersistentContact(True) + solver.SetMeshUniversalContact(True) + sphere_material = solver.LoadMaterial( + {"E": 1e9, "nu": 0.3, "CoR": 0.8, "mu": 0.3, "Crr": 0.01, "Cohesion": 50.0}) + mesh_material = solver.LoadMaterial( + {"E": 2e9, "nu": 0.4, "CoR": 0.6, "mu": 0.3, "Crr": 0.01, "Cohesion": 50.0}) + solver.SetMaterialPropertyPair("CoR", sphere_material, mesh_material, 0.6) + solver.SetMaterialPropertyPair("Cohesion", sphere_material, mesh_material, 100.0) + sphere = solver.LoadSphereType(11728.0, 1.0, sphere_material) + first = solver.AddClumps(sphere, [[-1.2, 0, 0]]) + first.SetVel([[1, 0, 0]]) + first.SetFamily(0) + first_tracker = solver.Track(first) + + # The upper mesh moves freely; the lower mesh is a fixed support. + plane_file = deme.GetDEMEDataFile("mesh/plane_20by20.obj") + plane = solver.AddWavefrontMeshObject(plane_file, mesh_material) + plane.SetInitPos([0, 0, -1.25]) + plane.SetMass(10000.0) + support = solver.AddWavefrontMeshObject(plane_file, mesh_material) + support.SetInitPos([0, 0, -1.5]) + support.SetFamily(100) + solver.SetFamilyFixed(100) + energy = solver.CreateInspector("clump_kinetic_energy") + + # This packaged force model needs both contact history and pairwise properties. + # Installed kernels sit alongside the data directory; use an absolute path + # because the C++ convenience search may still point at the build machine. + model_file = deme.GetDEMEDataFile("../kernel/DEMUserScripts/ForceModelWithCohesion.cu") + force = solver.ReadContactForceModel(model_file) + force.SetPerContactWildcards({"delta_time", "delta_tan_x", "delta_tan_y", "delta_tan_z"}) + force.SetMustPairwiseMatProp({"CoR", "mu", "Crr", "Cohesion"}) + solver.SetTimeStepSize(2e-5) + solver.SetGravitationalAcceleration([0, 0, -9.8]) + solver.SetCDUpdateFreq(10) + solver.SetMaxVelocity(6.0) + solver.SetExpandSafetyType("auto") + solver.SetExpandSafetyMultiplier(1.2) + solver.SetIntegrator("centered_difference") + print("Initializing CUDA kernels (a cold cache can take tens of minutes)...", flush=True) + solver.Initialize() + + # UpdateClumps is the current Python binding of the C++ Update operation. + # It incorporates new clumps without rebuilding the simulation from scratch. + second = solver.AddClumps(sphere, [[1.2, 0, 0]]) + second.SetVel([[-1, 0, 0]]) + second.SetFamily(1) + second_tracker = solver.Track(second) + solver.UpdateClumps() + print("Running sphere collision...", flush=True) + args.output_dir.mkdir(parents=True, exist_ok=True) + duration = 0.3 if args.smoke_test else args.duration + frame_time = 0.01 + for frame in range(math.ceil(duration / frame_time) + 1): + if frame: + solver.MarkPersistentContact() + solver.DoDynamicsThenSync(min(frame_time, duration - (frame - 1) * frame_time)) + solver.WriteSphereFile(str(args.output_dir / f"spheres_{frame:04d}.vtk")) + solver.WriteMeshFile(str(args.output_dir / f"meshes_{frame:04d}.vtk")) + solver.WriteContactFile(str(args.output_dir / f"contacts_{frame:04d}.csv")) + positions = (first_tracker.Pos(), second_tracker.Pos()) + if not all(math.isfinite(value) for position in positions for value in position): + raise RuntimeError("Non-finite sphere position") + print(f"Finished: positions={positions}, kinetic energy={energy.GetValue():.6g}") + print(f"Output: {args.output_dir.resolve()}") + + +if __name__ == "__main__": + main() diff --git a/python/tests/cuda12_smoke.py b/python/tests/cuda12_smoke.py new file mode 100644 index 00000000..ab53ccfd --- /dev/null +++ b/python/tests/cuda12_smoke.py @@ -0,0 +1,99 @@ +"""Validate pip CUDA headers/compiler without a GPU; optionally run the solver. + +Run against an installed wheel with --gpu for a fresh JIT simulation. The default +mode deliberately loads only the Python bootstrap, so CI needs no driver stub. +""" + +import argparse +import ctypes +from importlib.metadata import distribution +import os +from pathlib import Path +import runpy +import subprocess +import sys +import tempfile + + +def main(): + """Compile representative CUDA headers, then optionally check free fall.""" + parser = argparse.ArgumentParser() + parser.add_argument("--distribution", default="deme") + parser.add_argument("--gpu", action="store_true") + args = parser.parse_args() + package = distribution(args.distribution) + bootstrap = runpy.run_path(str(package.locate_file("deme/_cuda.py"))) + environment_before = dict(os.environ) + includes = bootstrap["configure_cuda"]() + assert dict(os.environ) == environment_before, "CUDA bootstrap must not change the environment" + assert includes, "Install the cuda12 extra before running this test" + nvrtc = bootstrap["_libraries"][-1] + program = ctypes.c_void_p() + source = b""" + #include + #include + #include + #include + extern "C" __global__ void smoke(float* out) { + curandState state; + curand_init(123, threadIdx.x, 0, &state); + out[threadIdx.x] = curand_uniform(&state); + } + """ + result = nvrtc.nvrtcCreateProgram(ctypes.byref(program), source, b"smoke.cu", 0, None, None) + assert result == 0, result + options = [b"--std=c++17", b"--gpu-architecture=compute_75"] + options.extend(("-I" + path).encode() for path in includes) + option_array = (ctypes.c_char_p * len(options))(*options) + try: + result = nvrtc.nvrtcCompileProgram(program, len(options), option_array) + size = ctypes.c_size_t() + nvrtc.nvrtcGetProgramLogSize(program, ctypes.byref(size)) + log = ctypes.create_string_buffer(size.value) + nvrtc.nvrtcGetProgramLog(program, log) + assert result == 0, log.value.decode() + finally: + nvrtc.nvrtcDestroyProgram(ctypes.byref(program)) + print("PASS: pip NVRTC compiled CUDA runtime, compiler, CURAND, and CCCL headers", flush=True) + + if args.gpu: + # A new cache ensures initialization really compiles the solver kernels. + with tempfile.TemporaryDirectory(prefix="deme-cuda12-jit-") as cache: + os.environ["DEME_JIT_CACHE_DIR"] = cache + environment_before = dict(os.environ) + import deme + import importlib + + importlib.reload(deme) + assert dict(os.environ) == environment_before, "Import must not export CUDA configuration" + # An exec'd child must see exactly the original environment, with no Python CUDA override. + child = subprocess.check_output( + [sys.executable, "-c", "import os; print(os.environ.get('DEME_CUDA_INCLUDE_PATH', ''))"], + text=True, + ).strip() + assert child == environment_before.get("DEME_CUDA_INCLUDE_PATH", "") + + mappings = Path("/proc/self/maps").read_text().splitlines() + cuda_mappings = [line for line in mappings if "libcudart.so" in line or "libnvrtc" in line] + assert cuda_mappings and all("/nvidia/" in line for line in cuda_mappings), cuda_mappings + solver = deme.DEMSolver([0]) + solver.SetVerbosity("ERROR") + solver.InstructBoxDomainDimension((-0.5, 0.5), (-0.5, 0.5), (-0.5, 0.5)) + solver.SetGravitationalAcceleration([0.0, 0.0, -9.81]) + material = solver.LoadMaterial({"E": 1e7, "nu": 0.3, "CoR": 0.5, "mu": 0.4, "Crr": 0.0}) + sphere = solver.LoadSphereType(1.0, 0.01, material) + batch = solver.AddClumps(sphere, [[0.0, 0.0, 0.0]]) + tracker = solver.Track(batch) + solver.SetInitTimeStep(1e-5) + solver.Initialize() + solver.DoDynamicsThenSync(0.01) + fingerprints = list(Path(cache).rglob("fingerprint.txt")) + assert fingerprints, "Expected fresh JIT cache entries" + assert all("-I/usr/local/cuda" not in path.read_text() for path in fingerprints) + position = tracker.Pos() + assert -0.0006 < position[2] < -0.0004, position + print("PASS: fresh solver JIT and GPU free fall", flush=True) + + +if __name__ == "__main__": + main() diff --git a/python/tests/test_pypi_readme.py b/python/tests/test_pypi_readme.py new file mode 100644 index 00000000..994c6332 --- /dev/null +++ b/python/tests/test_pypi_readme.py @@ -0,0 +1,51 @@ +"""Check PyPI README preparation without building the CUDA extension.""" + +import importlib.util +from email.parser import BytesParser +from pathlib import Path +import tempfile +import unittest + + +REPOSITORY = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location("prepare_pypi_readme", REPOSITORY / "docs/prepare_pypi_readme.py") +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class PyPIReadmeTests(unittest.TestCase): + """Cover link portability and preservation of the release description.""" + + def test_files_directories_anchors_and_remote_content(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "docs").mkdir() + (root / "docs/guide.rst").touch() + readme = root / "README.md" + unchanged = '# DEME 3\nmesh–mesh\n[Section](#section)\n[Site](https://example.com/)\n\n' + readme.write_text(unchanged + '[Guide](docs/guide.rst#usage)\n[Demos](docs)\n', encoding="utf-8") + module.prepare_readme(readme, "https://github.com/example/DEME/", "release/3") + prepared = readme.read_text(encoding="utf-8") + self.assertTrue(prepared.startswith(unchanged)) + self.assertIn('https://github.com/example/DEME/blob/release%2F3/docs/guide.rst#usage', prepared) + self.assertIn('https://github.com/example/DEME/tree/release%2F3/docs', prepared) + module.prepare_readme(readme, "https://github.com/example/DEME", "other") + self.assertEqual(readme.read_text(encoding="utf-8"), prepared) + # Wheel METADATA has UTF-8 text without a MIME charset; decode bytes explicitly. + metadata = BytesParser().parsebytes( + ('Metadata-Version: 2.1\nDescription-Content-Type: text/markdown\n\n' + prepared).encode('utf-8') + ) + self.assertEqual(metadata.get_payload(decode=True).decode('utf-8'), prepared) + + def test_missing_link_fails_without_rewriting(self): + with tempfile.TemporaryDirectory() as directory: + readme = Path(directory) / "README.md" + source = '[Missing](missing.rst)\n' + readme.write_text(source) + with self.assertRaisesRegex(ValueError, "does not exist"): + module.prepare_readme(readme, "https://github.com/example/DEME", "abc123") + self.assertEqual(readme.read_text(), source) + + +if __name__ == "__main__": + unittest.main() diff --git a/recipe/meta.yaml b/recipe/meta.yaml new file mode 100644 index 00000000..f979b52b --- /dev/null +++ b/recipe/meta.yaml @@ -0,0 +1,64 @@ +{% set version_match = load_file_regex(load_file="VERSION", regex_pattern='^(\\d+\\.\\d+\\.\\d+)\\s*$') %} +{% set deme_version = version_match[1] %} + +package: + name: deme + version: {{ deme_version }} + +source: + path: .. + +build: + number: 0 + script: {{ PYTHON }} -m pip install . -vvv + skip: true # [not linux] + +# Note: Build with: conda build recipe/ -c conda-forge +# The conda-forge channel is required for build dependencies + +requirements: + build: + - {{ compiler('c') }} + - {{ compiler('cxx') }} + - cmake >=3.18 + - ninja + - cudatoolkit >=11.0 + - make + + host: + - python >=3.8 + - pip + - scikit-build-core >=0.8.0 + - pybind11 >=2.11.0 + - numpy >=1.19.0 + - cudatoolkit >=11.0 + + run: + - python >=3.8 + - numpy >=1.19.0 + - cudatoolkit >=11.0 + - libstdcxx-ng >=12 + +test: + imports: + - deme + - DEME + commands: + - python -c "import deme, DEME; assert deme.__version__ == DEME.__version__ == '{{ deme_version }}'" + +about: + home: https://github.com/projectchrono/DEM-Engine + license: BSD-3-Clause + license_file: LICENSE.md + summary: Python bindings for DEM-Engine - a GPU-accelerated Discrete Element Method simulation engine + description: | + DEM-Engine is a GPU-accelerated discrete element method (DEM) simulation engine + that provides high-performance particle and granular dynamics simulations. + This package provides Python bindings to the DEM-Engine library, allowing + users to set up and run simulations from Python scripts. + doc_url: https://github.com/projectchrono/DEM-Engine + dev_url: https://github.com/projectchrono/DEM-Engine + +extra: + recipe-maintainers: + - Ruochun diff --git a/src/.format_all b/src/.format_all deleted file mode 100755 index d330f0c1..00000000 --- a/src/.format_all +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Recursively find source files and apply clang-format with -i -style=file -find . \( \ - -name "*.cpp" -o \ - -name "*.c" -o \ - -name "*.h" -o \ - -name "*.hpp" -o \ - -name "*.cu" -o \ - -name "*.cuh" \ -\) -exec clang-format -i -style=file {} + - -echo "✔ All matching source files formatted with clang-format -style=file" - diff --git a/src/DEM/API.h b/src/DEM/API.h index 8bacc623..2bd8e2e6 100644 --- a/src/DEM/API.h +++ b/src/DEM/API.h @@ -11,10 +11,12 @@ #include #include #include +#include #include "kT.h" #include "dT.h" #include "../core/utils/CudaAllocator.hpp" +#include "../core/utils/CudaDebugSync.hpp" #include "../core/utils/ThreadManager.h" #include "../core/utils/GpuManager.h" #include "../core/utils/DEMEPaths.h" @@ -23,6 +25,7 @@ #include "BdrsAndObjs.h" #include "Models.h" #include "AuxClasses.h" +#include "utils/VisualizationData.h" /// Main namespace for the DEM-Engine package. namespace deme { @@ -47,23 +50,28 @@ class DEMTracker; ////////////////////////////////////////////////////////////// /// Main DEM-Engine solver. +/// +/// Fixed-size owner `ToDevice` and `FromDevice` methods validate ranges, capacities, and CUDA pointer metadata by +/// default. Their trailing `validate` argument may be disabled when the caller guarantees those preconditions and +/// needs to avoid validation overhead. Required device routing and documented transformations still apply. class DEMSolver { public: - /// Default constructor: scans available GPUs and uses at most 2. + /// Construct a solver using the requested number of visible CUDA devices. One GPU places both workers on device 0; + /// two GPUs place dT on device 0 and kT on device 1 when available, falling back to device 0 on a one-GPU system. DEMSolver(unsigned int nGPUs = 2); - /// Construct using explicit GPU device IDs. Errors if any ID is unavailable. Warns and uses the first 2 if more - /// than 2 are given. - DEMSolver(std::vector device_ids); + /// Construct a solver on explicitly selected logical CUDA devices. One ID places both workers on that device; two + /// IDs assign dT and kT respectively. IDs follow CUDA_VISIBLE_DEVICES remapping. + explicit DEMSolver(const std::vector& device_ids); ~DEMSolver(); - /// Set output detail level. - void SetVerbosity(VERBOSITY verbose) { verbosity = verbose; } + /// Return the logical CUDA devices assigned to dT and kT, in that order. + std::vector GetGPUDeviceIDs() const { return m_gpu_device_ids; } /// Instruct the dimension of the `world'. On initialization, this info will be used to figure out how to assign the /// num of voxels in each direction. If your `useful' domain is not box-shaped, then define a box that contains your /// domian. void InstructBoxDomainDimension(float x, float y, float z, const std::string& dir_exact = "none"); - /// @brief Set the size of the simulation `world'. + /// @brief Set the size of the simulation "world". /// @param x Lower and upper limit for X coordinate. /// @param y Lower and upper limit for Y coordinate. /// @param z Lower and upper limit for Z coordinate. @@ -74,23 +82,25 @@ class DEMSolver { const std::pair& z, const std::string& dir_exact = "none"); - /// Instruct if and how we should add boundaries to the simulation world upon initialization. Choose between `none', - /// `all' (add 6 boundary planes) and `top_open' (add 5 boundary planes and leave the z-directon top open). Also + /// Instruct if and how we should add boundaries to the simulation world upon initialization. Choose between "none", + /// "all" (add 6 boundary planes) and "top_open" (add 5 boundary planes and leave the z-directon top open). Also /// specifies the material that should be assigned to those bounding boundaries. void InstructBoxDomainBoundingBC(const std::string& inst, const std::shared_ptr& mat) { m_user_add_bounding_box = inst; m_bounding_box_material = mat; } - /// Set gravitational pull. - void SetGravitationalAcceleration(float3 g) { G = g; } + /// Set gravitational pull. This takes effect immediately when called from a synchronized stance after Initialize(). + void SetGravitationalAcceleration(float3 g); void SetGravitationalAcceleration(const std::vector& g) { assertThreeElements(g, "SetGravitationalAcceleration", "g"); - G = make_float3(g[0], g[1], g[2]); + SetGravitationalAcceleration(make_float3(g[0], g[1], g[2])); } - /// Set the initial time step size. If using constant step size, then this will be used throughout; otherwise, the - /// actual step size depends on the variable step strategy. - void SetInitTimeStep(double ts_size) { m_ts_size = ts_size; } + /// Set the time step size before or after initialization. Post-initialization calls must be made from a + /// synchronized stance and take effect on the next step. + void SetTimeStepSize(double ts_size); + /// Legacy pre-initialization name for SetTimeStepSize. Kept for backward compatibility. + void SetInitTimeStep(double ts_size) { SetTimeStepSize(ts_size); } /// Return the number of clumps that are currently in the simulation. Must be used after initialization. size_t GetNumClumps() const { return nOwnerClumps; } /// Return the total number of owners (clumps + meshes + analytical objects) that are currently in the simulation. @@ -104,6 +114,8 @@ class DEMSolver { /// Get the current expand factor in simulation. float GetExpandFactor() const; /// Set the number of dT steps before it waits for a contact-pair info update from kT. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers and interaction + /// manager. void SetCDUpdateFreq(int freq) { m_updateFreq = freq; m_suggestedFutureDrift = 2 * freq; @@ -115,14 +127,29 @@ class DEMSolver { double GetSimTime() const; /// Set the simulation time manually. void SetSimTime(double time); - /// @brief Set the strategy for auto-adapting time step size (NOT implemented, no effect yet). - /// @param type "none" or "max_vel" or "int_diff". + + /// Capture renderable sphere and triangle geometry at the current solver state. + /// @details This call synchronously transfers the required owner state to the host. It does not advance the + /// simulation and should not be called concurrently with DoDynamics(). Disabled geometry categories are not + /// transferred into the returned snapshot. + DEMVisualizationSnapshot GetVisualizationSnapshot(bool include_spheres = true, bool include_triangles = true) const; + /// Get cached-renderer geometry in owner-local coordinates. Refresh when the frame revision changes. + DEMVisualizationScene GetVisualizationScene() const; + /// Fill reusable owner arrays at a synchronized simulation boundary; never call concurrently with dynamics. + void GetVisualizationFrame(DEMVisualizationFrame& frame, bool include_velocities = false) const; + /// @brief Set the strategy for auto-adapting time step size. + /// @param type "none", "hertz_const", "max_vel" or "int_diff". Currently, only "hertz_const" has behavior; it + /// computes a fixed setup-time timestep from Hertzian material stiffness, minimum clump mass, and minimum radius. void SetAdaptiveTimeStepType(const std::string& type); + /// @brief Use the setup-time Hertzian constant timestep estimate. + void UseHertzConstTimeStep() { SetAdaptiveTimeStepType("hertz_const"); } /// @brief Set the time integrator for this simulator. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. /// @param intg "forward_euler" or "extended_taylor" or "centered_difference". void SetIntegrator(const std::string& intg); /// @brief Set the time integrator for this simulator. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetIntegrator(TIME_INTEGRATOR intg) { m_integrator = intg; } /// Return whether this simulation system is initialized @@ -136,6 +163,12 @@ class DEMSolver { /// Set the jitification options. It is only needed by advanced users. void SetJitifyOptions(const std::vector& options) { m_jitify_options = options; } + /// Process-wide switch to synchronize after CUDA operations that enqueue stream work. + /// Enabled by default for debugging; disable it for normal asynchronous performance. + void SetCudaDebugSync(bool enable = true) { SetCudaDebugSyncEnabled(enable); } + /// Return whether process-wide CUDA debug synchronization is enabled. + bool GetCudaDebugSync() const { return IsCudaDebugSyncEnabled(); } + /// Explicitly instruct the bin size (for contact detection) that the solver should use. void SetInitBinSize(double bin_size) { use_user_defined_bin_size = INIT_BIN_SIZE_TYPE::EXPLICIT; @@ -172,8 +205,10 @@ class DEMSolver { /// Get the current force model. std::shared_ptr GetContactForceModel() { return m_force_model; } - /// Instruct the solver if contact pair arrays should be sorted (based on the types of contacts) before usage. - void SetSortContactPairs(bool use_sort) { should_sort_contacts = use_sort; } + /// Do not use this method. It has no effect. + void SetSortContactPairs(bool use_sort) { + DEME_WARNING(std::string("SetSortContactPairs now has no effect. Do not use it in code.")); + } /// Instruct the solver to rearrange and consolidate clump templates information, then jitify it into GPU kernels /// (if set to true), rather than using flattened sphere component configuration arrays whose entries are associated @@ -190,77 +225,126 @@ class DEMSolver { /// them it into GPU kernels. void DisableJitifyMassProperties() { jitify_mass_moi = false; } - // NOTE: compact force calculation (in the hope to use shared memory) is not implemented - void UseCompactForceKernel(bool use_compact); - /// (Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are expanded /// for the purpose of contact detection (safe, and creates false positives). If fix is set to true, then this /// expand factor does not change even if the user uses variable time step size. + /// After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetExpandFactor(float beta, bool fix = true) { m_expand_factor = beta; use_user_defined_expand_factor = fix; } - /// Input the maximum expected particle velocity. If `force' is set to false, the solver will not use a velocity - /// larger than max_vel for determining the margin thickness; if `force' is set to true, the solver will not + /// Input the maximum expected particle velocity. If "force" is set to false, the solver will not use a velocity + /// larger than max_vel for determining the margin thickness; if "force" is set to true, the solver will not /// calculate maximum system velocity and will always use max_vel to calculate the margin thickness. - /// @brief Set the maximum expected particle velocity. The solver will not use a velocity larger than this for - /// determining the margin thickness, and velocity larger than this will be considered a system anomaly. + /// @brief Set the maximum expected simulation velocity. Note this call is not necessarily required; the solver can + /// automatically derive the maximum velocity. The velocity here means the magnitude of velocity of primtive + /// geometries (spheres and triangles). + /// @details Additionally, the solver will not use a velocity larger than this for determining the margin thickness, + /// and velocity larger than this will be considered a system anomaly. /// @param max_vel Expected max velocity. + /// After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetMaxVelocity(float max_vel); /// @brief Set the method this solver uses to derive current system velocity (for safety purposes in contact /// detection). /// @param insp_type A string. If "auto": the solver automatically derives. + /// After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetExpandSafetyType(const std::string& insp_type); // void SetExpandSafetyType(const std::shared_ptr& insp) { // m_max_v_finder_type = MARGIN_FINDER_TYPE::DEM_INSPECTOR; - // m_approx_max_vel_func = insp; + // m_approx_vel_func = insp; // } /// Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the contact - /// `safety' margin. This can be greater than one if the simulation velocity can increase significantly in one kT + /// "safety" margin. This can be greater than one if the simulation velocity can increase significantly in one kT /// update cycle, but this is not common and should be close to 1 in general. + /// After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetExpandSafetyMultiplier(float param) { m_expand_safety_multi = param; } - /// Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving the - /// thinckness of the contact `safety' margin. This need not to be large unless the simulation velocity can increase + /// Set a "base" velocity, which we will always add to our estimated maximum system velocity, when deriving the + /// thinckness of the contact "safety" margin. This need not to be large unless the simulation velocity can increase /// significantly in one kT update cycle. + /// After initialization, call UpdateSimParams() for this change to take effect in the workers. void SetExpandSafetyAdder(float vel) { m_expand_base_vel = vel; } + /// @brief Control whether angular velocity contributes to the contact detection margin. + /// @details Default is auto: false for pure single-sphere clumps; true when multi-sphere clumps or meshes exist. + void SetUseAngularVelocityMargin(bool use); + + /// @brief Manually seed the current triangle--triangle penetration margin in dT. + /// @details This fills every entry of the per-triangle maxTriTriPenetration array with the same value, which will + /// be used in the NEXT contact detection run in kT. One use case is if at the start of simulation, there is already + /// significant mesh--mesh penetration, then without calling this method, the solver might not be able to resolve + /// triangles that are deeply inside another mesh. Use this method to boost the solver's mesh--mesh contact + /// detection proactiveness for one step, then the solver will automatically know how much of an extra margin it + /// needs to add from this step onwards. + /// @param penetration The penetration value to assign to each triangle (must be non-negative). + void SetTriTriPenetration(double penetration); + + /// @brief Set the maximum allowed triangle--triangle penetration used as the margin added in kT contact detection. + /// @details This value caps the penetration margin added to the triangle--triangle contact detection to prevent + /// excessively large values. The default is a huge value, so if not called, it doesn't affect the workflow. Note it + /// only affects kT's contact detection algorithm's proactiveness in detecting future contacts, not capping the + /// actual physical contact used in force calculation. + /// @param max_margin Maximum allowed penetration margin (must be non-negative). + void SetMaxTriTriPenetration(double max_margin); + /// @brief Set the ratio threshold used to reject suspicious triangle-triangle contacts. + /// @details A negative value disables this guard when the corresponding contact kernel support is enabled. + void SetTriTriContactRejectionRatio(float ratio); /// @brief Used to force the solver to error out when there are too many spheres in a bin. A huge number can be used /// to discourage this error type. /// @param max_sph Max number of spheres in a bin. - void SetMaxSphereInBin(unsigned int max_sph) { threshold_too_many_spheres_in_bin = max_sph; } + void SetMaxSphereInBin(unsigned int max_sph); /// @brief Used to force the solver to error out when there are too many spheres in a bin. A huge number can be used /// to discourage this error type. /// @param max_tri Max number of triangles in a bin. - void SetMaxTriangleInBin(unsigned int max_tri) { threshold_too_many_tri_in_bin = max_tri; } + void SetMaxTriangleInBin(unsigned int max_tri); /// @brief Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage this - /// error type. Defaulted to 5e4. + /// error type. Defaulted to 1e3. /// @param vel Error-out velocity. - void SetErrorOutVelocity(float vel) { threshold_error_out_vel = vel; } - - /// @brief Set the average number of contacts a sphere has, before the solver errors out. A huge number can be used - /// to discourage this error type. Defaulted to 100. + void SetErrorOutVelocity(float vel); + /// @brief Set the angular velocity which when exceeded, the solver errors out. A huge number can be used to + /// discourage this error type. Defaulted to 1e4. + /// @param ang_vel Error-out angular velocity. + void SetErrorOutAngularVelocity(float ang_vel); + + /// @brief Set the average number of contacts a primitive geometry (sphere or triangle) has, before the solver + /// errors out. A huge number can be used to discourage this error type. Defaulted to 100. /// @param num_cnts Error-out contact number. - void SetErrorOutAvgContacts(float num_cnts) { threshold_error_out_num_cnts = num_cnts; } + void SetErrorOutAvgContacts(float num_cnts); - /// @brief Get the current number of contacts each sphere has. + /// @brief Get the current number of contacts each primitive geometry (sphere or triangle) has. /// @return Number of contacts. - float GetAvgSphContacts() const { return kT->stateParams.avgCntsPerSphere; } + float GetAvgPrimitiveContacts() const { return kT->stateParams.avgCntsPerPrimitive; } + + float GetAvgSphContacts() const { + if (!dT->solverFlags.meshUniversalContact) { + return GetAvgPrimitiveContacts(); + } else { + DEME_ERROR( + std::string("GetAvgSphContacts is not available when meshUniversalContact is enabled. You may use " + "GetAvgPrimitiveContacts instead.")); + return -1.f; + } + } /// @brief Enable or disable the use of adaptive bin size (by default it is on). + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. /// @param use Enable or disable. void UseAdaptiveBinSize(bool use = true) { auto_adjust_bin_size = use; } - /// @brief Disable the use of adaptive bin size (always use initial size). + /// @brief Disable the use of adaptive bin size. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. void DisableAdaptiveBinSize() { auto_adjust_bin_size = false; } - /// @brief Enable or disable the use of adaptive max update step count (by default it is on). + /// @brief Enable or disable adaptive max update step count. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. /// @param use Enable or disable. void UseAdaptiveUpdateFreq(bool use = true) { auto_adjust_update_freq = use; } - /// @brief Disable the use of adaptive max update step count (always use initial update frequency). + /// @brief Disable adaptive max update step count. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. void DisableAdaptiveUpdateFreq() { auto_adjust_update_freq = false; } /// @brief Adjust how frequent kT updates the bin size. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the worker. /// @param n Number of contact detections before kT makes one adjustment to bin size. void SetAdaptiveBinSizeDelaySteps(unsigned int n) { if (n < NUM_STEPS_RESERVED_AFTER_CHANGING_BIN_SIZE) @@ -271,52 +355,85 @@ class DEMSolver { auto_adjust_observe_steps = (n >= 1) ? n : 1; } /// @brief Set the max rate that the bin size can change in one adjustment. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the worker. /// @param rate 0: never changes; 1: can double or halve size in one go; suggest using default. void SetAdaptiveBinSizeMaxRate(float rate) { auto_adjust_max_rate = (rate > 0) ? rate : 0; } - /// @brief Set how fast kT changes the direction of bin size adjustmemt when there's a more beneficial direction. + /// @brief Set how fast kT changes adjustment direction. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the worker. /// @param acc 0.01: slowly change direction; 1: quickly change direction void SetAdaptiveBinSizeAcc(float acc) { auto_adjust_acc = clampBetween(acc, 0.01, 1.0); } - /// @brief Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a - /// bin). + /// @brief Set how proactive the solver is in avoiding oversized bins. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the worker. /// @param ratio 0: not proavtive; 1: very proactive. void SetAdaptiveBinSizeUpperProactivity(float ratio) { auto_adjust_upper_proactive_ratio = clampBetween(ratio, 0.0, 1.0); } - /// @brief Set how proactive the solver is in avoiding the bin being too small (leading to too many bins in domain). + /// @brief Set how proactive the solver is in avoiding undersized bins. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the worker. /// @param ratio 0: not proavtive; 1: very proactive. void SetAdaptiveBinSizeLowerProactivity(float ratio) { auto_adjust_lower_proactive_ratio = clampBetween(ratio, 0.0, 1.0); } /// @brief Get the current bin (for contact detection) size. Must be called from synchronized stance. /// @return Bin size. - double GetBinSize() { return kT->simParams->binSize; } + double GetBinSize() { return kT->simParams->dyn.binSize; } // NOTE: No need to get binSize from the device, as binSize is only changed on the host /// @brief Get the current number of bins (for contact detection). Must be called from synchronized stance. /// @return Number of bins. size_t GetBinNum() { return kT->stateParams.numBins; } - /// @brief Set the upper bound of kT update frequency (when it is adjusted automatically). + /// @brief Set the upper bound of kT update frequency. + /// @note After initialization, call UpdateSimParams() for this change to take effect in the workers. /// @details This only affects when the update freq is updated automatically. To manually control the freq, use /// SetCDUpdateFreq then call DisableAdaptiveUpdateFreq. /// @param max_freq dT will not receive updates less frequently than 1 update per max_freq steps. void SetCDMaxUpdateFreq(unsigned int max_freq) { upper_bound_future_drift = 2 * max_freq; } - /// @brief Set the number of steps dT configures its max drift more than average drift steps. + /// @brief Set the number of steps dT configures its max drift above average. + /// @note After initialization, call UpdateSimParams() for this change to take effect in dT. /// @param n Number of steps. Suggest using default. void SetCDNumStepsMaxDriftAheadOfAvg(float n) { max_drift_ahead_of_avg_drift = n; } - /// @brief Set the multiplier which dT configures its max drift to be w.r.t. the average drift steps. + /// @brief Set the maximum-drift multiplier. + /// @note After initialization, call UpdateSimParams() for this change to take effect in dT. /// @param m The multiplier. Suggest using default. void SetCDNumStepsMaxDriftMultipleOfAvg(float m) { max_drift_multiple_of_avg_drift = m; } - /// @brief Set the number of past kT updates that dT will use to calibrate the max future drift limit. + /// @brief Set the drift-regulator history size. + /// @note After initialization, call UpdateSimParams() for this change to take effect in dT. /// @param n Number of kT updates. Suggest using default. void SetCDNumStepsMaxDriftHistorySize(unsigned int n); /// @brief Get the current update frequency used by the solver. /// @return The current update frequency. float GetUpdateFreq() const; - /// Set the number of threads per block in force calculation (default 256). + /// Set the number of threads per block in force calculation (256, 128, 64 etc.). void SetForceCalcThreadsPerBlock(unsigned int nTh) { dT->DT_FORCE_CALC_NTHREADS_PER_BLOCK = nTh; } + /// @brief Set whether the meshes should have contacts with other meshes and analytical objects. + /// @param use If true, meshes can have contacts with clumps, other meshes and analytical objects. If false, meshes + /// can have contacts only with clumps. + /// @details Set to false to speedup the simulation if its involved meshes are not expected to have contacts. The + /// default is false. + void SetMeshUniversalContact(bool use = true); + + /// @brief Set whether the solver should expect the user to mark certain contacts as persistent across kT updates. + /// @param use If true, the solver will expect the user to mark certain contacts as persistent across kT updates. + /// If false, all contacts are treated as non-persistent. + /// @details Set this to true if you later will call MarkPersistentContact series of methods. + void SetPersistentContact(bool use = true); + + /// @brief Set whether to use simple patch ID-based triangle combination. + /// @param use If true, all triangles sharing the same patch ID pair are combined into one patch contact. + void SetSimplePatchCombination(bool use = true); + + /// @brief Set whether flooded patch-island IDs should be stabilized across contact-detection steps. + void SetStablePatchIslandIDs(bool use = true); + + /// @brief Declare that all meshed particles in the simulation have a low polygon count. + /// @param use If true, skip per-triangle mesh-mesh penetration-margin bookkeeping. + /// @details Toggle this on only when mesh-mesh contacts are always SAT-traceable, i.e. no triangle from one mesh is + /// expected to be completely submerged inside another mesh. + void SetMeshParticlesLowPoly(bool use = true); + /// @brief Load a clump type into the API-level cache. /// @return the shared ptr to the clump type just loaded. std::shared_ptr LoadClumpType(float mass, @@ -386,6 +503,11 @@ class DEMSolver { std::shared_ptr LoadSphereType(float mass, float radius, const std::shared_ptr& material); + /// A simplified version of LoadClumpType: it just loads a one-sphere clump template, with explicit MOI supplied + std::shared_ptr LoadSphereType(float mass, + float moi, + float radius, + const std::shared_ptr& material); /// @brief Load materials properties (Young's modulus, Poisson's ratio...) into the system. /// @param mat_prop Property name--value pairs, as an unordered_map. @@ -408,6 +530,10 @@ class DEMSolver { /// @param ptr Shared pointer for the object to duplicate. /// @return A duplicate of the object (with effectively a deep copy). std::shared_ptr Duplicate(const std::shared_ptr& ptr); + /// @brief Duplicate a mesh object that is loaded into the system. + /// @param ptr Shared pointer for the mesh object to duplicate. + /// @return A duplicate of the mesh object (with effectively a deep copy). + std::shared_ptr Duplicate(const std::shared_ptr& ptr); /// @brief Set the value for a material property that by nature involves a pair of a materials (e.g. friction /// coefficient). @@ -445,6 +571,155 @@ class DEMSolver { /// @param n The number of consecutive owners. /// @return The family number. std::vector GetOwnerFamily(bodyID_t ownerID, bodyID_t n = 1) const; + /// Fill caller-provided CUDA memory with positions of consecutive owners. Capacity is measured in elements. + /// @param validate If true, validate the owner range, capacity, and CUDA pointer metadata. Disable only when the + /// caller guarantees those preconditions and needs to avoid validation overhead. + void GetOwnerPositionToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with velocities of consecutive owners. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerVelocityToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with local-frame angular velocities of consecutive owners. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerAngVelLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with global-frame angular velocities of consecutive owners. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerAngVelGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with public-order (x, y, z, w) orientation quaternions. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerOriQToDevice(float4* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with global-frame contact accelerations. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerAccToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with local-frame contact angular accelerations. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerAngAccLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with global-frame contact angular accelerations. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerAngAccGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// @brief Get one reduced contact wrench for each consecutive owner. + /// @details Each force is the global-frame sum of recorded contact forces on that owner. Each torque is the + /// corresponding global-frame moment about DEME's current owner position, including force-generated moments and + /// force-model-only torque such as rolling resistance. Owners without recorded contact receive a zero wrench. + /// This reads the current dT force records; it does not trigger contact detection or force evaluation. Contact + /// recording must remain enabled (the default). + /// @param forces Output global resultant forces, resized to `count`. + /// @param torques Output global resultant torques, resized to `count`. + /// @param ownerID First owner in the consecutive range. + /// @param count Number of owners to reduce. A zero count clears both output vectors. + void GetOwnerContactWrench(std::vector& forces, + std::vector& torques, + bodyID_t ownerID, + bodyID_t count = 1) const; + /// @brief Write one reduced contact wrench per consecutive owner directly to CUDA memory. + /// @details This is the device-output counterpart of GetOwnerContactWrench and uses the same reduction. Each force + /// is the global-frame sum of recorded contact forces on an owner. Each torque is the corresponding global-frame + /// moment about DEME's current owner position, including force-generated moments and force-model-only torque such + /// as rolling resistance. Owners without recorded contact receive a zero wrench. This reads the current dT force + /// records; it does not trigger contact detection or force evaluation. Contact recording must remain enabled (the + /// default). The call is synchronous, so both destination buffers may be consumed when it returns. + /// @param force_destination Writable CUDA memory for `count` float3 resultant forces. + /// @param torque_destination Writable CUDA memory for `count` float3 resultant torques. + /// @param capacity Available elements in each destination buffer; must be at least `count`. + /// @param destination_device Logical CUDA device owning both destination buffers. CUDA selects the available + /// inter-device transfer route for cross-device output. + /// @param ownerID First owner in the consecutive range. + /// @param count Number of owners to reduce. A zero count is a no-op. + void GetOwnerContactWrenchToDevice(float3* force_destination, + float3* torque_destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t count = 1) const; + /// Fill caller-provided CUDA memory with owner family numbers as unsigned integers. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerFamilyToDevice(unsigned int* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// @brief Handover helper: get all clump-owner center positions in one call. + /// @return Position vector with one entry per clump owner, ordered by owner ID. + std::vector GetClumpPositionsHandover() const; + /// @brief Request an immediate contact detection update. + void RequestContactUpdate(); + /// @brief Enable per-triangle P/V/PxV tracking for the specified mesh owners. + /// @details Owner IDs are simulation owner IDs; each selected owner must be a mesh owner. + /// Tracking buffers are reset when this method is called. + void SetTrianglePVTrackingOwners(const std::vector& mesh_owner_ids); + /// @brief Disable per-triangle P/V/PxV tracking and clear tracking state. + void DisableTrianglePVTracking(); + /// @brief Get frame-window averaged per-triangle P, V and P*V for one tracked owner. + /// @param ownerID Mesh owner ID used in SetTrianglePVTrackingOwners. + /// @param avgP Output vector of averaged normal-force shares per triangle. + /// @param avgV Output vector of averaged tangential slip speeds per triangle. + /// @param avgPV Output vector of averaged P*V values per triangle. + /// @param reset_window If true, clear the current accumulation window after reading. + /// @return True if the owner is currently tracked; false otherwise. + bool GetTrackedOwnerTrianglePV(bodyID_t ownerID, + std::vector& avgP, + std::vector& avgV, + std::vector& avgPV, + bool reset_window = true); + /// @brief Enable a mesh wear model driven by per-triangle P*V. + /// @param ownerID Mesh owner ID. + /// @param wear_rate Wear-rate coefficient [length / (P*V * time)]. + /// @param update_interval Geometry update interval [s], must be >= solver time step. + /// @param start_time Wear start time [s]. + /// @param end_time Wear end time [s], negative value means no end time. + /// @param normal_sign Wear direction sign along triangle normal (+1) or opposite to it (-1). + void EnableMeshWearModel(bodyID_t ownerID, + double wear_rate, + double update_interval, + double start_time = 0.0, + double end_time = -1.0, + float normal_sign = -1.0f); + /// @brief Disable mesh wear model for one owner. + void DisableMeshWearModel(bodyID_t ownerID); + /// @brief Disable all active mesh wear models. + void DisableAllMeshWearModels(); + /// @brief Force-apply any pending wear deformation immediately. + void FlushMeshWearModels(); /// @brief Get the mass of n consecutive owners. /// @param ownerID First owner's ID. /// @param n The number of consecutive owners. @@ -455,6 +730,22 @@ class DEMSolver { /// @param n The number of consecutive owners. /// @return The moment of inertia (in principal axis frame). std::vector GetOwnerMOI(bodyID_t ownerID, bodyID_t n = 1) const; + /// Fill caller-provided CUDA memory with owner masses. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerMassToDevice(float* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; + /// Fill caller-provided CUDA memory with owner principal moments of inertia. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerMOIToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n = 1, + bool validate = true) const; /// @brief Set position of consecutive owners starting from ownerID, based on input position vector. N (the size of /// the input vector) elements will be modified. @@ -465,9 +756,52 @@ class DEMSolver { /// Set velocity of consecutive owners starting from ownerID, based on input velocity vector. N (the size of the /// input vector) elements will be modified. void SetOwnerVelocity(bodyID_t ownerID, const std::vector& vel); - /// Set quaternion of consecutive owners starting from ownerID, based on input quaternion vector. N (the size of the - /// input vector) elements will be modified. + /// Set quaternion of consecutive owners starting from ownerID. Finite, nonzero inputs are normalized before being + /// stored; invalid quaternions are rejected. N (the size of the input vector) elements will be modified. void SetOwnerOriQ(bodyID_t ownerID, const std::vector& oriQ); + /// Synchronously set global positions for consecutive owners directly from CUDA memory. The source may belong to + /// any logical CUDA device accessible to this process; remote input is copied to dT before unpacking. A zero count + /// is a no-op. + /// @param source_device Logical CUDA device owning `source`. + /// @param validate If true, validate the owner range and CUDA pointer metadata. Disable only when the caller + /// guarantees those preconditions and needs to avoid validation overhead. + void SetOwnerPositionFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); + /// Synchronously set public-order (x, y, z, w) orientations directly from CUDA memory. Inputs are normalized before + /// being stored; with validation enabled, non-finite and zero-length quaternions are rejected. A zero count is a + /// no-op. + /// @param validate See SetOwnerPositionFromDevice. When false, the extra validity-check kernel is skipped, but + /// quaternion normalization remains part of the setter's semantics. + void SetOwnerOriQFromDevice(bodyID_t ownerID, + const float4* source, + int source_device, + size_t count = 1, + bool validate = true); + /// Synchronously set global linear velocities directly from CUDA memory. A zero count is a no-op. + /// @param validate See SetOwnerPositionFromDevice. + void SetOwnerVelocityFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); + /// Synchronously set local-frame angular velocities directly from CUDA memory. A zero count is a no-op. + /// @param validate See SetOwnerPositionFromDevice. + void SetOwnerAngVelFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); + /// Synchronously set global angular velocities directly from CUDA memory. Conversion to DEME's local frame uses + /// each owner's orientation at the time of this call. A zero count is a no-op. + /// @param validate See SetOwnerPositionFromDevice. + void SetOwnerAngVelGlobalFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); /// @brief Set the family number of consecutive owners. /// @param ownerID The ID of the owner. /// @param fam Family number. @@ -481,17 +815,50 @@ class DEMSolver { void AddOwnerNextStepAcc(bodyID_t ownerID, const std::vector& acc); /// @brief Add an extra angular accelerations to consecutive owners for the next time step. /// @param ownerID The number of the starting owner. - /// @param acc The extra angular acceleration to add. N (the size of this vector) elements will be modified based on - /// its values. + /// @param angAcc The extra angular acceleration to add. N (the size of this vector) elements will be modified based + /// on its values. void AddOwnerNextStepAngAcc(bodyID_t ownerID, const std::vector& angAcc); + /// @brief Queue global-frame linear accelerations for consecutive owners directly from CUDA memory. + /// @details This is the device-input counterpart of AddOwnerNextStepAcc. It replaces any previously queued + /// next-step linear-acceleration contribution for the selected owners. During the next force/integration step, + /// contact acceleration is accumulated on top and gravity is applied separately. The queued contribution is + /// consumed after one step. The call is synchronous, so `source` may be reused when it returns. + /// @param ownerID First owner in the consecutive range. + /// @param source CUDA memory containing `count` float3 global-frame accelerations. + /// @param source_device Logical CUDA device owning `source`; remote input is copied to dT before unpacking. + /// @param count Number of consecutive owners. A zero count is a no-op. + /// @param validate If true, validate the owner range and CUDA pointer metadata. Disable only when the caller + /// guarantees those preconditions and needs to avoid validation overhead. + void AddOwnerNextStepAccFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); + /// @brief Queue local-frame angular accelerations for consecutive owners directly from CUDA memory. + /// @details This is the device-input counterpart of AddOwnerNextStepAngAcc and uses each owner's local principal- + /// axis frame. It replaces any previously queued next-step angular-acceleration contribution for the selected + /// owners. Contact angular acceleration is accumulated on top during the next force/integration step, and the + /// queued contribution is consumed after that step. The call is synchronous, so `source` may be reused when it + /// returns. + /// @param ownerID First owner in the consecutive range. + /// @param source CUDA memory containing `count` float3 local-frame angular accelerations. + /// @param source_device Logical CUDA device owning `source`; remote input is copied to dT before unpacking. + /// @param count Number of consecutive owners. A zero count is a no-op. + /// @param validate See AddOwnerNextStepAccFromDevice. + void AddOwnerNextStepAngAccFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count = 1, + bool validate = true); + /// @brief Rewrite the relative positions of the flattened triangle soup. void SetTriNodeRelPos(size_t owner, size_t triID, const std::vector& new_nodes); /// @brief Update the relative positions of the flattened triangle soup. void UpdateTriNodeRelPos(size_t owner, size_t triID, const std::vector& updates); /// @brief Get a handle for the mesh this tracker is tracking. /// @return Pointer to the mesh. - std::shared_ptr& GetCachedMesh(bodyID_t ownerID); + std::shared_ptr& GetCachedMesh(bodyID_t ownerID); /// @brief Get the current locations of all the nodes in the mesh being tracked. /// @param ownerID The ownerID of the mesh. /// @return A vector of float3 representing the global coordinates of the mesh nodes. @@ -617,7 +984,7 @@ class DEMSolver { } /// @brief Load clumps (of the same template) into the simulation. - /// @param input_types The type (shared pointer pointing to the clump type handle). + /// @param input_type The type (shared pointer pointing to the clump type handle). /// @param input_xyz Vector of the initial locations of the clumps. /// @return Handle to the loaded batch of clumps. std::shared_ptr AddClumps(std::shared_ptr& input_type, @@ -634,15 +1001,161 @@ class DEMSolver { return AddClumps(input_type, loc_xyz); } - /// Load a mesh-represented object - std::shared_ptr AddWavefrontMeshObject(const std::string& filename, - const std::shared_ptr& mat, - bool load_normals = true, - bool load_uv = false); - std::shared_ptr AddWavefrontMeshObject(const std::string& filename, - bool load_normals = true, - bool load_uv = false); - std::shared_ptr AddWavefrontMeshObject(DEMMeshConnected& mesh); + /// @brief Load a mesh-represented object into the simulation, using the internal mesh format. + std::shared_ptr AddMesh(DEMMesh& mesh); + + /// @brief Control whether subsequently loaded meshes use DEME 2 patch behavior. + /// @details When enabled, every triangle of each subsequently loaded mesh is assigned to an individual patch. When + /// disabled, the solver leaves patch assignment unchanged. This setting defaults to false and does not modify + /// meshes that were loaded previously. + /// @param use Whether to apply DEME 2 patch behavior to subsequently loaded meshes. + void SetDEME2MeshBehavior(bool use = true) { use_deme2_mesh_behavior = use; } + /// @brief Load a shell mesh (triangular surface plus finite thickness) into the simulation. + /// @param mesh Mesh object. + /// @param shell_thickness Full shell thickness, in simulation length units. Must be finite and non-negative. + std::shared_ptr AddShellMesh(DEMMesh& mesh, float shell_thickness); + + /// @brief Load a mesh-represented object from a mesh file (.obj, .stl, or .ply). + /// @param filename Path to the mesh file. + /// @param mat Material to assign to the mesh. + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded mesh object. + std::shared_ptr AddWavefrontMeshObject(const std::string& filename, + const std::shared_ptr& mat, + bool load_normals = true, + bool load_uv = false); + /// @brief Load a shell mesh from a mesh file (.obj, .stl, or .ply). + /// @param filename Path to the mesh file. + /// @param mat Material to assign to the shell mesh. + /// @param shell_thickness Full shell thickness, in simulation length units. Must be finite and non-negative. + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded shell mesh object. + std::shared_ptr AddWavefrontShellObject(const std::string& filename, + const std::shared_ptr& mat, + float shell_thickness, + bool load_normals = true, + bool load_uv = false); + /// @brief Load a mesh-represented object from a mesh file (.obj, .stl, or .ply). + /// @param filename Path to the mesh file. + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded mesh object. + std::shared_ptr AddWavefrontMeshObject(const std::string& filename, + bool load_normals = true, + bool load_uv = false); + /// @brief Load a shell mesh from a mesh file (.obj, .stl, or .ply), without assigning material immediately. + /// @param filename Path to the mesh file. + /// @param shell_thickness Full shell thickness, in simulation length units. Must be finite and non-negative. + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded shell mesh object. + std::shared_ptr AddWavefrontShellObject(const std::string& filename, + float shell_thickness, + bool load_normals = true, + bool load_uv = false); + /// A legacy method. Use AddMesh instead. + std::shared_ptr AddWavefrontMeshObject(DEMMesh& mesh) { return AddMesh(mesh); } + + /// @brief Load a mesh type into the API-level cache as a template. + /// @details The mesh is not immediately added to the simulation, but stored as a template + /// that can be instantiated multiple times at different locations. This is similar to LoadClumpType() + /// for clump templates. + /// @param filename Path to the mesh file (.obj, .stl, or .ply). + /// @param mat Material to assign to the mesh. + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded mesh template. + std::shared_ptr LoadMeshType(const std::string& filename, + const std::shared_ptr& mat, + bool load_normals = true, + bool load_uv = false); + /// @brief Load a mesh type into the API-level cache as a template. + /// @param filename Path to the mesh file (.obj, .stl, or .ply). + /// @param load_normals Whether to load normals from the file. + /// @param load_uv Whether to load UV coordinates from the file. + /// @return A shared pointer to the loaded mesh template. + std::shared_ptr LoadMeshType(const std::string& filename, bool load_normals = true, bool load_uv = false); + /// @brief Load a mesh type into the API-level cache as a template. + /// @param mesh A DEMMesh object. + /// @return A shared pointer to the loaded mesh template. + std::shared_ptr LoadMeshType(DEMMesh& mesh); + + /// @brief Instantiate a mesh from a template and add it to the simulation. + /// @param mesh_template The mesh template (shared pointer returned from LoadMeshType). + /// @param init_pos Initial position of the mesh instance. + /// @return A shared pointer to the instantiated mesh. + std::shared_ptr AddMeshFromTemplate(const std::shared_ptr& mesh_template, + const float3& init_pos = make_float3(0)); + + /// @brief Load a rigid combined-clump template with fixed member-relative transforms. + /// @details All components must be clump templates. The combined template stores member poses in a + /// master-member-relative frame so the whole group can later be instantiated at arbitrary global pose. + /// If `component_rel_oriQ` is empty, identity orientation is assumed for all members. + /// @param component_templates Member clump templates in this combined group. + /// @param component_rel_pos Member positions in the user-provided template frame. + /// @param component_rel_oriQ Member orientations in the user-provided template frame (optional). + /// @param master_component Index of the member chosen as the master reference frame. + /// @return A combined template handle that can be instantiated via AddCombinedFromTemplate. + std::shared_ptr LoadCombinedClumpType( + const std::vector>& component_templates, + const std::vector& component_rel_pos, + const std::vector& component_rel_oriQ = std::vector(), + size_t master_component = 0); + /// @brief Load a rigid combined-mesh template with fixed member-relative transforms. + /// @details All components must be mesh templates. The combined template stores member poses in a + /// master-member-relative frame so the whole group can later be instantiated at arbitrary global pose. + /// If `component_rel_oriQ` is empty, identity orientation is assumed for all members. + /// @param component_templates Member mesh templates in this combined group. + /// @param component_rel_pos Member positions in the user-provided template frame. + /// @param component_rel_oriQ Member orientations in the user-provided template frame (optional). + /// @param master_component Index of the member chosen as the master reference frame. + /// @return A combined template handle that can be instantiated via AddCombinedFromTemplate. + std::shared_ptr LoadCombinedMeshType( + const std::vector>& component_templates, + const std::vector& component_rel_pos, + const std::vector& component_rel_oriQ = std::vector(), + size_t master_component = 0); + /// @brief Instantiate a combined template at a user-specified global pose. + /// @details Creates n_instances * n_members owners and records combined-group runtime metadata + /// used for combined-owner contact/kinematics policies. + /// @param combined_template A handle returned by LoadCombinedClumpType or LoadCombinedMeshType. + /// @param init_pos A vector of global positions for each instantiation in the batch. + /// @param init_oriQ A vector of global orientations for each instantiation in the batch. + /// @return A combined-instances handle that references all instantiated member owners in the batch. + std::shared_ptr AddCombinedFromTemplate( + const std::shared_ptr& combined_template, + const std::vector& init_pos, + const std::vector& init_oriQ = std::vector()); + /// @brief Single-pose convenience overload of AddCombinedFromTemplate. + std::shared_ptr AddCombinedFromTemplate( + const std::shared_ptr& combined_template, + const float3& init_pos, + const float4& init_oriQ = make_float4(0, 0, 0, 1)) { + return AddCombinedFromTemplate(combined_template, std::vector(1, init_pos), + std::vector(1, init_oriQ)); + } + /// @brief Allow/disallow contact generation among owners that belong to the same combined owner group. + /// @details Default is false (contacts within one combined group are suppressed). + void SetAllowIntraCombinedOwnerContacts(bool allow = true); + /// @brief Query resolved owner IDs and fixed relative transforms for one combined instance. + /// @details Returns `false` if the index is invalid or owner IDs are not resolved yet + /// (e.g., before Initialize/Update assigns owner numbering). + /// @param combined_instance_id Zero-based index in the combined-instance cache. + /// @param master_owner_id Output master owner ID of this combined group. + /// @param member_owner_ids Output owner IDs for all members (same order as template components). + /// @param member_rel_pos Output fixed member-relative positions in master frame. + /// @param member_rel_oriQ Output fixed member-relative orientations in master frame. + /// @return True if metadata is available and outputs are populated. + bool GetCombinedInstanceInfo(size_t combined_instance_id, + bodyID_t& master_owner_id, + std::vector& member_owner_ids, + std::vector& member_rel_pos, + std::vector& member_rel_oriQ); + /// @brief Number of combined instances currently cached. + /// @details This count is pre-initialization setup metadata and is cleared by cache-clearing workflows. + size_t GetNumCombinedInstances() const { return cached_combined_instances.size(); } /// @brief Create a DEMTracker to allow direct control/modification/query to this external object/batch of /// clumps/triangle mesh object. @@ -670,6 +1183,46 @@ class DEMSolver { std::shared_ptr PythonTrack(const std::shared_ptr& obj) { return Track(obj); } + /// @brief Create a single tracker that tracks all member owners in a combined-instances batch. + /// @details The tracker references all owners instantiated by the batch. Since AddCombinedFromTemplate + /// creates owners in consecutive memory, a single tracker with an offset range covers them all. + /// Use the offset parameter in tracker query methods to access individual owners. + std::shared_ptr Track(const std::shared_ptr& combined_inst) { + if (!combined_inst || combined_inst->member_objs.empty()) { + DEME_ERROR("Track received a null or empty combined-instances handle."); + } + // All member_objs were added consecutively. The first member_obj's load_order and obj_type + // define the tracker base; the tracker spans all owners across all member_objs. + const auto& first_obj = combined_inst->member_objs.front(); + if (!first_obj) { + DEME_ERROR("Track encountered a null member object in combined-instances handle."); + } + auto tracker = Track(first_obj); + + // Override nSpanOwners to cover all owners in this CombinedInstances, not just the first member's batch. + // member_objs.size() == n_instances * n_members, each contributing one owner. + tracker->obj->nSpanOwnersOverride = combined_inst->member_objs.size(); + + // Compute total geometric entities across all members for nGeos override. + size_t total_geos = 0; + const auto& templ = combined_inst->type; + if (templ->member_type == OWNER_TYPE::CLUMP) { + for (size_t i = 0; i < templ->clump_templates.size(); i++) { + total_geos += templ->clump_templates[i]->nComp; + } + // Multiply by number of instances + total_geos *= combined_inst->n_instances; + } else if (templ->member_type == OWNER_TYPE::MESH) { + for (size_t i = 0; i < templ->mesh_templates.size(); i++) { + total_geos += templ->mesh_templates[i]->GetNumTriangles(); + } + total_geos *= combined_inst->n_instances; + } + if (total_geos > 0) { + tracker->obj->nGeosOverride = total_geos; + } + return tracker; + } /// Create a inspector object that can help query some statistical info of the clumps in the simulation std::shared_ptr CreateInspector(const std::string& quantity = "clump_max_z"); @@ -707,16 +1260,16 @@ class DEMSolver { const std::string& velZ, bool dictate = true, const std::string& pre = "none"); - /// Let the linear velocities of all entites in this family always keep `as is', and not influenced by the force + /// Let the linear velocities of all entites in this family always remain "as is" and not be influenced by the force /// exerted from other simulation entites. void SetFamilyPrescribedLinVel(unsigned int ID); - /// Let the X component of the linear velocities of all entites in this family always keep `as is', and not + /// Let the X component of the linear velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedLinVelX(unsigned int ID); - /// Let the Y component of the linear velocities of all entites in this family always keep `as is', and not + /// Let the Y component of the linear velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedLinVelY(unsigned int ID); - /// Let the Z component of the linear velocities of all entites in this family always keep `as is', and not + /// Let the Z component of the linear velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedLinVelZ(unsigned int ID); @@ -736,16 +1289,16 @@ class DEMSolver { const std::string& velZ, bool dictate = true, const std::string& pre = "none"); - /// Let the linear velocities of all entites in this family always keep `as is', and not influenced by the force - /// exerted from other simulation entites. + /// Let the angular velocities of all entites in this family always remain "as is" and not be influenced by the + /// force exerted from other simulation entites. void SetFamilyPrescribedAngVel(unsigned int ID); - /// Let the X component of the angular velocities of all entites in this family always keep `as is', and not + /// Let the X component of the angular velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedAngVelX(unsigned int ID); - /// Let the Y component of the angular velocities of all entites in this family always keep `as is', and not + /// Let the Y component of the angular velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedAngVelY(unsigned int ID); - /// Let the Z component of the angular velocities of all entites in this family always keep `as is', and not + /// Let the Z component of the angular velocities of all entites in this family always remain "as is" and not be /// influenced by the force exerted from other simulation entites. void SetFamilyPrescribedAngVelZ(unsigned int ID); @@ -755,7 +1308,7 @@ class DEMSolver { /// @param Y Y coordinate (can be an expression). /// @param Z Z coordinate (can be an expression). /// @param dictate If true, prevent entities in this family to have (both linear and rotational) positional updates - /// resulted from the `simulation physics'; if false, only specified components (that is, not specified with "none") + /// resulted from the "simulation physics"; if false, only specified components (that is, not specified with "none") /// will not be influenced by the force exerted from other simulation entites. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. @@ -765,27 +1318,31 @@ class DEMSolver { const std::string& Z, bool dictate = true, const std::string& pre = "none"); - /// @brief Let the linear positions of all entites in this family always keep `as is'. + /// @brief Let the linear positions of all entites in this family always remain "as is". void SetFamilyPrescribedPosition(unsigned int ID); - /// @brief Let the X component of the linear positions of all entites in this family always keep `as is'. + /// @brief Let the X component of the linear positions of all entites in this family always remain "as is". void SetFamilyPrescribedPositionX(unsigned int ID); - /// @brief Let the Y component of the linear positions of all entites in this family always keep `as is'. + /// @brief Let the Y component of the linear positions of all entites in this family always remain "as is". void SetFamilyPrescribedPositionY(unsigned int ID); - /// @brief Let the Z component of the linear positions of all entites in this family always keep `as is'. + /// @brief Let the Z component of the linear positions of all entites in this family always remain "as is". void SetFamilyPrescribedPositionZ(unsigned int ID); /// @brief Keep the orientation quaternions of all entites in this family to remain exactly the user-specified /// values. /// @param ID Family number. - /// @param q_formula The code from which the quaternion should be calculated. Must `return' a float4. For example, + /// @param q_formula The code from which the quaternion should be calculated. Must return a float4. For example, /// "float tmp=make_float4(1,1,1,1); return tmp;". /// @param dictate If true, prevent entities in this family to have (both linear and rotational) positional updates - /// resulted from the `simulation physics'; otherwise, the `simulation physics' still takes effect. + /// resulted from the "simulation physics"; otherwise, the "simulation physics" still takes effect. void SetFamilyPrescribedQuaternion(unsigned int ID, const std::string& q_formula, bool dictate = true); - /// @brief Let the orientation quaternions of all entites in this family always keep `as is'. + /// @brief Let the orientation quaternions of all entites in this family always remain "as is". void SetFamilyPrescribedQuaternion(unsigned int ID); /// @brief The entities in this family will always experience an extra acceleration defined using this method. + /// @param ID Family number. + /// @param X X component of the acceleration expression. + /// @param Y Y component of the acceleration expression. + /// @param Z Z component of the acceleration expression. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. void AddFamilyPrescribedAcc(unsigned int ID, @@ -795,6 +1352,10 @@ class DEMSolver { const std::string& pre = "none"); /// @brief The entities in this family will always experience an extra angular acceleration defined using this /// method. + /// @param ID Family number. + /// @param X X component of the angular-acceleration expression. + /// @param Y Y component of the angular-acceleration expression. + /// @param Z Z component of the angular-acceleration expression. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. void AddFamilyPrescribedAngAcc(unsigned int ID, @@ -804,7 +1365,11 @@ class DEMSolver { const std::string& pre = "none"); /// @brief The entities in this family will always experience an added linear-velocity correction defined using this - /// method. At the same time, they are still subject to the `simulation physics'. + /// method. At the same time, they are still subject to the "simulation physics". + /// @param ID Family number. + /// @param X X component of the velocity-correction expression. + /// @param Y Y component of the velocity-correction expression. + /// @param Z Z component of the velocity-correction expression. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. void CorrectFamilyLinVel(unsigned int ID, @@ -813,7 +1378,11 @@ class DEMSolver { const std::string& Z, const std::string& pre = "none"); /// @brief The entities in this family will always experience an added angular-velocity correction defined using - /// this method. At the same time, they are still subject to the `simulation physics'. + /// this method. At the same time, they are still subject to the "simulation physics". + /// @param ID Family number. + /// @param X X component of the angular-velocity-correction expression. + /// @param Y Y component of the angular-velocity-correction expression. + /// @param Z Z component of the angular-velocity-correction expression. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. void CorrectFamilyAngVel(unsigned int ID, @@ -823,7 +1392,11 @@ class DEMSolver { const std::string& pre = "none"); /// @brief The entities in this family will always experience an added positional correction defined using this - /// method. At the same time, they are still subject to the `simulation physics'. + /// method. At the same time, they are still subject to the "simulation physics". + /// @param ID Family number. + /// @param X X component of the position-correction expression. + /// @param Y Y component of the position-correction expression. + /// @param Z Z component of the position-correction expression. /// @param pre Prerequisite code. For example, you can generate a float3 with this prerequisite code, then assign /// XYZ components based on this float3. void CorrectFamilyPosition(unsigned int ID, @@ -832,8 +1405,9 @@ class DEMSolver { const std::string& Z, const std::string& pre = "none"); /// @brief The entities in this family will always experience an added quaternion correction defined using this - /// method. At the same time, they are still subject to the `simulation physics'. - /// @param q_formula The code from which the quaternion should be calculated. Must `return' a float4. For example, + /// method. At the same time, they are still subject to the "simulation physics". + /// @param ID Family number. + /// @param q_formula The code from which the quaternion should be calculated. Must return a float4. For example, /// "float tmp=make_float4(1,1,1,1); return tmp;". void CorrectFamilyQuaternion(unsigned int ID, const std::string& q_formula); @@ -841,9 +1415,6 @@ class DEMSolver { void SetContactWildcards(const std::set& wildcards); /// @brief Set the names for the extra quantities that will be associated with each owner. void SetOwnerWildcards(const std::set& wildcards); - /// @brief Set the names for the extra quantities that will be associated with each geometry entity (such as sphere, - /// triangle). - void SetGeometryWildcards(const std::set& wildcards); /// @brief Change the value of contact wildcards to val if either of the contact geometries is in family N. /// @param N Family number. If one contact geometry is in N, this contact wildcard is modified. @@ -928,22 +1499,21 @@ class DEMSolver { std::vector& forces, std::vector& torques, bool torque_in_local = false); - - /// @brief Set the wildcard values of some triangles. - /// @param geoID The ID of the starting (first) triangle that needs to be modified. - /// @param name The name of the wildcard. - /// @param vals A vector of values that will be assigned to the triangles starting from geoID. - void SetTriWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals); - /// @brief Set the wildcard values of some spheres. - /// @param geoID The ID of the starting (first) sphere that needs to be modified. - /// @param name The name of the wildcard. - /// @param vals A vector of values that will be assigned to the spheres starting from geoID. - void SetSphereWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals); - /// @brief Set the wildcard values of some analytical components. - /// @param geoID The ID of the starting (first) analytical component that needs to be modified. - /// @param name The name of the wildcard. - /// @param vals A vector of values that will be assigned to the analytical components starting from geoID. - void SetAnalWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals); + /// Compact contact points and forces concerning ownerIDs into caller-provided CUDA memory. + /// Capacity must be at least the solver's current total contact count; the returned value is the useful count. + size_t GetOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + size_t capacity, + int destination_device); + /// Compact contact points, forces, and extra torques concerning ownerIDs into caller-provided CUDA memory. + size_t GetOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bool torque_in_local = false); /// @brief Set the wildcard values of some owners. /// @param ownerID The ID of the starting (first) owner that needs to be modified. @@ -989,30 +1559,20 @@ class DEMSolver { /// @param n Total number of owners to query, starting from ownerID. /// @return Value of this wildcard. std::vector GetOwnerWildcardValue(bodyID_t ownerID, const std::string& name, bodyID_t n = 1); + /// Fill caller-provided CUDA memory with one owner wildcard over a consecutive owner range. + /// @param validate See GetOwnerPositionToDevice. + void GetOwnerWildcardValueToDevice(float* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + const std::string& name, + bodyID_t n = 1, + bool validate = true); /// @brief Get the owner wildcard's values of all entities. std::vector GetAllOwnerWildcardValue(const std::string& name); /// @brief Get the owner wildcard's values of all entities in family N. std::vector GetFamilyOwnerWildcardValue(unsigned int N, const std::string& name); - /// @brief Get the geometry wildcard's values of a series of triangles. - /// @param geoID The ID of the first triangle. - /// @param name Wildcard's name. - /// @param n The number of triangles to query following the ID of the first one. - /// @return Vector of values of the wildcards. - std::vector GetTriWildcardValue(bodyID_t geoID, const std::string& name, size_t n); - /// @brief Get the geometry wildcard's values of a series of spheres. - /// @param geoID The ID of the first sphere. - /// @param name Wildcard's name. - /// @param n The number of spheres to query following the ID of the first one. - /// @return Vector of values of the wildcards. - std::vector GetSphereWildcardValue(bodyID_t geoID, const std::string& name, size_t n); - /// @brief Get the geometry wildcard's values of a series of analytical entities. - /// @param geoID The ID of the first analytical entity. - /// @param name Wildcard's name. - /// @param n The number of analytical entities to query following the ID of the first one. - /// @return Vector of values of the wildcards. - std::vector GetAnalWildcardValue(bodyID_t geoID, const std::string& name, size_t n); - /// @brief If the user used async-ed version of a tracker's get/set methods (to get a speed boost in many piecemeal /// accesses of a long array), this method should be called to mark the end of to-host transactions. But usually, /// the user would use sync-ed version of the methods by default and this call is not needed in that case. @@ -1042,32 +1602,25 @@ class DEMSolver { const std::pair& Z = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), const std::set& orig_fam = std::set()); - /// Change the sizes of the clumps by a factor. This method directly works on the clump components spheres, - /// therefore requiring sphere components to be store in flattened array (default behavior), not jitified templates. - void ChangeClumpSizes(const std::vector& IDs, const std::vector& factors); - /// If true, each jitification string substitution will do a one-liner to one-liner replacement, so that if the - /// kernel compilation fails, the error meessage line number will reflex the actual spot where that happens (instead + /// kernel compilation fails, the error message line number will reflex the actual spot where that happens (instead /// of some random number) void EnsureKernelErrMsgLineNum(bool flag = true) { ensure_kernel_line_num = flag; } - /// Whether the force collection (acceleration calc and reduction) process should be using CUB. If true, the - /// acceleration array is flattened and reduced using CUB; if false, the acceleration is computed and directly - /// applied to each body through atomic operations. - void UseCubForceCollection(bool flag = true) { use_cub_to_reduce_force = flag; } - /// Reduce contact forces to accelerations right after calculating them, in the same kernel, instead of in a - /// separate pass over the contact array (the default). Measured on DEMdemo_PlateSinkage with 3-sphere clumps at - /// 150k to 600k grains: about +21% throughput on an NVIDIA Blackwell GPU and +96% to +204% on an AMD MI350X, - /// where the separate pass is the dominant cost and also slows the concurrent contact detection. Contact-force - /// recording and output are unaffected; the only capability given up is tracker force-pair queries, which throw - /// while this is on. + /// separate pass over the contact array (the default). Upstream main measured DEMdemo_PlateSinkage with 3-sphere + /// clumps at 150k to 600k grains: about +21% throughput on an NVIDIA Blackwell GPU and +96% to +204% on an AMD + /// MI350X, where the separate pass is the dominant cost and also slows concurrent contact detection. These are + /// upstream measurements, not a performance guarantee for this branch. Contact-force recording and output are + /// unaffected; tracker force-pair queries throw while this is on. + /// @note After initialization, call UpdateSimParams() for this change to take effect in dT. void SetCollectAccRightAfterForceCalc(bool flag = true) { collect_force_in_force_kernel = flag; } /// Instruct the solver that there is no need to record the contact force (and contact point location etc.) in an /// array. If set to true, the contact forces must be reduced to accelerations right in the force calculation kernel /// (meaning SetCollectAccRightAfterForceCalc is effectively called too). Calling this method could reduce some /// memory usage, but will disable contact pair output. + /// @note After initialization, call UpdateSimParams() for this change to take effect in dT. void SetNoForceRecord(bool flag = true) { no_recording_contact_forces = flag; if (flag) @@ -1096,15 +1649,24 @@ class DEMSolver { /// Remove host-side cached vectors (so you can re-define them, and then re-initialize system) void ClearCache(); + /// Output methods enqueue asynchronous writes; call WaitForPendingOutput() to block for completion. /// Write the current status of clumps to a file void WriteClumpFile(const std::string& outfilename, unsigned int accuracy = 10) const; void WriteClumpFile(const std::filesystem::path& outfilename, unsigned int accuracy = 10) const { WriteClumpFile(outfilename.string(), accuracy); } - /// Write the current status of `clumps' to a file, but not as clumps, instead, as each individual sphere. This may - /// make small-scale rendering easier. + /// Write the current status of "clumps" to a file, but not as clumps, instead, as each individual sphere. CSV and + /// VTK formats are supported. VTK stores sphere centers and the `r` radius scalar as point data for ParaView's + /// Glyph filter, avoiding duplicated sphere tessellations in every output frame. void WriteSphereFile(const std::string& outfilename) const; void WriteSphereFile(const std::filesystem::path& outfilename) const { WriteSphereFile(outfilename.string()); } + /// @brief Write directly displayable VTK surfaces for all analytical boundary components. + /// @details Infinite primitives are clipped to the domain specified by InstructBoxDomainDimension. + void WriteAnalyticalFile(const std::string& outfilename, unsigned int circumferential_resolution = 32) const; + void WriteAnalyticalFile(const std::filesystem::path& outfilename, + unsigned int circumferential_resolution = 32) const { + WriteAnalyticalFile(outfilename.string(), circumferential_resolution); + } /// @brief Write all contact pairs to a file. /// @details The outputted torque using this method is in global, rather than each object's local coordinate system. /// @param outfilename Output filename. @@ -1124,6 +1686,8 @@ class DEMSolver { /// Write the current status of all meshes to a file. void WriteMeshFile(const std::string& outfilename) const; void WriteMeshFile(const std::filesystem::path& outfilename) const { WriteMeshFile(outfilename.string()); } + /// Wait for any in-flight async output to finish. + void WaitForPendingOutput() const; /// @brief Read 3 columns of your choice from a CSV filem and group them by clump_header. /// @param infilename CSV filename. @@ -1270,10 +1834,13 @@ class DEMSolver { /// initialization. void UpdateSimParams(); - /// @brief TTransfer newly loaded clumps to the GPU-side in mid-simulation. - void UpdateClumps(); + /// @brief Transfer newly loaded clumps and meshes to the GPU-side in mid-simulation. + void Update(); + + /// @brief Legacy method name for Update. + void UpdateClumps() { Update(); } - /// @brief Update the time step size. Used after system initialization. + /// @brief Legacy post-initialization name for SetTimeStepSize. Kept for backward compatibility. /// @param ts Time step size. void UpdateStepSize(double ts); @@ -1284,8 +1851,11 @@ class DEMSolver { /// Show the wall time and percentages of wall time spend on various solver tasks. void ShowTimingStats(); - /// Show potential anomalies that may have been there in the simulation, then clear the anomaly log. - void ShowAnomalies(); + /// Enable or disable GPU event-based timing. Disabling this avoids cudaEvent overhead in timed sections. + /// Call this when the solver is not actively running dynamics. + void SetGPUTimersEnabled(bool enabled); + /// Return whether GPU event-based timing is enabled. + bool GetGPUTimersEnabled() const { return m_gpu_timers_enabled; } /// Reset the collaboration stats between dT and kT back to the initial value (0). You should call this if you want /// to start over and re-inspect the stats of the new run; otherwise, it is generally not needed, you can go ahead @@ -1328,24 +1898,30 @@ class DEMSolver { void SetContactOutputContent(unsigned int content) { m_cnt_out_content = content; } /// Specify the file format of meshes. void SetMeshOutputFormat(MESH_FORMAT format) { m_mesh_out_format = format; } + /// Specify per-triangle metadata to include in mesh VTK output. XYZ geometry is always written. + void SetMeshOutputContent(unsigned int content) { m_mesh_out_content = content; } + void SetMeshOutputContent(MESH_OUTPUT_CONTENT content) { m_mesh_out_content = static_cast(content); } + /// Enable/disable patch color metadata in PLY mesh output. + void EnableMeshPatchColorOutput(bool enable = true) { m_mesh_out_ply_patch_colors = enable; } /// Enable/disable outputting owner wildcard values to file. void EnableOwnerWildcardOutput(bool enable = true) { m_is_out_owner_wildcards = enable; } /// Enable/disable outputting contact wildcard values to the contact file. void EnableContactWildcardOutput(bool enable = true) { m_is_out_cnt_wildcards = enable; } - /// Enable/disable outputting geometry wildcard values to the contact file. - void EnableGeometryWildcardOutput(bool enable = true) { m_is_out_geo_wildcards = enable; } /// @brief Set the verbosity level of the solver. - /// @param verbose "QUIET", "ERROR", "WARNING", "INFO", "STEP_ANOMALY", "STEP_METRIC", "DEBUG" or "STEP_DEBUG". - /// Recommend "INFO". + /// @param verbose "QUIET", "ERROR", "WARNING", "INFO", "METRIC" or "DEBUG". Recommend "INFO". void SetVerbosity(const std::string& verbose); + /// @brief Set output detail level. + void SetVerbosity(verbosity_t verbose); /// @brief Choose sphere and clump output file format. - /// @param format Choice among "CSV", "BINARY". + /// @param format Choice among "CSV", "BINARY", and "VTK". VTK is supported by WriteSphereFile. void SetOutputFormat(const std::string& format); /// @brief Specify the information that needs to go into the clump or sphere output files. /// @param content A list of "XYZ", "QUAT", "ABSV", "VEL", "ANG_VEL", "ABS_ACC", "ACC", "ANG_ACC", "FAMILY", "MAT", - /// "OWNER_WILDCARD" and/or "GEO_WILDCARD". + /// and/or "OWNER_WILDCARD". void SetOutputContent(const std::vector& content); + /// Specify mesh VTK output fields by name. Supported names mirror MESH_OUTPUT_CONTENT. + void SetMeshOutputContent(const std::vector& content); /// @brief Specify the file format of contact pairs. /// @param format Choice among "CSV", "BINARY". void SetContactOutputFormat(const std::string& format); @@ -1356,6 +1932,13 @@ class DEMSolver { /// @brief Specify the output file format of meshes. /// @param format A choice between "VTK", "OBJ". void SetMeshOutputFormat(const std::string& format); + /// @brief Clear stored solver logs (errors, warnings, messages). + void ClearLog() { Logger::GetInstance().Clear(); } + /// @brief Show error and warnings. + void ShowWarnings(); + /// @brief Show the anomalies that have been recorded in the log. This is useful for debugging and for users to + /// understand what potentially went wrong in their simulation. + void ShowAnomalies(); // void SetOutputContent(const std::string& content) { SetOutputContent({content}); } // void SetContactOutputContent(const std::string& content) { SetContactOutputContent({content}); } @@ -1374,26 +1957,45 @@ class DEMSolver { void PrintKinematicScratchSpaceUsage() const { kT->printScratchSpaceUsage(); } /// Let dT do this call and return the reduce value of the inspected quantity. - float dTInspectReduce(const std::shared_ptr& inspection_kernel, + float dTInspectReduce(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, - bool all_domain); - float* dTInspectNoReduce(const std::shared_ptr& inspection_kernel, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes); + float* dTInspectNoReduce(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, - bool all_domain); + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes); + float dTInspectReduceDevice(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes); + float* dTInspectNoReduceDevice(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes); private: + /// Validate device placement and construct the two GPU workers on their assigned devices. + void constructWorkers(const std::vector& device_ids); + //////////////////////////////////////////////////////////////////////////////// // Flag-like behavior-related variables cached on the host side //////////////////////////////////////////////////////////////////////////////// // Verbosity - VERBOSITY verbosity = INFO; - // If true, dT should sort contact arrays (based on contact type) before usage - bool should_sort_contacts = true; + verbosity_t verbosity = VERBOSITY_INFO; // If true, the solvers may need to do a per-step sweep to apply family number changes bool famnum_can_change_conditionally = false; @@ -1425,13 +2027,16 @@ class DEMSolver { // The output file content for contact pairs unsigned int m_cnt_out_content = CNT_OUTPUT_CONTENT::OWNER | CNT_OUTPUT_CONTENT::GEO_ID | CNT_OUTPUT_CONTENT::FORCE | CNT_OUTPUT_CONTENT::CNT_POINT | - CNT_OUTPUT_CONTENT::CNT_WILDCARD; + CNT_OUTPUT_CONTENT::NORMAL | CNT_OUTPUT_CONTENT::CNT_WILDCARD; // The output file format for meshes MESH_FORMAT m_mesh_out_format = MESH_FORMAT::VTK; + // Mesh geometry is always output; optional per-triangle VTK fields default to none. + unsigned int m_mesh_out_content = static_cast(MESH_OUTPUT_CONTENT::XYZ); + bool m_mesh_out_ply_patch_colors = false; // If the solver should output wildcards to file bool m_is_out_owner_wildcards = false; bool m_is_out_cnt_wildcards = false; - bool m_is_out_geo_wildcards = false; + mutable std::thread m_output_thread; // User-instructed simulation `world' size. Note it is an approximate of the true size and we will generate a world // not smaller than this. This is useful if the user want to automatically add BCs enclosing this user-defined @@ -1494,7 +2099,17 @@ class DEMSolver { // User-instructed approximate maximum velocity (of any point on a body in the simulation) float m_approx_max_vel = DEME_HUGE_FLOAT; // The inspector that will be used for querying system max velocity - std::shared_ptr m_approx_max_vel_func; + std::shared_ptr m_approx_vel_func; + // The inspector that will be used for querying system angular velocity magnitude + std::shared_ptr m_approx_angvel_func; + + // User-instructed maximum tri-tri penetration margin (to prevent super large margins) + double m_max_tritri_penetration = DEME_HUGE_FLOAT; + // Ratio threshold for rejecting suspicious tri-tri contacts based on penetration depth vs center-contact distance. + float m_triTriContactRejectionRatio = 0.8f; + // Whether angular velocity contributes to the contact margin (auto-detected if not user-set). + bool m_use_angvel_margin = true; + bool m_use_angvel_margin_user_set = false; // The number of user-estimated (max) number of owners that will be present in the simulation. If 0, then the arrays // will just be resized at intialization based on the input size. @@ -1503,19 +2118,17 @@ class DEMSolver { // Whether the GPU-side systems have been initialized bool sys_initialized = false; // Smallest sphere radius (used to let the user know whether the expand factor is sufficient) - float m_smallest_radius = FLT_MAX; + float m_smallest_radius = DEME_HUGE_FLOAT; // The number of dT steps before it waits for a kT update. The default value means every dT step will wait for a // newly produced contact-pair info (from kT) before proceeding. - int m_suggestedFutureDrift = 40; + int m_suggestedFutureDrift = 12; // This is an unused variable which is supposed to be related to m_suggestedFutureDrift... - int m_updateFreq = 20; + int m_updateFreq = 6; // The extra libs that the kernels need to include. - // Default: none (curand/hiprand not used by built-in kernels). - // Users can add custom includes via SetKernelInclude() if needed. - std::string kernel_includes = ""; + std::string kernel_includes = "#include \n"; // If and how we should add boundaries to the simulation world upon initialization. Choose between none, all and // top_open. @@ -1529,15 +2142,14 @@ class DEMSolver { // If we should ensure that when kernel jitification fails, the line number reported reflexes where error happens bool ensure_kernel_line_num = false; - // If we should flatten then reduce forces (true), or use atomic operation to reduce forces (false) - bool use_cub_to_reduce_force = false; - // If the solver sees there are more spheres in a bin than a this `maximum', it errors out unsigned int threshold_too_many_spheres_in_bin = 32768; // If the solver sees there are more triangles in a bin than a this `maximum', it errors out unsigned int threshold_too_many_tri_in_bin = 32768; // The max velocity at which the simulation should error out float threshold_error_out_vel = 1e3; + // The max angular velocity at which the simulation should error out + float threshold_error_out_angvel = DEME_HUGE_FLOAT; // Num of steps that kT takes average before making a conclusion on the performance of this bin size unsigned int auto_adjust_observe_steps = 25; // See corresponding method for those... @@ -1554,9 +2166,11 @@ class DEMSolver { bool no_recording_contact_forces = false; // See SetCollectAccRightAfterForceCalc bool collect_force_in_force_kernel = false; + // See SetGPUTimersEnabled + bool m_gpu_timers_enabled = true; // Error-out avg num contacts - float threshold_error_out_num_cnts = 100.; + float threshold_error_out_num_cnts = 300.; // Integrator type TIME_INTEGRATOR m_integrator = TIME_INTEGRATOR::EXTENDED_TAYLOR; @@ -1592,6 +2206,10 @@ class DEMSolver { size_t nSpheresGM = 0; // Total number of triangle facets size_t nTriGM = 0; + // Total number of triangles that need neighbor info (compact neighbor array size) + size_t nTriNeighbors = 0; + // Total number of mesh patches + size_t nMeshPatches = 0; // Number of analytical entites (as components of some external objects) unsigned int nAnalGM = 0; // Total number of owner bodies @@ -1613,11 +2231,21 @@ class DEMSolver { size_t nTriObjLoad = 0; // Number of clump templates loaded. Never decreases. size_t nClumpTemplateLoad = 0; + // Number of mesh templates loaded. Never decreases. + size_t nMeshTemplateLoad = 0; + // Number of combined templates loaded. Never decreases. + size_t nCombinedTemplateLoad = 0; + // Internal mesh-template identity counter. Explicit LoadMeshType calls and legacy AddMesh calls both consume a + // mark, but AddMeshFromTemplate preserves the already-loaded template mark. + size_t nMeshTemplateMarks = 0; + // If enabled, meshes receive one patch per triangle as they enter the solver's cache or template collection. + bool use_deme2_mesh_behavior = false; // Number of materials loaded. Never decreases. size_t nMaterialsLoad = 0; // The above quantities, when they were last time initialized. Used for sanity checks at user re-initialization. size_t nLastTimeClumpTemplateLoad = 0; + size_t nLastTimeMeshTemplateLoad = 0; unsigned int nLastTimeExtObjLoad = 0; size_t nLastTimeBatchClumpsLoad = 0; size_t nLastTimeTriObjLoad = 0; @@ -1637,11 +2265,12 @@ class DEMSolver { // Num of clump templates types, basically. It's also the number of clump template mass properties. unsigned int nDistinctClumpBodyTopologies; - // A design choice is that each analytical obj and meshed obj is its own mass type, so the following 2 quantities - // are not independent, so we just won't use them Num of analytical objects loaded unsigned int - // nExtObjMassProperties; Num of meshed objects loaded unsigned int nMeshMassProperties; + // Analytical objects each keep their own mass type. Meshes use per-owner mass types when mass/MOI are flattened, + // and compact template-level mass/MOI entries when mass/MOI are jitified. + // Num of analytical objects loaded unsigned int nExtObjMassProperties; Num of meshed mass properties loaded + // unsigned int nMeshMassProperties; - // Sum of the above 3 items (but in fact nDistinctClumpBodyTopologies + nExtObj + nTriMeshes) + // Sum of clump template, analytical object and mesh mass/MOI entries for the active mass-property mode. unsigned int nDistinctMassProperties; // Num of material types @@ -1664,13 +2293,11 @@ class DEMSolver { // A map that records the numbering for user-defined owner wildcards std::unordered_map m_owner_wc_num; - // A map that records the numbering for user-defined geometry wildcards - std::unordered_map m_geo_wc_num; // A map that records the numbering for user-defined per-contact wildcards std::unordered_map m_cnt_wc_num; // Meshes cached on dT side that has corresponding owner number associated. Useful for modifying meshes. - std::vector> m_meshes; + std::vector> m_meshes; // A map between the owner of mesh, and the offset this mesh lives in m_meshes array. std::unordered_map m_owner_mesh_map; @@ -1694,6 +2321,11 @@ class DEMSolver { // templates, not including triangles, analytical geometries etc. std::vector> m_templates; + // Cached mesh templates (not yet instantiated in simulation) + std::vector> m_mesh_templates; + // Cached combined templates (template-level only). + std::vector> m_combined_templates; + // Shared pointers to a batch of clumps loaded into the system. Through this returned handle, the user can further // specify the vel, ori etc. of this batch of clumps. std::vector> cached_input_clump_batches; @@ -1702,7 +2334,46 @@ class DEMSolver { std::vector> cached_extern_objs; // Shared pointers to meshed objects cached at the API system - std::vector> cached_mesh_objs; + std::vector> cached_mesh_objs; + // Combined template instances. + std::vector> cached_combined_instances; + bool m_combined_runtime_dirty = true; + bool m_allow_intra_combined_owner_contacts = false; + + // User-requested per-triangle P/V/P*V debug tracking owners. + std::vector m_user_tri_pv_tracking_owners; + struct TrianglePVSnapshot { + std::vector avgP; + std::vector avgV; + std::vector avgPV; + }; + std::unordered_map m_last_tri_pv_snapshot; + + struct MeshWearModelState { + double wear_rate = 0.0; + double update_interval = 0.0; + double start_time = 0.0; + double end_time = -1.0; + float normal_sign = -1.f; + size_t tri_start = 0; + size_t tri_count = 0; + double pending_time = 0.0; + std::vector pending_depth; + // Maps duplicate/coincident mesh nodes to one canonical vertex group for smooth wear deformation. + std::vector vertex_to_canon; + size_t n_canon_vertices = 0; + // Reference triangle normals in mesh-local coordinates captured when wear is enabled. + std::vector ref_tri_normals; + // Upper bound on one mesh-deformation substep's applied wear depth per triangle. + float max_depth_per_update = 0.f; + // Internal safety-cap fraction of median edge length used to compute max_depth_per_update. + float max_depth_fraction_of_median_edge = 0.5f; + // Median edge length measured when wear model is enabled. + float median_edge_length = 0.f; + // Warning throttle counter for cap-hit messages. + size_t cap_warning_count = 0; + }; + std::unordered_map m_mesh_wear_models; // User-input prescribed motion std::vector m_input_family_prescription; @@ -1739,6 +2410,8 @@ class DEMSolver { std::vector m_input_mesh_obj_xyz; std::vector m_input_mesh_obj_rot; std::vector m_input_mesh_obj_family; + std::vector m_input_mesh_obj_convex; + std::vector m_input_mesh_obj_never_winner; // Processed unique family prescription info std::vector m_unique_family_prescription; @@ -1750,7 +2423,7 @@ class DEMSolver { // Flattened (analytical) object component definition arrays, potentially jitifiable // These extra analytical entities' owners' ID will be appended to those added thru normal AddClump - std::vector m_anal_owner; + std::vector m_anal_owner; // Material types of these analytical geometries std::vector m_anal_materials; // Initial locations of this obj's components relative to obj's CoM @@ -1758,11 +2431,11 @@ class DEMSolver { // Some float3 quantity that is representitive of a component's initial orientation (such as plane normal, and its // meaning can vary among different types) std::vector m_anal_comp_rot; - // Some float quantity that is representitive of a component's size (e.g. cylinder radius or cone slope) + // Some float quantity that is representitive of a component's size (e.g. for a cylinder, top radius) std::vector m_anal_size_1; - // Some float quantity that is representitive of a component's size (e.g. cone lower axial bound) + // Some float quantity that is representitive of a component's size (e.g. for a cylinder, bottom radius) std::vector m_anal_size_2; - // Some float quantity that is representitive of a component's size (e.g. cone upper axial bound) + // Some float quantity that is representitive of a component's size (e.g. for a cylinder, its length) std::vector m_anal_size_3; // Component object types std::vector m_anal_types; @@ -1770,13 +2443,36 @@ class DEMSolver { // topologically a plane then this param is meaningless, since its normal is determined by its rotation. std::vector m_anal_normals; - // These extra mesh facets' owners' ID will be appended to analytical entities' - std::vector m_mesh_facet_owner; - // Material types of these mesh facets - std::vector m_mesh_facet_materials; - // Material types of these mesh facets + // Persistent analytical definitions used for visualization after the flattened initialization arrays above are + // released. Runtime owner IDs and transforms remain in dT and are combined with these local-frame definitions at + // output time. + struct AnalyticalOutputDefinition { + objType_t type; + float3 position; + float3 axis; + float size_1; + float size_2; + float size_3; + float normal_sign; + }; + std::vector m_anal_output_definitions; + + // These mesh facets' owners' ID, flattened + std::vector m_mesh_facet_owner; + // Patch ID for each mesh facet, flattened + std::vector m_mesh_facet_patch; + // Per-facet edge neighbors (global triangle indices, NULL_BODYID if boundary) + std::vector m_mesh_facet_neighbor1; + std::vector m_mesh_facet_neighbor2; + std::vector m_mesh_facet_neighbor3; + // Three nodes of each triangle, flattened std::vector m_mesh_facets; + // These mesh patches' owners' ID, flattened + std::vector m_mesh_patch_owner; + // Material types of these mesh patches + std::vector m_mesh_patch_materials; + // Clump templates will be flatten and transferred into kernels upon Initialize() std::vector m_template_clump_mass; std::vector m_template_clump_moi; @@ -1791,6 +2487,10 @@ class DEMSolver { // Meshed objects that will be flatten and transferred into kernels upon Initialize() std::vector m_mesh_obj_mass; std::vector m_mesh_obj_moi; + // Deduped mesh mass/MOI lists for jitified mass properties and per-mesh offsets into those lists. + std::vector m_mesh_mass_jit; + std::vector m_mesh_moi_jit; + std::vector m_mesh_mass_offsets; /* // Dan and Ruochun decided NOT to extract unique input values. // Instead, we trust users: we simply store all clump template info users give. @@ -1823,6 +2523,8 @@ class DEMSolver { std::unique_ptr kTMain_InteractionManager; std::unique_ptr dT; std::unique_ptr kT; + // Logical CUDA device IDs assigned to the dynamic and kinematic workers, respectively. + std::vector m_gpu_device_ids; //////////////////////////////////////////////////////////////////////////////// // DEM system's private methods @@ -1845,6 +2547,8 @@ class DEMSolver { void preprocessClumpTemplates(); /// Count the number of `things' that should be in the simulation now void updateTotalEntityNum(); + /// Decide whether mesh mass/MOI can remain jitified or should use flattened owner-level arrays. + void decideMeshMassJitification(); /// Jitify GPU kernels, based on pre-processed user inputs void jitifyKernels(); /// Figure out the unit length l and numbers of voxels along each direction, based on domain size X, Y, Z @@ -1899,6 +2603,8 @@ class DEMSolver { size_t nSpheres, size_t nTriMesh, size_t nFacets, + size_t nTriNeighbors, + size_t nMeshPatches, unsigned int nExtObj_old, unsigned int nAnalGM_old); /// Add content to the flattened analytical component array. @@ -1918,9 +2624,12 @@ class DEMSolver { void assertSysInit(const std::string& method_name); /// Assert that the DEM simulation system is not initialized void assertSysNotInit(const std::string& method_name); - /// Print due information on worker threads reported anomalies - bool goThroughWorkerAnomalies(); /// @brief Implementation of getting (unsorted) contact pairs from dT. + /// @param idA Output owner IDs for the first geometry in each contact. + /// @param idB Output owner IDs for the second geometry in each contact. + /// @param cnt_type Output contact types. + /// @param famA Output families for the first geometry in each contact. + /// @param famB Output families for the second geometry in each contact. /// @param type_func Exclude certain contact types from being outputted if this evaluates to false. void getContacts_impl(std::vector& idA, std::vector& idB, @@ -1928,6 +2637,20 @@ class DEMSolver { std::vector& famA, std::vector& famB, std::function type_func) const; + /// Resolve owner IDs for combined instances once owner numbering is available. + void resolveCombinedOwners(size_t nExistOwners = 0); + /// Refresh flattened combined-owner runtime metadata in worker arrays. + void refreshCombinedRuntimeResources(); + /// Resolve global triangle range for a mesh owner in dT's flattened triangle soup. + bool findOwnerTriangleRange(bodyID_t ownerID, size_t& tri_start, size_t& tri_count); + /// Rebuild dT per-triangle PV tracking owner list from user-tracking and wear-model owners. + void refreshTrianglePVTrackingOwners(); + /// Cache the current dT per-triangle PV window for non-reset display queries. + void cacheTrackedTrianglePVWindow(); + /// Apply all active mesh wear models over this DoDynamics call's time interval. + void updateMeshWearModels(double call_start_time, double call_end_time); + /// Apply one bounded pending-wear chunk of one mesh owner to its node positions. + bool applyMeshWearModel(bodyID_t ownerID, MeshWearModelState& model); /// The implimentation of persistency assignment void assignFamilyPersistentContact_impl( unsigned int N1, diff --git a/src/DEM/APIPrivate.cpp b/src/DEM/APIPrivate.cpp index a1e3333c..bcc22640 100644 --- a/src/DEM/APIPrivate.cpp +++ b/src/DEM/APIPrivate.cpp @@ -6,7 +6,8 @@ #include #include "API.h" #include "Defines.h" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" +#include "utils/MeshUtils.hpp" #include #include @@ -15,6 +16,7 @@ #include #include #include +#include namespace deme { @@ -30,36 +32,41 @@ void DEMSolver::assertSysNotInit(const std::string& method_name) { } } +// Persistent contact is always primitive contact-based void DEMSolver::assignFamilyPersistentContact_impl( unsigned int N1, unsigned int N2, notStupidBool_t is_or_not, const std::function& condition) { - if (kT->solverFlags.isHistoryless) { + if (!kT->solverFlags.hasPersistentContacts) { DEME_ERROR( - "You cannot mark persistent contacts when using a wildcard-less/history-less contact model (since " - "persistency is a part of the history).\nYou can use a different force model, and if you have to use this " - "one, add a placeholder wildcard."); + std::string("You must first enable persistent contact support by calling SetPersistentContact(true) before " + "marking persistent contacts.")); } // Get device-major info to host first - kT->previous_idGeometryA.toHost(); - kT->previous_idGeometryB.toHost(); - kT->previous_contactType.toHost(); - kT->contactPersistency.toHost(); - if (dT->solverFlags.canFamilyChangeOnDevice) { - dT->familyID.toHost(); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->previous_idPrimitiveA.toHost(); + kT->previous_idPrimitiveB.toHost(); + kT->previous_contactTypePrimitive.toHost(); + kT->contactPersistency.toHost(); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (dT->solverFlags.canFamilyChangeOnDevice) { + dT->familyID.toHost(); + } } // What we mark are actually the prev contact arrays. These arrays will be checked by kT and if a contact is marked // as persistent but not found in CD, it will be added to the contact array. - for (size_t i = 0; i < *(kT->solverScratchSpace.numPrevContacts); i++) { - bodyID_t bodyA = kT->previous_idGeometryA[i]; - bodyID_t bodyB = kT->previous_idGeometryB[i]; - contact_t c_type = kT->previous_contactType[i]; + for (size_t i = 0; i < *(kT->solverScratchSpace.numPrevPrimitiveContacts); i++) { + bodyID_t bodyA = kT->previous_idPrimitiveA[i]; + bodyID_t bodyB = kT->previous_idPrimitiveB[i]; + contact_t c_type = kT->previous_contactTypePrimitive[i]; - bodyID_t ownerA = dT->ownerClumpBody[bodyA]; // ownerClumpBody can't change on device - // As for B, it depends on type - bodyID_t ownerB = dT->getGeoOwnerID(bodyB, c_type); + bodyID_t ownerA = dT->getGeoOwnerID(bodyA, decodeTypeA(c_type)); + bodyID_t ownerB = dT->getGeoOwnerID(bodyB, decodeTypeB(c_type)); family_t famA = dT->familyID[ownerA]; family_t famB = dT->familyID[ownerB]; @@ -68,11 +75,10 @@ void DEMSolver::assignFamilyPersistentContact_impl( } } - if (is_or_not == CONTACT_IS_PERSISTENT) { - kT->solverFlags.hasPersistentContacts = true; - dT->solverFlags.hasPersistentContacts = true; + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->contactPersistency.toDevice(); } - kT->contactPersistency.toDevice(); } void DEMSolver::assignFamilyPersistentContactEither(unsigned int N, notStupidBool_t is_or_not) { @@ -95,24 +101,22 @@ void DEMSolver::assignFamilyPersistentContact(unsigned int N1, unsigned int N2, }); } void DEMSolver::assignPersistentContact(notStupidBool_t is_or_not) { - if (kT->solverFlags.isHistoryless) { + if (!kT->solverFlags.hasPersistentContacts) { DEME_ERROR( - "You cannot mark persistent contacts when using a wildcard-less/history-less contact model (since " - "persistency is a part of the history).\nYou can use a different force model, and if you have to use this " - "one, add a placeholder wildcard."); + std::string("You must first enable persistent contact support by calling SetPersistentContact(true) before " + "marking persistent contacts.")); } + ScopedCudaDevice device_scope(kT->streamInfo.device); kT->contactPersistency.toHost(); // What we mark are actually the prev contact arrays. These arrays will be checked by kT and if a contact is marked // as persistent but not found in CD, it will be added to the contact array. - for (size_t i = 0; i < *(kT->solverScratchSpace.numPrevContacts); i++) { + // Note: contactPersistency is a part of the primitive contact building process, so it is always existent and has + // correct size. + for (size_t i = 0; i < *(kT->solverScratchSpace.numPrevPrimitiveContacts); i++) { kT->contactPersistency[i] = is_or_not; } - if (is_or_not == CONTACT_IS_PERSISTENT) { - kT->solverFlags.hasPersistentContacts = true; - dT->solverFlags.hasPersistentContacts = true; - } kT->contactPersistency.toDevice(); } @@ -186,6 +190,10 @@ void DEMSolver::generateEntityResources() { // Count how many triangle tempaltes are there and flatten them preprocessTriangleObjs(); + + // Mesh-particle mass/MOI jitification is only useful while repeated template instances stay compact. If the compact + // table still grows too large, switch to flattened owner-level mass/MOI before kernel source is generated. + decideMeshMassJitification(); } void DEMSolver::postResourceGen() { @@ -197,9 +205,33 @@ void DEMSolver::postResourceGen() { postResourceGenChecksAndTabKeeping(); } +void DEMSolver::decideMeshMassJitification() { + if (!jitify_mass_moi) { + return; + } + if (m_mesh_mass_jit.size() <= THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES) { + return; + } + + DEME_WARNING( + "There are %zu distinct mesh mass/MOI template entries, but only %u are configured for mass/MOI jitification. " + "Falling back to flattened mass/MOI arrays.\nFor mesh-particle runs, use LoadMeshType/AddMeshFromTemplate so " + "many particles share template-level mass/MOI entries. If this many distinct mesh mass/MOI entries is " + "intentional, calling DisableJitifyMassProperties() before Initialize() will select the same flattened path " + "explicitly.", + m_mesh_mass_jit.size(), THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES); + jitify_mass_moi = false; +} + void DEMSolver::updateTotalEntityNum() { nDistinctClumpBodyTopologies = m_template_clump_mass.size(); - nDistinctMassProperties = nDistinctClumpBodyTopologies + nExtObj + nTriMeshes; + const size_t mesh_mass_entries = jitify_mass_moi ? m_mesh_mass_jit.size() : m_mesh_obj_mass.size(); + const size_t total_mass_entries = nDistinctClumpBodyTopologies + nExtObj + mesh_mass_entries; + if (total_mass_entries > std::numeric_limits::max()) { + DEME_ERROR("The number of mass/MOI entries (%zu) exceeds the supported unsigned int range.", + total_mass_entries); + } + nDistinctMassProperties = static_cast(total_mass_entries); // Also, external objects may introduce more material types nMatTuples = m_loaded_materials.size(); @@ -245,10 +277,38 @@ void DEMSolver::postResourceGenChecksAndTabKeeping() { "%u different mass properties (from the contribution of clump templates, analytical objects and meshed " "objects) are loaded, but the max allowance is %u (No.%u is reserved).\nYou may avoid this by calling " "DisableJitifyMassProperties() before system initialization to disable jitification for mass " - "properties", + "properties. If many mesh particles are present, prefer LoadMeshType/AddMeshFromTemplate so repeated " + "instances share mesh-template mass/MOI entries.", nDistinctMassProperties, std::numeric_limits::max() - 1, std::numeric_limits::max()); } + // Mass + MOI jitification emits one mass array and three MOI arrays into constant memory. Catch the practical + // limit here so mesh-particle users get an API-level fix suggestion instead of a late CUDA_ERROR_INVALID_PTX. + { + constexpr size_t kBytesPerMassEntry = 4 * sizeof(float); + int devices[] = {dT->streamInfo.device, kT->streamInfo.device}; + size_t min_const_mem = std::numeric_limits::max(); + for (int dev : devices) { + cudaDeviceProp prop{}; + if (cudaGetDeviceProperties(&prop, dev) == cudaSuccess) { + min_const_mem = std::min(min_const_mem, static_cast(prop.totalConstMem)); + } + } + if (min_const_mem != std::numeric_limits::max()) { + const size_t needed_bytes = kBytesPerMassEntry * nDistinctMassProperties; + constexpr size_t kSafetyMargin = 1024; + if (needed_bytes + kSafetyMargin > min_const_mem) { + DEME_ERROR( + "Mass/MOI jitification would require %zu bytes of constant memory for %u entries, exceeding " + "the available device constant memory (%zu bytes with a %zu-byte safety margin).\nIf many mesh " + "particles were added with AddMesh/AddWavefrontMeshObject, load one mesh template with " + "LoadMeshType and instantiate it with AddMeshFromTemplate so repeated particles share " + "template-level mass/MOI entries. If you intentionally need this many distinct mass/MOI " + "entries, call DisableJitifyMassProperties() before Initialize() to use flattened mass/MOI.", + needed_bytes, nDistinctMassProperties, min_const_mem, kSafetyMargin); + } + } + } } // Sanity check for analytical geometries @@ -289,6 +349,9 @@ void DEMSolver::addAnalCompTemplate(const objType_t type, m_anal_size_3.push_back(d3); float normal_sign = (normal == ENTITY_NORMAL_INWARD) ? 1 : -1; m_anal_normals.push_back(normal_sign); + // Unlike the flattened arrays, this compact definition persists after Initialize() so current owner transforms can + // be combined with the original component geometry for analytical VTK output. + m_anal_output_definitions.push_back({type, pos, rot, d1, d2, d3, normal_sign}); } void DEMSolver::jitifyKernels() { @@ -313,19 +376,27 @@ void DEMSolver::jitifyKernels() { DEME_GPU_CALL(cudaSetDevice(dT->streamInfo.device)); dT->jitifyKernels(m_subs, m_jitify_options); - // Now, inspectors need to be jitified too... but the current design jitify inspector kernels at the first time - // they are used. for (auto& insp : m_inspectors) { - // insp->Initialize(m_subs); - // } - // Solver system's own max vel inspector should be init-ed. Don't bother init-ing it while using, because it is // called at high frequency, let's save an if check. Forced initialization (since doing it before system // completes init). - m_approx_max_vel_func->Initialize(m_subs, m_jitify_options, true); - dT->approxMaxVelFunc = m_approx_max_vel_func; + m_approx_vel_func->Initialize(m_subs, m_jitify_options, true); + dT->approxVelFunc = m_approx_vel_func; + // Initialize angular velocity magnitude inspector + m_approx_angvel_func->Initialize(m_subs, m_jitify_options, true); + dT->approxAngVelFunc = m_approx_angvel_func; }); kT_build.join(); dT_build.join(); + + // Eagerly initialize user-created inspectors so their kernels compile before first use. + for (auto& insp : m_inspectors) { + if (insp) { + insp->Initialize(m_subs, m_jitify_options, true); + if (insp->inspection_kernel) { + insp->inspection_kernel->kernel(insp->kernel_name).instantiate(); + } + } + } } void DEMSolver::getContacts_impl(std::vector& idA, @@ -335,12 +406,13 @@ void DEMSolver::getContacts_impl(std::vector& idA, std::vector& famB, std::function type_func) const { // Get device-major info to host first + ScopedCudaDevice device_scope(dT->streamInfo.device); if (dT->solverFlags.canFamilyChangeOnDevice) { dT->familyID.toHostAsync(dT->streamInfo.stream); } - dT->idGeometryA.toHostAsync(dT->streamInfo.stream); - dT->idGeometryB.toHostAsync(dT->streamInfo.stream); - dT->contactType.toHostAsync(dT->streamInfo.stream); + dT->idPatchA.toHostAsync(dT->streamInfo.stream); + dT->idPatchB.toHostAsync(dT->streamInfo.stream); + dT->contactTypePatch.toHostAsync(dT->streamInfo.stream); size_t num_contacts = dT->getNumContacts(); idA.resize(num_contacts); @@ -353,10 +425,10 @@ void DEMSolver::getContacts_impl(std::vector& idA, size_t useful_contacts = 0; for (size_t i = 0; i < num_contacts; i++) { - contact_t this_type = dT->contactType[i]; + contact_t this_type = dT->contactTypePatch[i]; if (type_func(this_type)) { - idA[useful_contacts] = dT->getGeoOwnerID(dT->idGeometryA[i], this_type); - idB[useful_contacts] = dT->getGeoOwnerID(dT->idGeometryB[i], this_type); + idA[useful_contacts] = dT->getPatchOwnerID(dT->idPatchA[i], decodeTypeA(this_type)); + idB[useful_contacts] = dT->getPatchOwnerID(dT->idPatchB[i], decodeTypeB(this_type)); cnt_type[useful_contacts] = this_type; famA[useful_contacts] = dT->familyID[idA[useful_contacts]]; famB[useful_contacts] = dT->familyID[idB[useful_contacts]]; @@ -380,8 +452,8 @@ void DEMSolver::figureOutNV() { for (int i = 0; i < 3 - 1; i++) for (int j = i + 1; j < 3; j++) if (XYZ[i] > XYZ[j]) { - elemSwap(XYZ + i, XYZ + j); - elemSwap(rankXYZ + i, rankXYZ + j); + std::swap(XYZ[i], XYZ[j]); + std::swap(rankXYZ[i], rankXYZ[j]); } // Record the size ranking float userSize321[3] = {XYZ[0], XYZ[1], XYZ[2]}; @@ -516,6 +588,11 @@ void DEMSolver::decideBinSize() { } } + // m_binSize sanity check + m_binSize = clampBetween(m_binSize, 1000. * DEME_TINY_FLOAT, m_boxX); + m_binSize = clampBetween(m_binSize, 1000. * DEME_TINY_FLOAT, m_boxY); + m_binSize = clampBetween(m_binSize, 1000. * DEME_TINY_FLOAT, m_boxZ); + m_num_bins = hostCalcBinNum(nbX, nbY, nbZ, m_voxelSize, m_binSize, nvXp2, nvYp2, nvZp2); // It's better to compute num of bins this way, rather than... // (uint64_t)(m_boxX / m_binSize + 1) * (uint64_t)(m_boxY / m_binSize + 1) * (uint64_t)(m_boxZ / m_binSize + 1); @@ -571,7 +648,24 @@ void DEMSolver::decideCDMarginStrat() { break; case (MARGIN_FINDER_TYPE::DEFAULT): // Default strategy is to use an inspector - m_approx_max_vel_func = this->CreateInspector("absv"); + m_approx_vel_func = this->CreateInspector("absv"); + // Also create inspector for angular velocity magnitude + m_approx_angvel_func = this->CreateInspector("absangvel"); + // Validate every owner in the existing dT magnitude kernels, before any reduction or kT handoff. + // Unlike the primitive margin checks, these run with fixed margins and with angular margins disabled. + // Keep this policy on the internal inspectors so public inspection behavior remains unchanged. + m_approx_vel_func->inspection_code += R"V0G0N( + if (!isfinite(quantity[myOwner])) { + DEME_ABORT_KERNEL("Non-finite linear velocity (NaN or Inf) for ownerID %llu.\n", + static_cast(myOwner)); + } + )V0G0N"; + m_approx_angvel_func->inspection_code += R"V0G0N( + if (!isfinite(quantity[myOwner])) { + DEME_ABORT_KERNEL("Non-finite angular velocity (NaN or Inf) for ownerID %llu.\n", + static_cast(myOwner)); + } + )V0G0N"; m_max_v_finder_type = MARGIN_FINDER_TYPE::DEM_INSPECTOR; break; } @@ -580,13 +674,14 @@ void DEMSolver::decideCDMarginStrat() { void DEMSolver::reportInitStats() const { DEME_INFO("\n"); - DEME_INFO("Number of system devices detected: %d", GpuManager::scanNumDevices()); + DEME_INFO("Number of logical CUDA devices visible to DEME: %d", dTkT_GpuManager->getNumVisibleDevices()); DEME_INFO("Number of active devices used by DEME: %d", dTkT_GpuManager->getNumDevices()); + DEME_INFO("Worker device assignments (dT, kT): %d, %d", m_gpu_device_ids[0], m_gpu_device_ids[1]); DEME_INFO("User-specified X-dimension range: [%.7g, %.7g]", m_user_box_min.x, m_user_box_max.x); DEME_INFO("User-specified Y-dimension range: [%.7g, %.7g]", m_user_box_min.y, m_user_box_max.y); DEME_INFO("User-specified Z-dimension range: [%.7g, %.7g]", m_user_box_min.z, m_user_box_max.z); - DEME_INFO("User-specified dimensions should NOT be larger than the following simulation world."); + DEME_INFO(std::string("User-specified dimensions should NOT be larger than the following simulation world.")); DEME_INFO("The dimension of the simulation world: %.17g, %.17g, %.17g", m_boxX, m_boxY, m_boxZ); DEME_INFO("Simulation world X range: [%.7g, %.7g]", m_boxLBF.x, m_boxLBF.x + m_boxX); DEME_INFO("Simulation world Y range: [%.7g, %.7g]", m_boxLBF.y, m_boxLBF.y + m_boxY); @@ -608,16 +703,16 @@ void DEMSolver::reportInitStats() const { DEME_INFO("The number of material types: %u", nMatTuples); switch (m_force_model->type) { case (FORCE_MODEL::HERTZIAN): - DEME_INFO("History-based Hertzian contact model is in use."); + DEME_INFO(std::string("History-based Hertzian contact model is in use.")); break; case (FORCE_MODEL::HERTZIAN_FRICTIONLESS): - DEME_INFO("Frictionless Hertzian contact model is in use."); + DEME_INFO(std::string("Frictionless Hertzian contact model is in use.")); break; case (FORCE_MODEL::CUSTOM): - DEME_INFO("A user-custom force model is in use."); + DEME_INFO(std::string("A user-custom force model is in use.")); break; default: - DEME_INFO("An unknown force model is in use, this is probably not going well..."); + DEME_INFO(std::string("An unknown force model is in use, this is probably not going well...")); } if (use_user_defined_expand_factor) { @@ -628,20 +723,18 @@ void DEMSolver::reportInitStats() const { DEME_INFO("This in the case of the smallest sphere, means enlarging radius by %.6g%%.", (m_expand_factor / m_smallest_radius) * 100.0); } else { - DEME_INFO("The solver to set to adaptively change the contact margin size."); + DEME_INFO(std::string("The solver to set to adaptively change the contact margin size.")); float initFutureDrift = (m_suggestedFutureDrift < 0.) ? 10.0 : m_suggestedFutureDrift; float expand_factor = (m_expand_safety_multi * AN_EXAMPLE_MAX_VEL_FOR_SHOWING_MARGIN_SIZE + m_expand_base_vel) * initFutureDrift * m_ts_size; - DEME_STEP_METRIC( + DEME_DEBUG_PRINTF( "To give an example, all geometries may be enlarged/thickened by around %.6g (estimated with the initial " "step size, initial update frequency and velocity %.4g) for contact detection purpose.", expand_factor, AN_EXAMPLE_MAX_VEL_FOR_SHOWING_MARGIN_SIZE); - DEME_STEP_METRIC("This in the case of the smallest sphere, means enlarging radius by %.6g%%.", - (expand_factor / m_smallest_radius) * 100.0); + DEME_DEBUG_PRINTF("This in the case of the smallest sphere, means enlarging radius by %.6g%%.", + (expand_factor / m_smallest_radius) * 100.0); } - DEME_INFO("\n"); - // Debug outputs DEME_DEBUG_EXEC(printf("These owners are tracked: "); for (const auto& tracked @@ -654,7 +747,9 @@ void DEMSolver::reportInitStats() const { void DEMSolver::preprocessAnalyticalObjs() { // nExtObj can increase in mid-simulation if the user re-initialize using an `Add' flavor nExtObj += cached_extern_objs.size(); - unsigned int thisExtObj = 0; + unsigned int thisLoadExtObj = + 0; // In preprocessing, this starts from 0 since if this is an update, the previous ext objs are already loaded + // and processed. This is the offset for the new ones being added in this update. for (const auto& ext_obj : cached_extern_objs) { // Load mass and MOI properties into arrays waiting to be transfered to kTdT m_ext_obj_mass.push_back(ext_obj->mass); @@ -672,34 +767,34 @@ void DEMSolver::preprocessAnalyticalObjs() { this_num_anal_ent++; switch (ext_obj->types.at(i)) { case OBJ_COMPONENT::PLANE: - addAnalCompTemplate(ANAL_OBJ_TYPE_PLANE, comp_mat.at(i), thisExtObj, param.plane.position, + addAnalCompTemplate(ANAL_OBJ_TYPE_PLANE, comp_mat.at(i), thisLoadExtObj, param.plane.position, param.plane.normal); break; case OBJ_COMPONENT::PLATE: - addAnalCompTemplate(ANAL_OBJ_TYPE_PLATE, comp_mat.at(i), thisExtObj, param.plate.center, + addAnalCompTemplate(ANAL_OBJ_TYPE_PLATE, comp_mat.at(i), thisLoadExtObj, param.plate.center, param.plate.normal, param.plate.h_dim_x, param.plate.h_dim_y); break; case OBJ_COMPONENT::CYL_INF: - addAnalCompTemplate(ANAL_OBJ_TYPE_CYL_INF, comp_mat.at(i), thisExtObj, param.cyl.center, + addAnalCompTemplate(ANAL_OBJ_TYPE_CYL_INF, comp_mat.at(i), thisLoadExtObj, param.cyl.center, param.cyl.dir, param.cyl.radius, 0, 0, param.cyl.normal); break; case OBJ_COMPONENT::CONE_INF: - addAnalCompTemplate(ANAL_OBJ_TYPE_CONE_INF, comp_mat.at(i), thisExtObj, param.cone.cone_tip, + addAnalCompTemplate(ANAL_OBJ_TYPE_CONE_INF, comp_mat.at(i), thisLoadExtObj, param.cone.cone_tip, param.cone.dir, param.cone.slope, param.cone.hmin, param.cone.hmax, param.cone.normal); break; case OBJ_COMPONENT::CONE: - addAnalCompTemplate(ANAL_OBJ_TYPE_CONE, comp_mat.at(i), thisExtObj, param.cone.cone_tip, + addAnalCompTemplate(ANAL_OBJ_TYPE_CONE, comp_mat.at(i), thisLoadExtObj, param.cone.cone_tip, param.cone.dir, param.cone.slope, param.cone.hmin, param.cone.hmax, param.cone.normal); break; default: - DEME_ERROR("There is at least one analytical boundary that has a type not supported."); + DEME_ERROR(std::string("There is at least one analytical boundary that has a type not supported.")); } } nAnalGM += this_num_anal_ent; m_ext_obj_comp_num.push_back(this_num_anal_ent); - thisExtObj++; + thisLoadExtObj++; } } @@ -765,12 +860,18 @@ void DEMSolver::preprocessClumps() { void DEMSolver::preprocessTriangleObjs() { nTriMeshes += cached_mesh_objs.size(); - unsigned int thisMeshObj = 0; + std::map mesh_mass_map; + bodyID_t thisMeshObj = + 0; // In preprocessing, this starts from 0 since if this is an update, the previous mesh objects are already + // loaded and processed. This is the offset for the new ones being added in this update. + bodyID_t thisLoadPatchCount = + 0; // In preprocessing, this starts from 0 since if this is an update, the previous patches are already loaded + // and processed. This is the offset for the new ones being added in this update. for (const auto& mesh_obj : cached_mesh_objs) { if (!(mesh_obj->isMaterialSet)) { - DEME_ERROR( + DEME_ERROR(std::string( "A meshed object is loaded but does not have associated material.\nPlease assign material to meshes " - "via SetMaterial."); + "via SetMaterial.")); } // Put the mesh into the host-side cache m_meshes.push_back(mesh_obj); @@ -785,13 +886,66 @@ void DEMSolver::preprocessTriangleObjs() { } m_mesh_obj_mass.push_back(mesh_obj->mass); m_mesh_obj_moi.push_back(mesh_obj->MOI); + const MeshMassJitKey mass_moi_key{mesh_obj->mesh_template_mark, mesh_obj->mass, mesh_obj->MOI.x, + mesh_obj->MOI.y, mesh_obj->MOI.z}; + auto it = mesh_mass_map.find(mass_moi_key); + if (it != mesh_mass_map.end()) { + m_mesh_mass_offsets.push_back(it->second); + } else if (m_mesh_mass_jit.size() >= THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES) { + if (m_mesh_mass_jit.size() == THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES) { + m_mesh_mass_jit.push_back(mesh_obj->mass); + m_mesh_moi_jit.push_back(mesh_obj->MOI); + } + // Offsets are ignored once decideMeshMassJitification switches to flattened mass/MOI. + m_mesh_mass_offsets.push_back(0); + } else { + const inertiaOffset_t offset = static_cast(mesh_mass_map.size()); + mesh_mass_map.emplace(mass_moi_key, offset); + m_mesh_mass_jit.push_back(mesh_obj->mass); + m_mesh_moi_jit.push_back(mesh_obj->MOI); + m_mesh_mass_offsets.push_back(offset); + } m_input_mesh_obj_xyz.push_back(mesh_obj->init_pos); m_input_mesh_obj_rot.push_back(mesh_obj->init_oriQ); m_input_mesh_obj_family.push_back(mesh_obj->family_code); + m_input_mesh_obj_convex.push_back(mesh_obj->IsConvex() ? 1 : 0); + m_input_mesh_obj_never_winner.push_back(mesh_obj->IsNeverWinner() ? 1 : 0); m_mesh_facet_owner.insert(m_mesh_facet_owner.end(), mesh_obj->GetNumTriangles(), thisMeshObj); + + const bodyID_t tri_offset = static_cast(m_mesh_facets.size()); + std::vector> local_neighbors; + if (mesh_obj->IsConvex() && mesh_obj->IsNeverWinner()) { + local_neighbors.assign(mesh_obj->GetNumTriangles(), {NULL_BODYID, NULL_BODYID, NULL_BODYID}); + } else { + local_neighbors = buildTriangleEdgeNeighbors(mesh_obj->m_face_v_indices, mesh_obj->m_vertices); + } + + // Initialize patch IDs if not already set (default: all facets in patch 0) + if (!mesh_obj->patches_explicitly_set && mesh_obj->m_patch_ids.empty()) { + mesh_obj->SetPatchIDs(std::vector(mesh_obj->GetNumTriangles(), 0)); + } + + // Populate patch owner and material arrays (one entry per patch in this mesh) + // Note patch_id in a mesh is always 0-based, and contiguous + std::vector patch_materials(mesh_obj->GetNumPatches()); + for (size_t facet_idx = 0; facet_idx < mesh_obj->GetNumPatches(); facet_idx++) { + // patch_id is per-triangle + bodyID_t patch_id = mesh_obj->m_patch_ids.at(facet_idx); + // Assign this facet's material to its patch (will overwrite for each facet, but they should be consistent + // per patch) + patch_materials[patch_id] = mesh_obj->materials.at(patch_id)->load_order; + } + + for (size_t patch_idx = 0; patch_idx < mesh_obj->GetNumPatches(); patch_idx++) { + m_mesh_patch_owner.push_back(thisMeshObj); + m_mesh_patch_materials.push_back(patch_materials[patch_idx]); + } + for (unsigned int i = 0; i < mesh_obj->GetNumTriangles(); i++) { - m_mesh_facet_materials.push_back(mesh_obj->materials.at(i)->load_order); + // Store which patch this facet belongs to + m_mesh_facet_patch.push_back(mesh_obj->m_patch_ids.at(i) + thisLoadPatchCount); + DEMTriangle tri = mesh_obj->GetTriangle(i); // If we wish to correct surface orientation based on given vertex normals, rather than using RHR... if (mesh_obj->use_mesh_normals) { @@ -811,11 +965,21 @@ void DEMSolver::preprocessTriangleObjs() { } } m_mesh_facets.push_back(tri); + + const auto& nb = local_neighbors[i]; + m_mesh_facet_neighbor1.push_back(nb[0] == NULL_BODYID ? NULL_BODYID : nb[0] + tri_offset); + m_mesh_facet_neighbor2.push_back(nb[1] == NULL_BODYID ? NULL_BODYID : nb[1] + tri_offset); + m_mesh_facet_neighbor3.push_back(nb[2] == NULL_BODYID ? NULL_BODYID : nb[2] + tri_offset); } + thisLoadPatchCount += mesh_obj->GetNumPatches(); nTriGM += mesh_obj->GetNumTriangles(); + nMeshPatches += mesh_obj->GetNumPatches(); // This is used to keep track of the total number of patches across + // all meshes, potentially multiple mesh loads thisMeshObj++; } + DEME_DEBUG_PRINTF("Total number of mesh patches after this update: %zu", nMeshPatches); + DEME_DEBUG_PRINTF("Total number of mesh triangles after this update: %zu", nTriGM); } void DEMSolver::figureOutMaterialProxies() { @@ -1029,24 +1193,23 @@ void DEMSolver::setSolverParams() { kT->verbosity = verbosity; dT->verbosity = verbosity; - // Whether there are meshes in the simulation - kT->solverFlags.hasMeshes = (nTriObjLoad > 0); - dT->solverFlags.hasMeshes = (nTriObjLoad > 0); - // I/O policies (only output content, not file format, matters for worker threads) auto output_level = m_out_content; if (m_is_out_owner_wildcards) { output_level = output_level | OUTPUT_CONTENT::OWNER_WILDCARD; } - if (m_is_out_geo_wildcards) { - output_level = output_level | OUTPUT_CONTENT::GEO_WILDCARD; - } dT->solverFlags.outputFlags = output_level; + dT->solverFlags.meshOutFlags = m_mesh_out_content; output_level = m_cnt_out_content; if (m_is_out_cnt_wildcards) { output_level = output_level | CNT_OUTPUT_CONTENT::CNT_WILDCARD; } dT->solverFlags.cntOutFlags = output_level; + // Contact normals are a contact-output field, so the internal storage flag follows the output content instead of a + // separate user-facing toggle. This keeps the write-back kernel enabled exactly when normal output can read it. + const bool store_contact_normals = (output_level & CNT_OUTPUT_CONTENT::NORMAL) != 0; + kT->simParams->storeNormal = store_contact_normals; + dT->simParams->storeNormal = store_contact_normals; // Transfer historyless-ness kT->solverFlags.isHistoryless = (m_force_model->m_contact_wildcards.size() == 0); @@ -1084,25 +1247,26 @@ void DEMSolver::setSolverParams() { dT->solverFlags.canFamilyChangeOnDevice = famnum_can_change_conditionally; // Force reduction strategy - kT->solverFlags.useCubForceCollect = use_cub_to_reduce_force; - dT->solverFlags.useCubForceCollect = use_cub_to_reduce_force; dT->solverFlags.useNoContactRecord = no_recording_contact_forces; dT->solverFlags.useForceCollectInPlace = collect_force_in_force_kernel; - // Whether sorts contact before using them (not implemented) - kT->solverFlags.should_sort_pairs = should_sort_contacts; - dT->solverFlags.should_sort_pairs = should_sort_contacts; + // Whether sorts contact before using them. + // NOTE: The current logic requires this to be true. We cannot set it to false now. + // kT->solverFlags.should_sort_pairs = should_sort_contacts; + // dT->solverFlags.should_sort_pairs = should_sort_contacts; // Error out policies - kT->solverFlags.errOutAvgSphCnts = threshold_error_out_num_cnts; - dT->solverFlags.errOutAvgSphCnts = threshold_error_out_num_cnts; + kT->solverFlags.errOutAvgPrimitiveCnts = threshold_error_out_num_cnts; + dT->solverFlags.errOutAvgPrimitiveCnts = threshold_error_out_num_cnts; // simParams-stored variables need to be sync-ed to device kT->simParams->errOutBinSphNum = threshold_too_many_spheres_in_bin; dT->simParams->errOutBinSphNum = threshold_too_many_spheres_in_bin; kT->simParams->errOutBinTriNum = threshold_too_many_tri_in_bin; dT->simParams->errOutBinTriNum = threshold_too_many_tri_in_bin; kT->simParams->errOutVel = threshold_error_out_vel; + kT->simParams->errOutAngVel = threshold_error_out_angvel; dT->simParams->errOutVel = threshold_error_out_vel; + dT->simParams->errOutAngVel = threshold_error_out_angvel; // Whether the solver should auto-update bin sizes kT->solverFlags.autoBinSize = auto_adjust_bin_size; @@ -1125,7 +1289,7 @@ void DEMSolver::setSolverParams() { dT->solverFlags.upperBoundFutureDrift = upper_bound_future_drift; dT->solverFlags.targetDriftMoreThanAvg = max_drift_ahead_of_avg_drift; dT->solverFlags.targetDriftMultipleOfAvg = max_drift_multiple_of_avg_drift; - dT->accumStepUpdater.SetCacheSize(max_drift_gauge_history_size); + dT->futureDriftRegulator.SetCacheSize(max_drift_gauge_history_size); } void DEMSolver::setSimParams() { @@ -1149,11 +1313,9 @@ void DEMSolver::setSimParams() { // Compute the number of wildcards in our force model unsigned int nContactWildcards = m_force_model->m_contact_wildcards.size(); unsigned int nOwnerWildcards = m_force_model->m_owner_wildcards.size(); - unsigned int nGeoWildcards = m_force_model->m_geo_wildcards.size(); - if (nContactWildcards > DEME_MAX_WILDCARD_NUM || nOwnerWildcards > DEME_MAX_WILDCARD_NUM || - nGeoWildcards > DEME_MAX_WILDCARD_NUM) { + if (nContactWildcards > DEME_MAX_WILDCARD_NUM || nOwnerWildcards > DEME_MAX_WILDCARD_NUM) { DEME_ERROR( - "You defined too many contact/owner/geometry wildcards! Currently the max amount is %u for each of " + "You defined too many contact/owner wildcards! Currently the max amount is %u for each of " "them.\nYou can change constant DEME_MAX_WILDCARD_NUM and re-compile, if you indeed would like more " "wildcards.", DEME_MAX_WILDCARD_NUM); @@ -1166,29 +1328,123 @@ void DEMSolver::setSimParams() { m_approx_max_vel = threshold_error_out_vel; } + // Beta adaptive timestep mode: compute one fixed Hertzian timestep at setup time. The other adaptive modes are + // accepted by the public API for compatibility but do not have behavior yet. + if (adapt_ts_type == ADAPT_TS_TYPE::HERTZ_CONST) { + auto sqr = [](double x) { return x * x; }; + auto effective_E = [&](double E1, double nu1, double E2, double nu2) -> double { + if (E1 <= 0.0 || E2 <= 0.0) { + return 0.0; + } + return 1.0 / (((1.0 - sqr(nu1)) / E1) + ((1.0 - sqr(nu2)) / E2)); + }; + + double max_eff_E = 0.0; + for (size_t i = 0; i < m_loaded_materials.size(); ++i) { + const auto& mat_a = m_loaded_materials[i]->mat_prop; + const double E1 = mat_a.count("E") ? static_cast(mat_a.at("E")) : 0.0; + const double nu1 = mat_a.count("nu") ? static_cast(mat_a.at("nu")) : 0.3; + for (size_t j = i; j < m_loaded_materials.size(); ++j) { + const auto& mat_b = m_loaded_materials[j]->mat_prop; + const double E2 = mat_b.count("E") ? static_cast(mat_b.at("E")) : 0.0; + const double nu2 = mat_b.count("nu") ? static_cast(mat_b.at("nu")) : 0.3; + max_eff_E = std::max(max_eff_E, effective_E(E1, nu1, E2, nu2)); + } + } + + double min_mass = std::numeric_limits::infinity(); + for (double mass : m_template_clump_mass) { + if (mass > 0.0 && mass < min_mass) { + min_mass = mass; + } + } + + const double min_radius = static_cast(m_smallest_radius); + if (std::isfinite(min_mass) && min_radius > 0.0 && max_eff_E > 0.0) { + const double effective_radius = 0.5 * min_radius; + const double effective_mass = 0.5 * min_mass; + const double hertz_stiffness = FOUR_OVER_THREE * std::sqrt(0.1) * max_eff_E * effective_radius; + const double hertz_dt = (PI / (2.0 * N_DT)) * std::sqrt(effective_mass / hertz_stiffness); + if (hertz_dt > 0.0 && std::isfinite(hertz_dt)) { + m_ts_size = hertz_dt; + DEME_INFO( + "Adaptive timestep hertz_const selected dt %.9g from min mass %.6g, min radius %.6g, max " + "effective E %.6g, and N_DT %.1f.", + m_ts_size, min_mass, min_radius, max_eff_E, N_DT); + } else { + DEME_WARNING("Adaptive timestep hertz_const produced invalid dt; keeping existing timestep %.9g.", + m_ts_size); + } + } else { + DEME_WARNING( + "Adaptive timestep hertz_const could not compute dt because setup data is incomplete or invalid " + "(min mass %.6g, min radius %.6g, max effective E %.6g); keeping existing timestep %.9g.", + min_mass, min_radius, max_eff_E, m_ts_size); + } + } + + if (!m_use_angvel_margin_user_set) { + bool has_multi_sphere_clump = false; + for (const auto& radii : m_template_sp_radii) { + if (radii.size() > 1) { + has_multi_sphere_clump = true; + break; + } + } + m_use_angvel_margin = has_multi_sphere_clump || nTriGM > 0; + } + + // Geometry wildcards historically had no output consumer. Allocate the mesh-provided names when explicitly + // requested so VTK CELL_DATA can preserve their per-triangle values. + std::set mesh_output_geo_wildcards; + if (m_mesh_out_content & static_cast(MESH_OUTPUT_CONTENT::GEO_WILDCARD)) { + // Cached meshes are cleared after initialization, so retain the worker's established names on later + // UpdateSimParams calls and merge names from any setup-time mesh cache. + if (sys_initialized) { + mesh_output_geo_wildcards = dT->m_geo_wildcard_names; + } + for (const auto& mesh : cached_mesh_objs) { + for (const auto& wildcard : mesh->geo_wildcards) { + mesh_output_geo_wildcards.insert(wildcard.first); + } + } + } dT->setSimParams(nvXp2, nvYp2, nvZp2, l, m_voxelSize, m_binSize, nbX, nbY, nbZ, m_boxLBF, m_user_box_min, - m_user_box_max, G, m_ts_size, m_expand_factor, m_approx_max_vel, m_expand_safety_multi, - m_expand_base_vel, m_force_model->m_contact_wildcards, m_force_model->m_owner_wildcards, - m_force_model->m_geo_wildcards); + m_user_box_max, G, m_ts_size, m_expand_factor, m_approx_max_vel, m_max_tritri_penetration, + m_triTriContactRejectionRatio, m_expand_safety_multi, m_expand_base_vel, m_use_angvel_margin, + m_force_model->m_contact_wildcards, m_force_model->m_owner_wildcards, mesh_output_geo_wildcards); kT->setSimParams(nvXp2, nvYp2, nvZp2, l, m_voxelSize, m_binSize, nbX, nbY, nbZ, m_boxLBF, m_user_box_min, - m_user_box_max, G, m_ts_size, m_expand_factor, m_approx_max_vel, m_expand_safety_multi, - m_expand_base_vel, m_force_model->m_contact_wildcards, m_force_model->m_owner_wildcards, - m_force_model->m_geo_wildcards); + m_user_box_max, G, m_ts_size, m_expand_factor, m_approx_max_vel, m_max_tritri_penetration, + m_triTriContactRejectionRatio, m_expand_safety_multi, m_expand_base_vel, m_use_angvel_margin, + m_force_model->m_contact_wildcards, m_force_model->m_owner_wildcards, mesh_output_geo_wildcards); } void DEMSolver::allocateGPUArrays() { + size_t tri_neighbors = 0; + for (const auto& mesh_obj : cached_mesh_objs) { + if (!mesh_obj) { + continue; + } + if (!(mesh_obj->IsConvex() && mesh_obj->IsNeverWinner())) { + tri_neighbors += mesh_obj->GetNumTriangles(); + } + } + nTriNeighbors = tri_neighbors; + // Resize arrays based on the statistical data we have std::thread dThread = std::move(std::thread([this]() { this->dT->allocateGPUArrays(this->nOwnerBodies, this->nOwnerClumps, this->nExtObj, this->nTriMeshes, - this->nSpheresGM, this->nTriGM, this->nAnalGM, this->nExtraContacts, - this->nDistinctMassProperties, this->nDistinctClumpBodyTopologies, - this->nDistinctClumpComponents, this->nJitifiableClumpComponents, this->nMatTuples); + this->nSpheresGM, this->nTriGM, this->nTriNeighbors, this->nMeshPatches, + this->nAnalGM, this->nExtraContacts, this->nDistinctMassProperties, + this->nDistinctClumpBodyTopologies, this->nDistinctClumpComponents, + this->nJitifiableClumpComponents, this->nMatTuples); })); std::thread kThread = std::move(std::thread([this]() { this->kT->allocateGPUArrays(this->nOwnerBodies, this->nOwnerClumps, this->nExtObj, this->nTriMeshes, - this->nSpheresGM, this->nTriGM, this->nAnalGM, this->nExtraContacts, - this->nDistinctMassProperties, this->nDistinctClumpBodyTopologies, - this->nDistinctClumpComponents, this->nJitifiableClumpComponents, this->nMatTuples); + this->nSpheresGM, this->nTriGM, this->nTriNeighbors, this->nAnalGM, + this->nExtraContacts, this->nDistinctMassProperties, + this->nDistinctClumpBodyTopologies, this->nDistinctClumpComponents, + this->nJitifiableClumpComponents, this->nMatTuples); })); dThread.join(); kThread.join(); @@ -1200,40 +1456,51 @@ void DEMSolver::initializeGPUArrays() { m_template_sp_radii, m_template_sp_relPos, m_template_clump_volume); // Now we can feed those GPU-side arrays with the cached API-level simulation info - dT->initGPUArrays( - // Clump batchs' initial stats - cached_input_clump_batches, - // Analytical objects' initial stats - m_input_ext_obj_xyz, m_input_ext_obj_rot, m_input_ext_obj_family, - // Meshed objects' initial stats - cached_mesh_objs, m_input_mesh_obj_xyz, m_input_mesh_obj_rot, m_input_mesh_obj_family, m_mesh_facet_owner, - m_mesh_facet_materials, m_mesh_facets, - // Clump template name mapping - m_template_number_name_map, - // Clump template info (mass, sphere components, materials etc.) - flattened_clump_templates, - // Analytical obj `template' properties - m_ext_obj_mass, m_ext_obj_moi, m_ext_obj_comp_num, - // Meshed obj `template' properties - m_mesh_obj_mass, m_mesh_obj_moi, - // Universal template info - m_loaded_materials, - // Family mask - m_family_mask_matrix, - // I/O and misc. - m_no_output_families, m_tracked_objs); - - kT->initGPUArrays( - // Clump batchs' initial stats - cached_input_clump_batches, - // Analytical objects' initial stats - m_input_ext_obj_family, - // Meshed objects' initial stats - m_input_mesh_obj_family, m_mesh_facet_owner, m_mesh_facets, - // Family mask - m_family_mask_matrix, - // Templates and misc. - flattened_clump_templates); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->initGPUArrays( + // Clump batchs' initial stats + cached_input_clump_batches, + // Analytical objects' initial stats + m_input_ext_obj_xyz, m_input_ext_obj_rot, m_input_ext_obj_family, + // Meshed objects' initial stats + cached_mesh_objs, m_input_mesh_obj_xyz, m_input_mesh_obj_rot, m_input_mesh_obj_family, + m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, m_mesh_facet_patch, + m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, m_mesh_patch_owner, + m_mesh_patch_materials, + // Clump template name mapping + m_template_number_name_map, + // Clump template info (mass, sphere components, materials etc.) + flattened_clump_templates, + // Analytical obj physics properties + m_ext_obj_mass, m_ext_obj_moi, m_ext_obj_comp_num, + // Meshed obj physics properties + m_mesh_obj_mass, m_mesh_obj_moi, m_mesh_mass_jit, m_mesh_moi_jit, m_mesh_mass_offsets, + // Universal template info + m_loaded_materials, + // Family mask + m_family_mask_matrix, + // I/O and misc. + m_no_output_families, m_tracked_objs); + } + + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->initGPUArrays( + // Clump batchs' initial stats + cached_input_clump_batches, + // Analytical objects' initial stats + m_input_ext_obj_family, + // Meshed objects' initial stats + m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, + m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, + // Analytical obj physics properties + m_ext_obj_comp_num, + // Family mask + m_family_mask_matrix, + // Templates and misc. + flattened_clump_templates); + } } /// When more clumps/meshed objects got loaded, this method should be called to transfer them to the GPU-side in @@ -1244,56 +1511,81 @@ void DEMSolver::updateClumpMeshArrays(size_t nOwners, size_t nSpheres, size_t nTriMesh, size_t nFacets, + size_t nTriNeighbors, + size_t nMeshPatches, unsigned int nExtObj, unsigned int nAnalGM) { // Pack clump templates together... that's easier to pass to dT kT ClumpTemplateFlatten flattened_clump_templates(m_template_clump_mass, m_template_clump_moi, m_template_sp_mat_ids, m_template_sp_radii, m_template_sp_relPos, m_template_clump_volume); - dT->updateClumpMeshArrays( - // Clump batchs' initial stats - cached_input_clump_batches, - // Analytical objects' initial stats - m_input_ext_obj_xyz, m_input_ext_obj_rot, m_input_ext_obj_family, - // Meshed objects' initial stats - cached_mesh_objs, m_input_mesh_obj_xyz, m_input_mesh_obj_rot, m_input_mesh_obj_family, m_mesh_facet_owner, - m_mesh_facet_materials, m_mesh_facets, - // Clump template info (mass, sphere components, materials etc.) - flattened_clump_templates, - // Analytical obj `template' properties - m_ext_obj_mass, m_ext_obj_moi, m_ext_obj_comp_num, - // Meshed obj `template' properties - m_mesh_obj_mass, m_mesh_obj_moi, - // Universal template info - m_loaded_materials, - // Family mask - m_family_mask_matrix, - // I/O and misc. - m_no_output_families, m_tracked_objs, - // Number of entities, old - nOwners, nClumps, nSpheres, nTriMesh, nFacets, nExtObj, nAnalGM); - kT->updateClumpMeshArrays( - // Clump batchs' initial stats - cached_input_clump_batches, - // Analytical objects' initial stats - m_input_ext_obj_family, - // Meshed objects' initial stats - m_input_mesh_obj_family, m_mesh_facet_owner, m_mesh_facets, - // Family mask - m_family_mask_matrix, - // Templates and misc. - flattened_clump_templates, - // Number of entities, old - nOwners, nClumps, nSpheres, nTriMesh, nFacets, nExtObj, nAnalGM); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->updateClumpMeshArrays( + // Clump batchs' initial stats + cached_input_clump_batches, + // Analytical objects' initial stats + m_input_ext_obj_xyz, m_input_ext_obj_rot, m_input_ext_obj_family, + // Meshed objects' initial stats + cached_mesh_objs, m_input_mesh_obj_xyz, m_input_mesh_obj_rot, m_input_mesh_obj_family, + m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, m_mesh_facet_patch, + m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, m_mesh_patch_owner, + m_mesh_patch_materials, + // Clump template info (mass, sphere components, materials etc.) + flattened_clump_templates, + // Analytical obj physics properties + m_ext_obj_mass, m_ext_obj_moi, m_ext_obj_comp_num, + // Meshed obj physics properties + m_mesh_obj_mass, m_mesh_obj_moi, m_mesh_mass_jit, m_mesh_moi_jit, m_mesh_mass_offsets, + // Universal template info + m_loaded_materials, + // Family mask + m_family_mask_matrix, + // I/O and misc. + m_no_output_families, m_tracked_objs, + // Number of entities, old + nOwners, nClumps, nSpheres, nTriMesh, nFacets, nTriNeighbors, nMeshPatches, nExtObj, nAnalGM); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->updateClumpMeshArrays( + // Clump batchs' initial stats + cached_input_clump_batches, + // Analytical objects' initial stats + m_input_ext_obj_family, + // Meshed objects' initial stats + m_input_mesh_obj_family, m_input_mesh_obj_convex, m_input_mesh_obj_never_winner, m_mesh_facet_owner, + m_mesh_facet_patch, m_mesh_facet_neighbor1, m_mesh_facet_neighbor2, m_mesh_facet_neighbor3, m_mesh_facets, + // Analytical obj physics properties + m_ext_obj_comp_num, + // Family mask + m_family_mask_matrix, + // Templates and misc. + flattened_clump_templates, + // Number of entities, old + nOwners, nClumps, nSpheres, nTriMesh, nFacets, nTriNeighbors, nMeshPatches, nExtObj, nAnalGM); + } } void DEMSolver::packDataPointers() { - dT->packDataPointers(); - kT->packDataPointers(); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->packDataPointers(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->packDataPointers(); + } // Each worker thread needs pointers used for data transfering. Note this step must be done after packDataPointers // are called, so each thread has its own pointers packed. - dT->packTransferPointers(kT.get()); - kT->packTransferPointers(dT.get()); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->packTransferPointers(kT.get()); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->packTransferPointers(dT.get()); + } // Finally, the API needs to map all mesh to their owners for (const auto& mmesh : m_meshes) { m_owner_mesh_map[mmesh->owner] = mmesh->cache_offset; @@ -1301,21 +1593,38 @@ void DEMSolver::packDataPointers() { } void DEMSolver::migrateSimParamsToDevice() { - dT->simParams.toDevice(); - kT->simParams.toDevice(); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams.toDevice(); + } } void DEMSolver::migrateArrayDataToDevice() { - dT->granData.toDevice(); - kT->granData.toDevice(); - // Then move DualArray data to device - dT->migrateDataToDevice(); - kT->migrateDataToDevice(); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->granData.toDevice(); + kT->migrateDataToDevice(); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->granData.toDevice(); + dT->migrateDataToDevice(); + } } void DEMSolver::migrateArrayDataToHost() { - dT->migrateDeviceModifiableInfoToHost(); - kT->migrateDeviceModifiableInfoToHost(); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->migrateDeviceModifiableInfoToHost(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->migrateDeviceModifiableInfoToHost(); + } } void DEMSolver::validateUserInputs() { @@ -1325,21 +1634,21 @@ void DEMSolver::validateUserInputs() { // LoadClumpType."); // } - // // If not 2 GPUs detected, output warnings as needed - // int ndevices = dTkT_GpuManager->getNumDevices(); - // if (ndevices == 0) { - // DEME_ERROR( - // "No GPU device is detected. Try lspci and see what you get.\nIf you indeed have GPU devices, maybe you " - // "should try rebooting or reinstalling cuda components?"); - // // } else if (ndevices == 1) { - // // DEME_WARNING( - // // "One GPU device is detected. On consumer cards, DEME's performance edge is limited with only one" - // // "GPU.\nTry allocating 2 GPU devices if possible."); - // } else if (ndevices > 2) { - // DEME_WARNING( - // "More than two GPU devices are detected.\nCurrently, DEME can make use of at most two devices.\nMore " - // "devices will not improve the performance."); - // } + // If not 2 GPUs detected, output warnings as needed + int ndevices = dTkT_GpuManager->getNumVisibleDevices(); + if (ndevices == 0) { + DEME_ERROR(std::string( + "No GPU device is detected. Try lspci and see what you get.\nIf you indeed have GPU devices, maybe you " + "should try rebooting or reinstalling cuda components?")); + // } else if (ndevices == 1) { + // DEME_WARNING( + // "One GPU device is detected. On consumer cards, DEME's performance edge is limited with only one" + // "GPU.\nTry allocating 2 GPU devices if possible."); + } else if (ndevices > 2) { + DEME_WARNING(std::string( + "More than two GPU devices are detected.\nCurrently, DEME can make use of at most two devices.\nMore " + "devices will not improve the performance.")); + } // Box size OK? float3 user_box_size = m_user_box_max - m_user_box_min; @@ -1350,28 +1659,17 @@ void DEMSolver::validateUserInputs() { } if (m_suggestedFutureDrift < 0) { - DEME_WARNING( + DEME_WARNING(std::string( "The physics of the DEM system can drift into the future as much as it wants compared to contact " "detections, because SetCDUpdateFreq was called with a negative argument.\nThere is also no guarantee on " "the contact margin size to be added, other than it will be no less than 0.\nPlease make sure this is " - "intended."); + "intended.")); } // Fix the reserved family (reserved family number is in user family, not in impl family) SetFamilyFixed(RESERVED_FAMILY_NUM); } -bool DEMSolver::goThroughWorkerAnomalies() { - bool there_is = false; - if (kT->anomalies.over_max_vel || dT->anomalies.over_max_vel) { - DEME_PRINTF( - "Workers reported there are simulation entities reached user-specified maximum velocity.\nDetails can be " - "shown by re-running with \"STEP_ANOMALY\" verbosity level.\n"); - there_is = true; - } - return there_is; -} - // inline unsigned int stash_material_in_templates(std::vector>& loaded_materials, // const std::shared_ptr& this_material) { // auto is_same = [&](const std::shared_ptr& ptr) { return is_material_same(ptr, this_material); }; @@ -1391,33 +1689,21 @@ bool DEMSolver::goThroughWorkerAnomalies() { inline void DEMSolver::equipForceModel(std::unordered_map& strMap) { // Empty ingr list auto added_ingredients = force_kernel_ingredient_stats; - //// TODO: Reassemble geo and owner wildcards here again in a set is not needed... Since set is ordered. - std::set added_owner_wildcards, added_geo_wildcards; + std::set added_owner_wildcards; // Analyze this model... what does it require? std::string model = m_force_model->m_force_model; std::string model_prerequisites = m_force_model->m_model_prerequisites; const std::set contact_wildcard_names = m_force_model->m_contact_wildcards; const std::set owner_wildcard_names = m_force_model->m_owner_wildcards; - const std::set geo_wildcard_names = m_force_model->m_geo_wildcards; - std::set geo_wildcard_names_error_checking; - // geo_wildcard_names needs some treatments: Add _A and _B to them for error checking... - if (geo_wildcard_names.size() > 0) { - for (const std::string& wc_name : geo_wildcard_names) { - geo_wildcard_names_error_checking.insert(wc_name + "_A"); - geo_wildcard_names_error_checking.insert(wc_name + "_B"); - } - } // Then clear the wc numbering registering array m_owner_wc_num.clear(); - m_geo_wc_num.clear(); m_cnt_wc_num.clear(); // If we spot that the force model requires an ingredient, we make sure that order goes to the ingredient // acquisition module std::string ingredient_definition = " ", cnt_wildcard_acquisition = " ", ingredient_acquisition_A = " ", ingredient_acquisition_B = " ", owner_geo_wildcard_write_back = " ", cnt_wildcard_write_back = " ", - cnt_wildcard_destroy_record = " ", geo_wc_acquisition_B_sph = " ", geo_wc_acquisition_B_tri = " ", - geo_wc_acquisition_B_anal = " "; + cnt_wildcard_destroy_record = " "; scan_force_model_ingr(added_ingredients, model); // As our numerical method stands now, AOwnerFamily and BOwnerFamily are always needed. add_force_model_ingr(added_ingredients, "AOwnerFamily"); @@ -1429,9 +1715,9 @@ inline void DEMSolver::equipForceModel(std::unordered_map 0) { - add_force_model_ingr(added_ingredients, "AGeo"); - add_force_model_ingr(added_ingredients, "BGeo"); - } - // Equip those acquisition strategies that need to be there equip_force_model_ingr_acq(ingredient_definition, ingredient_acquisition_A, ingredient_acquisition_B, added_ingredients); // Then equip acquisition strategies for owner wildcards equip_owner_wildcards(ingredient_definition, ingredient_acquisition_A, ingredient_acquisition_B, owner_geo_wildcard_write_back, added_owner_wildcards); - // Then equip acquisition strategies for geo wildcards. - // geo_wc_acquisition_B_sph, geo_wc_acquisition_B_tri, geo_wc_acquisition_B_anal cannot be incorporated into - // ingredient_acquisition_B, since they are different for the 3 cases... - equip_geo_wildcards(ingredient_definition, ingredient_acquisition_A, geo_wc_acquisition_B_sph, - geo_wc_acquisition_B_tri, geo_wc_acquisition_B_anal, added_geo_wildcards); - // Currently, owner_wildcard_write_back and geo_wildcard_write_back might be blank, since give the write-back + // Currently, owner_wildcard_write_back might be blank, since give the write-back // control to the user, and they may need to use atomic operations (atomicExch or atomicAdd) to update the // wildcards. @@ -1516,13 +1774,6 @@ inline void DEMSolver::equipForceModel(std::unordered_map& strMap) { @@ -1712,9 +1963,9 @@ inline void DEMSolver::equipFamilyPrescribedMotions(std::unordered_map& strMap) { @@ -2110,7 +2365,7 @@ inline void DEMSolver::equipIntegrationScheme(std::unordered_map& strMap) { @@ -2131,20 +2386,7 @@ inline void DEMSolver::equipSimParams(std::unordered_map( - DEME_MIN(DEME_MIN(runtimeWarpSize, DEME_KT_CD_NTHREADS_PER_BLOCK), DEME_NUM_BODIES_PER_BLOCK)); - strMap["_nActiveLoadingThreads_"] = std::to_string(nActiveLoadingThreads); + strMap["_nActiveLoadingThreads_"] = std::to_string(NUM_ACTIVE_TEMPLATE_LOADING_THREADS); // nTotalBodyTopologies includes clump topologies and ext obj topologies strMap["_nDistinctMassProperties_"] = std::to_string(nDistinctMassProperties); strMap["_nJitifiableClumpComponents_"] = std::to_string(nJitifiableClumpComponents); @@ -2161,10 +2403,10 @@ inline void DEMSolver::equipKernelIncludes(std::unordered_map #include "API.h" #include "Defines.h" -#include "HostSideHelpers.hpp" +#include "utils/AnalyticalOutput.hpp" +#include "utils/CombinedOwnerUtils.hpp" +#include "utils/HostSideHelpers.hpp" +#include "utils/MeshUtils.hpp" #include "AuxClasses.h" #include "../kernel/DEMHelperKernels.cuh" @@ -17,102 +20,78 @@ #include #include #include +#include +#include +#include namespace deme { DEMSolver::DEMSolver(unsigned int nGPUs) { - if (nGPUs == 0) { - DEME_ERROR("DEMSolver was set to use 0 GPUs and that is currently not supported."); + if (nGPUs == 0 || nGPUs > 2) { + DEME_ERROR("DEMSolver supports one or two GPU devices, but %u was requested.", nGPUs); } - dTkT_InteractionManager = std::make_unique(); - kTMain_InteractionManager = std::make_unique(); - dTMain_InteractionManager = std::make_unique(); - - // Set default solver params - setDefaultSolverParams(); + int visible_devices = 0; + DEME_GPU_CALL(cudaGetDeviceCount(&visible_devices)); + if (visible_devices < 1) { + DEME_ERROR("DEMSolver requires at least one visible CUDA device."); + } - // Determine which device IDs to use: scan available GPUs, use at most 2 - int detected = GpuManager::scanNumDevices(); - if (nGPUs > 2) { - DEME_WARNING("DEMSolver was requested to use %u GPUs, but at most 2 are supported. Using 2 GPUs.", nGPUs); - nGPUs = 2; - } - // Build a 2-element device ID list (one per thread); both threads share the same device when using 1 GPU - unsigned int nToUse = std::min((unsigned int)detected, nGPUs); - std::vector device_ids; - if (nToUse >= 2) { - device_ids = {0, 1}; + if (nGPUs == 2 && visible_devices >= 2) { + constructWorkers({0, 1}); } else { - device_ids = {0, 0}; + if (nGPUs == 2) { + DEME_WARNING( + "Two GPUs were requested, but only one CUDA device is visible. Both DEME workers will use device 0."); + } + constructWorkers({0}); } - // Always use id list to avoid GpuManager deciding device usage by itself - dTkT_GpuManager = std::make_unique(device_ids); +} - // Thread-based worker creation may be needed as the workers allocate DualStructs on construction - std::thread dT_construct([&]() { - // Get a device/stream ID to use from the GPU Manager - const GpuManager::StreamInfo dT_stream_info = dTkT_GpuManager->getAvailableStream(); - DEME_GPU_CALL(cudaSetDevice(dT_stream_info.device)); - dT = std::make_unique(dTMain_InteractionManager.get(), dTkT_InteractionManager.get(), - dT_stream_info); - }); +DEMSolver::DEMSolver(const std::vector& device_ids) { + constructWorkers(device_ids); +} - std::thread kT_construct([&]() { - const GpuManager::StreamInfo kT_stream_info = dTkT_GpuManager->getAvailableStream(); - DEME_GPU_CALL(cudaSetDevice(kT_stream_info.device)); - kT = std::make_unique(kTMain_InteractionManager.get(), dTkT_InteractionManager.get(), - kT_stream_info); - }); +void DEMSolver::constructWorkers(const std::vector& device_ids) { + if (device_ids.empty() || device_ids.size() > 2) { + DEME_ERROR("DEMSolver device selection requires one or two CUDA device IDs; received %zu.", device_ids.size()); + } - dT_construct.join(); - kT_construct.join(); + int visible_devices = 0; + DEME_GPU_CALL(cudaGetDeviceCount(&visible_devices)); + if (visible_devices < 1) { + DEME_ERROR("DEMSolver requires at least one visible CUDA device."); + } + for (int device : device_ids) { + if (device < 0 || device >= visible_devices) { + DEME_ERROR("CUDA device ID %d is out of range. %d CUDA device(s) are visible.", device, visible_devices); + } + } - // Make friends - dT->kT = kT.get(); - kT->dT = dT.get(); -} + const int dT_device = device_ids[0]; + const int kT_device = device_ids.size() == 1 ? device_ids[0] : device_ids[1]; + m_gpu_device_ids = {dT_device, kT_device}; -DEMSolver::DEMSolver(std::vector device_ids) { dTkT_InteractionManager = std::make_unique(); kTMain_InteractionManager = std::make_unique(); dTMain_InteractionManager = std::make_unique(); + // There is one stream record per worker. Repeated IDs intentionally place both workers on the same GPU. + dTkT_GpuManager = std::make_unique(m_gpu_device_ids); + // Set default solver params setDefaultSolverParams(); - // Validate device IDs against the number of physically available GPUs - int detected = GpuManager::scanNumDevices(); - if (device_ids.empty()) { - DEME_ERROR("DEMSolver was given an empty device ID list. Please provide at least one device ID."); - } - for (int id : device_ids) { - if (id < 0 || id >= detected) { - DEME_ERROR("DEMSolver was given device ID %d, but only %d GPU device(s) are available.", id, detected); - } - } - if (device_ids.size() > 2) { - DEME_WARNING("DEMSolver was given %zu device IDs, but at most 2 are supported. Using only the first 2.", - device_ids.size()); - device_ids.resize(2); - } - // When only one device is specified, both threads run on that device - if (device_ids.size() == 1) { - device_ids = {device_ids[0], device_ids[0]}; - } - // Always use id list to avoid GpuManager deciding device usage by itself - dTkT_GpuManager = std::make_unique(device_ids); - // Thread-based worker creation may be needed as the workers allocate DualStructs on construction std::thread dT_construct([&]() { - const GpuManager::StreamInfo dT_stream_info = dTkT_GpuManager->getAvailableStream(); + const GpuManager::StreamInfo dT_stream_info = dTkT_GpuManager->getAvailableStreamFromDevice(dT_device); DEME_GPU_CALL(cudaSetDevice(dT_stream_info.device)); dT = std::make_unique(dTMain_InteractionManager.get(), dTkT_InteractionManager.get(), dT_stream_info); }); std::thread kT_construct([&]() { - const GpuManager::StreamInfo kT_stream_info = dTkT_GpuManager->getAvailableStream(); + const GpuManager::StreamInfo kT_stream_info = dTkT_GpuManager->getAvailableStreamFromDevice(kT_device); DEME_GPU_CALL(cudaSetDevice(kT_stream_info.device)); kT = std::make_unique(kTMain_InteractionManager.get(), dTkT_InteractionManager.get(), kT_stream_info); @@ -127,6 +106,7 @@ DEMSolver::DEMSolver(std::vector device_ids) { } DEMSolver::~DEMSolver() { + WaitForPendingOutput(); if (sys_initialized) DoDynamicsThenSync(0.0); kT.reset(); @@ -137,37 +117,100 @@ DEMSolver::~DEMSolver() { dTkT_GpuManager.reset(); } -void DEMSolver::SetVerbosity(const std::string& verbose) { - std::string u_verbose = str_to_upper(verbose); - switch (hash_charr(u_verbose.c_str())) { - case ("QUIET"_): - verbosity = VERBOSITY::QUIET; +void DEMSolver::SetGravitationalAcceleration(float3 g) { + G = g; + if (!sys_initialized) { + return; + } + + // Gravity is read directly by device kernels, so keep both workers' host mirrors and device copies coherent. + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->Gx = g.x; + dT->simParams->Gy = g.y; + dT->simParams->Gz = g.z; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->Gx = g.x; + kT->simParams->Gy = g.y; + kT->simParams->Gz = g.z; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetTimeStepSize(double ts_size) { + m_ts_size = ts_size; + if (!sys_initialized) { + return; + } + + // The device representation is currently float; narrow only at the host/device boundary. + const float device_ts = static_cast(ts_size); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->dyn.h = device_ts; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->dyn.h = device_ts; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetVerbosity(verbosity_t verbose) { + switch (verbose) { + case VERBOSITY_QUIET: + Logger::GetInstance().SetVerbosity(VERBOSITY_QUIET); + break; + case VERBOSITY_ERROR: + Logger::GetInstance().SetVerbosity(VERBOSITY_ERROR); break; - case ("ERROR"_): - verbosity = VERBOSITY::ERR; + case VERBOSITY_WARNING: + Logger::GetInstance().SetVerbosity(VERBOSITY_WARNING); break; - case ("WARNING"_): - verbosity = VERBOSITY::WARNING; + case VERBOSITY_INFO: + Logger::GetInstance().SetVerbosity(VERBOSITY_INFO); + break; + case VERBOSITY_METRIC: + Logger::GetInstance().SetVerbosity(VERBOSITY_METRIC); + break; + case VERBOSITY_DEBUG: + Logger::GetInstance().SetVerbosity(VERBOSITY_DEBUG); + break; + default: + DEME_ERROR("Verbosity-level %d is unknown in SetVerbosity call.", verbose); + } + verbosity = verbose; +} + +void DEMSolver::SetVerbosity(const std::string& verbose) { + switch (hash_charr(str_to_upper(verbose).c_str())) { + case "QUIET"_: + SetVerbosity(VERBOSITY_QUIET); break; - case ("INFO"_): - verbosity = VERBOSITY::INFO; + case "ERROR"_: + SetVerbosity(VERBOSITY_ERROR); break; - case ("STEP_ANOMALY"_): - verbosity = VERBOSITY::STEP_ANOMALY; + case "WARNING"_: + SetVerbosity(VERBOSITY_WARNING); break; - case ("STEP_METRIC"_): - verbosity = VERBOSITY::STEP_METRIC; + case "INFO"_: + SetVerbosity(VERBOSITY_INFO); break; - case ("DEBUG"_): - verbosity = VERBOSITY::DEBUG; + case "METRIC"_: + SetVerbosity(VERBOSITY_METRIC); break; - case ("STEP_DEBUG"_): - verbosity = VERBOSITY::STEP_DEBUG; + case "DEBUG"_: + SetVerbosity(VERBOSITY_DEBUG); break; default: DEME_ERROR("Instruction %s is unknown in SetVerbosity call.", verbose.c_str()); } } + void DEMSolver::SetOutputFormat(const std::string& format) { std::string u_format = str_to_upper(format); switch (hash_charr(u_format.c_str())) { @@ -177,13 +220,9 @@ void DEMSolver::SetOutputFormat(const std::string& format) { case ("BINARY"_): m_out_format = OUTPUT_FORMAT::BINARY; break; - case ("CHPF"_): -#ifdef DEME_USE_CHPF - m_out_format = OUTPUT_FORMAT::CHPF; + case ("VTK"_): + m_out_format = OUTPUT_FORMAT::VTK; break; -#else - DEME_ERROR("ChPF is not enabled when the code was compiled."); -#endif default: DEME_ERROR("Instruction %s is unknown in SetOutputFormat call.", format.c_str()); } @@ -197,13 +236,6 @@ void DEMSolver::SetContactOutputFormat(const std::string& format) { case ("BINARY"_): m_cnt_out_format = OUTPUT_FORMAT::BINARY; break; - case ("CHPF"_): -#ifdef DEME_USE_CHPF - m_cnt_out_format = OUTPUT_FORMAT::CHPF; - break; -#else - DEME_ERROR("ChPF is not enabled when the code was compiled."); -#endif default: DEME_ERROR("Instruction %s is unknown in SetContactOutputFormat call.", format.c_str()); } @@ -217,6 +249,12 @@ void DEMSolver::SetMeshOutputFormat(const std::string& format) { case ("OBJ"_): m_mesh_out_format = MESH_FORMAT::OBJ; break; + case ("STL"_): + m_mesh_out_format = MESH_FORMAT::STL; + break; + case ("PLY"_): + m_mesh_out_format = MESH_FORMAT::PLY; + break; default: DEME_ERROR("Instruction %s is unknown in SetMeshOutputFormat call.", format.c_str()); } @@ -263,14 +301,70 @@ void DEMSolver::SetOutputContent(const std::vector& content) { case ("OWNER_WILDCARD"_): m_out_content = m_out_content | OUTPUT_CONTENT::OWNER_WILDCARD; break; + default: + DEME_ERROR("Instruction %s is unknown in SetOutputContent call.", content[i].c_str()); + } + } +} + +void DEMSolver::SetMeshOutputContent(const std::vector& content) { + m_mesh_out_content = static_cast(MESH_OUTPUT_CONTENT::XYZ); + for (const auto& field : content) { + const std::string upper_field = str_to_upper(field); + switch (hash_charr(upper_field.c_str())) { + case ("XYZ"_): + break; + case ("QUAT"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::QUAT); + break; + case ("ABSV"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::ABSV); + break; + case ("VEL"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::VEL); + break; + case ("ANG_VEL"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::ANG_VEL); + break; + case ("ABS_ACC"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::ABS_ACC); + break; + case ("ACC"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::ACC); + break; + case ("ANG_ACC"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::ANG_ACC); + break; + case ("FAMILY"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::FAMILY); + break; + case ("MAT"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::MAT); + break; + case ("OWNER_WILDCARD"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::OWNER_WILDCARD); + break; case ("GEO_WILDCARD"_): - m_out_content = m_out_content | OUTPUT_CONTENT::GEO_WILDCARD; + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::GEO_WILDCARD); + break; + case ("OWNER"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::OWNER); + break; + case ("MESH_ID"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::MESH_ID); + break; + case ("TRI_ID"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::TRI_ID); + break; + case ("PATCH_ID"_): + m_mesh_out_content |= static_cast(MESH_OUTPUT_CONTENT::PATCH_ID); break; default: - DEME_ERROR("Instruction %s is unknown in SetOutputContent call.", content[i].c_str()); + DEME_ERROR("Instruction %s is unknown in SetMeshOutputContent call.", field.c_str()); } } } + void DEMSolver::SetContactOutputContent(const std::vector& content) { std::vector u_content(content.size()); for (unsigned int i = 0; i < content.size(); i++) { @@ -315,20 +409,63 @@ void DEMSolver::SetContactOutputContent(const std::vector& content) } } +void DEMSolver::SetMeshUniversalContact(bool use) { + kT->solverFlags.meshUniversalContact = use; + dT->solverFlags.meshUniversalContact = use; +} + +void DEMSolver::SetPersistentContact(bool use) { + kT->solverFlags.hasPersistentContacts = use; + dT->solverFlags.hasPersistentContacts = use; +} + +void DEMSolver::SetSimplePatchCombination(bool use) { + kT->solverFlags.useSimplePatchCombination = use; + dT->solverFlags.useSimplePatchCombination = use; +} + +void DEMSolver::SetStablePatchIslandIDs(bool use) { + kT->solverFlags.useStablePatchIslandIDs = use; + dT->solverFlags.useStablePatchIslandIDs = use; +} + +void DEMSolver::SetMeshParticlesLowPoly(bool use) { + kT->simParams->meshParticlesLowPoly = use; + dT->simParams->meshParticlesLowPoly = use; + if (sys_initialized) { + // This toggle is read inside both workers' device kernels, so on-the-fly changes must update device simParams. + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams.toDevice(); + } + } +} + void DEMSolver::SyncMemoryTransfer() { - dT->syncMemoryTransfer(); - kT->syncMemoryTransfer(); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->syncMemoryTransfer(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->syncMemoryTransfer(); + } } std::vector DEMSolver::GetOwnerContactClumps(bodyID_t ownerID) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); // Is this owner a clump? ownerType_t this_type = dT->ownerTypes[ownerID]; // ownerTypes has no way to change on device std::vector geo_to_watch; // geo IDs that need to scan // Get device-major info to host first - dT->idGeometryA.toHostAsync(dT->streamInfo.stream); - dT->idGeometryB.toHostAsync(dT->streamInfo.stream); - dT->contactType.toHostAsync(dT->streamInfo.stream); + dT->idPatchA.toHostAsync(dT->streamInfo.stream); + dT->idPatchB.toHostAsync(dT->streamInfo.stream); + dT->contactTypePatch.toHostAsync(dT->streamInfo.stream); // These arrays can't change on device switch (this_type) { @@ -345,8 +482,8 @@ std::vector DEMSolver::GetOwnerContactClumps(bodyID_t ownerID) const { } break; case OWNER_T_MESH: - for (bodyID_t i = 0; i < nTriGM; i++) { - if (ownerID == dT->ownerMesh[i]) + for (bodyID_t i = 0; i < nMeshPatches; i++) { + if (ownerID == dT->ownerPatchMesh[i]) geo_to_watch.push_back(i); } break; @@ -356,38 +493,19 @@ std::vector DEMSolver::GetOwnerContactClumps(bodyID_t ownerID) const { dT->syncMemoryTransfer(); std::vector clumps_in_cnt; - // If this is not clump, then checking idB for it is enough - if (this_type != OWNER_T_CLUMP) { - for (size_t i = 0; i < dT->getNumContacts(); i++) { - auto idA = dT->idGeometryA[i]; - auto idB = dT->idGeometryB[i]; - if (!check_exist(geo_to_watch, idB)) - continue; - auto cnt_type = dT->contactType[i]; - // If it is a mesh facet, then contact type needs to match - if (this_type == OWNER_T_MESH) { - if (cnt_type == SPHERE_MESH_CONTACT) { - clumps_in_cnt.push_back(dT->ownerClumpBody[idA]); - } - } else { // If analytical, then contact type larger than PLANE is fine - if (cnt_type >= SPHERE_PLANE_CONTACT) { - clumps_in_cnt.push_back(dT->ownerClumpBody[idA]); - } - } - } - } else { // If a clump, then both idA and idB need to be checked - for (size_t i = 0; i < dT->getNumContacts(); i++) { - auto idA = dT->idGeometryA[i]; - auto idB = dT->idGeometryB[i]; - auto cnt_type = dT->contactType[i]; + for (size_t i = 0; i < dT->getNumContacts(); i++) { + auto idA = dT->idPatchA[i]; + auto idB = dT->idPatchB[i]; + auto cnt_type = dT->contactTypePatch[i]; + // Using decode to get the A and B type + auto typeA = decodeTypeA(cnt_type); + auto typeB = decodeTypeB(cnt_type); + // Ensure it's a clump-related contact + if (typeA == GEO_T_SPHERE || typeB == GEO_T_SPHERE) { if (check_exist(geo_to_watch, idA)) { - if (cnt_type == SPHERE_SPHERE_CONTACT) { - clumps_in_cnt.push_back(dT->ownerClumpBody[idB]); - } + clumps_in_cnt.push_back(dT->getPatchOwnerID(idB, typeB)); } else if (check_exist(geo_to_watch, idB)) { - if (cnt_type == SPHERE_SPHERE_CONTACT) { - clumps_in_cnt.push_back(dT->ownerClumpBody[idA]); - } + clumps_in_cnt.push_back(dT->getPatchOwnerID(idA, typeA)); } } } @@ -409,6 +527,11 @@ std::shared_ptr DEMSolver::Duplicate(const std::shared_ptrAddClumps(obj); } +std::shared_ptr DEMSolver::Duplicate(const std::shared_ptr& ptr) { + // Make a copy + DEMMesh obj = *ptr; + return this->AddMesh(obj); +} std::vector> DEMSolver::GetClumpContacts() const { std::vector idA_tmp, idB_tmp; @@ -531,31 +654,389 @@ std::vector> DEMSolver::GetContacts( } std::shared_ptr DEMSolver::GetContactDetailedInfo(float force_thres) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->generateContactInfo(force_thres); } std::vector DEMSolver::GetOwnerPosition(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerPos(ownerID, n); } +std::vector DEMSolver::GetClumpPositionsHandover() const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + return dT->getOwnerPos(0, nOwnerClumps); +} std::vector DEMSolver::GetOwnerAngVel(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerAngVel(ownerID, n); } std::vector DEMSolver::GetOwnerVelocity(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerVel(ownerID, n); } std::vector DEMSolver::GetOwnerOriQ(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerOriQ(ownerID, n); } std::vector DEMSolver::GetOwnerAcc(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerAcc(ownerID, n); } std::vector DEMSolver::GetOwnerAngAcc(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerAngAcc(ownerID, n); } std::vector DEMSolver::GetOwnerFamily(bodyID_t ownerID, bodyID_t n) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerFamily(ownerID, n); } +void DEMSolver::GetOwnerPositionToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::POSITION, 0, + validate); +} + +void DEMSolver::GetOwnerVelocityToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::VELOCITY, 0, + validate); +} + +void DEMSolver::GetOwnerAngVelLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, + OwnerDataField::ANGULAR_VELOCITY_LOCAL, 0, validate); +} + +void DEMSolver::GetOwnerAngVelGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, + OwnerDataField::ANGULAR_VELOCITY_GLOBAL, 0, validate); +} + +void DEMSolver::GetOwnerOriQToDevice(float4* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::ORIENTATION, 0, + validate); +} + +void DEMSolver::GetOwnerAccToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, + OwnerDataField::CONTACT_ACCELERATION, 0, validate); +} + +void DEMSolver::GetOwnerAngAccLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, + OwnerDataField::CONTACT_ANGULAR_ACCELERATION_LOCAL, 0, validate); +} + +void DEMSolver::GetOwnerAngAccGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, + OwnerDataField::CONTACT_ANGULAR_ACCELERATION_GLOBAL, 0, validate); +} + +void DEMSolver::GetOwnerContactWrench(std::vector& forces, + std::vector& torques, + bodyID_t ownerID, + bodyID_t count) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerContactWrench(forces, torques, ownerID, count); +} + +void DEMSolver::GetOwnerContactWrenchToDevice(float3* force_destination, + float3* torque_destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t count) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerContactWrenchToDevice(force_destination, torque_destination, capacity, destination_device, ownerID, + count); +} + +void DEMSolver::GetOwnerFamilyToDevice(unsigned int* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::FAMILY, 0, + validate); +} + +void DEMSolver::RequestContactUpdate() { + assertSysInit("RequestContactUpdate"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->announceCritical(); +} + +void DEMSolver::SetTrianglePVTrackingOwners(const std::vector& mesh_owner_ids) { + assertSysInit("SetTrianglePVTrackingOwners"); + m_user_tri_pv_tracking_owners = mesh_owner_ids; + refreshTrianglePVTrackingOwners(); +} + +void DEMSolver::DisableTrianglePVTracking() { + assertSysInit("DisableTrianglePVTracking"); + m_user_tri_pv_tracking_owners.clear(); + refreshTrianglePVTrackingOwners(); +} + +bool DEMSolver::GetTrackedOwnerTrianglePV(bodyID_t ownerID, + std::vector& avgP, + std::vector& avgV, + std::vector& avgPV, + bool reset_window) { + assertSysInit("GetTrackedOwnerTrianglePV"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + const bool ok = dT->getTrackedOwnerTrianglePV(ownerID, avgP, avgV, avgPV, reset_window); + if (!ok) { + return false; + } + // Wear consumes and resets the live tracking window inside DoDynamics; keep the last averaged window available for + // display-style queries that do not request a reset. + if (!reset_window && dT->triPVWindowSteps == 0) { + auto it = m_last_tri_pv_snapshot.find(ownerID); + if (it != m_last_tri_pv_snapshot.end()) { + avgP = it->second.avgP; + avgV = it->second.avgV; + avgPV = it->second.avgPV; + } + } + return true; +} + +void DEMSolver::EnableMeshWearModel(bodyID_t ownerID, + double wear_rate, + double update_interval, + double start_time, + double end_time, + float normal_sign) { + assertSysInit("EnableMeshWearModel"); + if (ownerID >= nOwnerBodies) { + DEME_ERROR("Owner ID %zu is out of range [0, %zu).", (size_t)ownerID, (size_t)nOwnerBodies); + } + if (m_owner_mesh_map.find(ownerID) == m_owner_mesh_map.end()) { + DEME_ERROR("Owner ID %zu is not a mesh owner. Wear model supports mesh owners only.", (size_t)ownerID); + } + if (!(std::isfinite(wear_rate) && wear_rate >= 0.0)) { + DEME_ERROR("Wear rate must be finite and non-negative (got %.9g).", wear_rate); + } + if (!(std::isfinite(update_interval) && update_interval > 0.0)) { + DEME_ERROR("Wear update interval must be finite and > 0 (got %.9g).", update_interval); + } + const double min_interval = static_cast(dT->simParams->dyn.h); + if (update_interval + 1e-15 < min_interval) { + DEME_ERROR("Wear update interval %.9g is smaller than current solver step %.9g.", update_interval, + min_interval); + } + if (!(std::isfinite(start_time) && start_time >= 0.0)) { + DEME_ERROR("Wear start time must be finite and >= 0 (got %.9g).", start_time); + } + if (std::isfinite(end_time) && end_time >= 0.0 && !(end_time > start_time)) { + DEME_ERROR("Wear end time %.9g must be > start time %.9g.", end_time, start_time); + } + if (!std::isfinite(normal_sign) || std::abs(normal_sign) <= DEME_TINY_FLOAT) { + DEME_ERROR("Wear normal_sign must be finite and non-zero (got %.9g).", (double)normal_sign); + } + + size_t tri_start = 0; + size_t tri_count = 0; + if (!findOwnerTriangleRange(ownerID, tri_start, tri_count)) { + DEME_ERROR("Cannot determine triangle range for mesh owner %zu.", (size_t)ownerID); + } + auto mesh_it = m_owner_mesh_map.find(ownerID); + auto& mesh = m_meshes.at(mesh_it->second); + const auto& faces = mesh->GetIndicesVertexes(); + if (faces.size() != tri_count) { + DEME_ERROR("Wear model triangle count mismatch for owner %zu (range=%zu, mesh=%zu).", (size_t)ownerID, + tri_count, faces.size()); + } + const auto& vertices = mesh->GetCoordsVertices(); + if (vertices.empty()) { + DEME_ERROR("Mesh owner %zu has no vertices; wear model cannot be enabled.", (size_t)ownerID); + } + + MeshWearModelState state; + state.wear_rate = wear_rate; + state.update_interval = update_interval; + state.start_time = start_time; + state.end_time = end_time; + state.normal_sign = (normal_sign >= 0.f) ? 1.f : -1.f; + state.tri_start = tri_start; + state.tri_count = tri_count; + state.pending_depth.assign(tri_count, 0.f); + + const double eps = computeVertexQuantEps(vertices); + state.vertex_to_canon = buildCanonicalVertexMap(vertices, eps); + if (state.vertex_to_canon.size() != vertices.size()) { + DEME_ERROR("Failed to build canonical vertex map for wear model owner %zu.", (size_t)ownerID); + } + size_t max_group = 0; + for (size_t g : state.vertex_to_canon) { + max_group = std::max(max_group, g); + } + state.n_canon_vertices = max_group + 1; + + constexpr double kWearCapFractionOfMedianEdge = 2.0; + state.max_depth_fraction_of_median_edge = static_cast(kWearCapFractionOfMedianEdge); + state.ref_tri_normals.assign(tri_count, make_float3(0.f)); + auto cross3 = [](const float3& a, const float3& b) { + return make_float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); + }; + std::vector edge_lengths; + edge_lengths.reserve(tri_count * 3); + double sum_edge_len = 0.0; + size_t n_edges = 0; + for (size_t tri = 0; tri < tri_count; tri++) { + const int3& f = faces[tri]; + if (f.x < 0 || f.y < 0 || f.z < 0) { + continue; + } + const size_t i0 = static_cast(f.x); + const size_t i1 = static_cast(f.y); + const size_t i2 = static_cast(f.z); + if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) { + continue; + } + const float3 v0 = vertices[i0]; + const float3 v1 = vertices[i1]; + const float3 v2 = vertices[i2]; + const float3 n_raw = cross3(v1 - v0, v2 - v0); + const float n_len = length(n_raw); + if (n_len > DEME_TINY_FLOAT && std::isfinite(n_len)) { + state.ref_tri_normals[tri] = n_raw / n_len; + } + const float e01 = length(v1 - v0); + const float e12 = length(v2 - v1); + const float e20 = length(v0 - v2); + if (e01 > DEME_TINY_FLOAT && std::isfinite(e01)) { + edge_lengths.push_back(e01); + } + if (e12 > DEME_TINY_FLOAT && std::isfinite(e12)) { + edge_lengths.push_back(e12); + } + if (e20 > DEME_TINY_FLOAT && std::isfinite(e20)) { + edge_lengths.push_back(e20); + } + sum_edge_len += static_cast(e01 + e12 + e20); + n_edges += 3; + } + + double median_edge = 0.0; + if (!edge_lengths.empty()) { + const size_t n = edge_lengths.size(); + const size_t mid = n / 2; + auto mid_it = edge_lengths.begin() + mid; + std::nth_element(edge_lengths.begin(), mid_it, edge_lengths.end()); + median_edge = static_cast(*mid_it); + if (n % 2 == 0) { + const auto max_lower_it = std::max_element(edge_lengths.begin(), mid_it); + if (max_lower_it != mid_it) { + median_edge = 0.5 * (median_edge + static_cast(*max_lower_it)); + } + } + } + if (!(median_edge > 0.0) || !std::isfinite(median_edge)) { + median_edge = (n_edges > 0) ? (sum_edge_len / static_cast(n_edges)) : 0.0; + } + state.median_edge_length = static_cast(std::max(0.0, median_edge)); + state.max_depth_per_update = static_cast(std::max(1e-8, median_edge * kWearCapFractionOfMedianEdge)); + + m_mesh_wear_models[ownerID] = std::move(state); + refreshTrianglePVTrackingOwners(); +} + +void DEMSolver::DisableMeshWearModel(bodyID_t ownerID) { + assertSysInit("DisableMeshWearModel"); + auto it = m_mesh_wear_models.find(ownerID); + if (it == m_mesh_wear_models.end()) { + return; + } + m_mesh_wear_models.erase(it); + refreshTrianglePVTrackingOwners(); +} + +void DEMSolver::DisableAllMeshWearModels() { + assertSysInit("DisableAllMeshWearModels"); + if (m_mesh_wear_models.empty()) { + return; + } + m_mesh_wear_models.clear(); + refreshTrianglePVTrackingOwners(); +} + +void DEMSolver::FlushMeshWearModels() { + assertSysInit("FlushMeshWearModels"); + if (m_mesh_wear_models.empty()) { + return; + } + + std::vector owners_to_apply; + owners_to_apply.reserve(m_mesh_wear_models.size()); + for (const auto& kv : m_mesh_wear_models) { + if (hasPendingWear(kv.second.pending_depth)) { + owners_to_apply.push_back(kv.first); + } + } + if (!owners_to_apply.empty()) { + WaitForPendingOutput(); + } + for (bodyID_t owner : owners_to_apply) { + auto it = m_mesh_wear_models.find(owner); + if (it == m_mesh_wear_models.end()) { + continue; + } + applyMeshWearModel(owner, it->second); + it->second.pending_time = 0.0; + } +} + std::vector DEMSolver::GetOwnerWildcardValue(bodyID_t ownerID, const std::string& name, bodyID_t n) { assertSysInit("GetOwnerWildcardValue"); if (m_owner_wc_num.find(name) == m_owner_wc_num.end()) { @@ -564,9 +1045,29 @@ std::vector DEMSolver::GetOwnerWildcardValue(bodyID_t ownerID, const std: "SetPerOwnerWildcards first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerWildcardValue(ownerID, m_owner_wc_num.at(name), n); } +void DEMSolver::GetOwnerWildcardValueToDevice(float* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + const std::string& name, + bodyID_t n, + bool validate) { + assertSysInit("GetOwnerWildcardValueToDevice"); + if (m_owner_wc_num.find(name) == m_owner_wc_num.end()) { + DEME_ERROR( + "No owner wildcard in the force model is named %s.\nIf you need to use it, declare it via " + "SetPerOwnerWildcards first.", + name.c_str()); + } + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::WILDCARD, + m_owner_wc_num.at(name), validate); +} + std::vector DEMSolver::GetAllOwnerWildcardValue(const std::string& name) { assertSysInit("GetAllOwnerWildcardValue"); if (m_owner_wc_num.find(name) == m_owner_wc_num.end()) { @@ -576,6 +1077,7 @@ std::vector DEMSolver::GetAllOwnerWildcardValue(const std::string& name) name.c_str()); } std::vector res; + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->getAllOwnerWildcardValue(res, m_owner_wc_num.at(name)); return res; } @@ -589,52 +1091,15 @@ std::vector DEMSolver::GetFamilyOwnerWildcardValue(unsigned int N, const name.c_str()); } std::vector res; + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->getFamilyOwnerWildcardValue(res, N, m_owner_wc_num.at(name)); return res; } -std::vector DEMSolver::GetTriWildcardValue(bodyID_t geoID, const std::string& name, size_t n) { - assertSysInit("GetTriWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - std::vector res; - dT->getTriWildcardValue(res, geoID, m_geo_wc_num.at(name), n); - return res; -} - -std::vector DEMSolver::GetSphereWildcardValue(bodyID_t geoID, const std::string& name, size_t n) { - assertSysInit("GetSphereWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - std::vector res; - dT->getSphereWildcardValue(res, geoID, m_geo_wc_num.at(name), n); - return res; -} - -std::vector DEMSolver::GetAnalWildcardValue(bodyID_t geoID, const std::string& name, size_t n) { - assertSysInit("GetAnalWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - std::vector res; - dT->getAnalWildcardValue(res, geoID, m_geo_wc_num.at(name), n); - return res; -} - size_t DEMSolver::GetOwnerContactForces(const std::vector& ownerIDs, std::vector& points, std::vector& forces) { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerContactForces(ownerIDs, points, forces); } size_t DEMSolver::GetOwnerContactForces(const std::vector& ownerIDs, @@ -642,9 +1107,32 @@ size_t DEMSolver::GetOwnerContactForces(const std::vector& ownerIDs, std::vector& forces, std::vector& torques, bool torque_in_local) { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getOwnerContactForces(ownerIDs, points, forces, torques, torque_in_local); } +size_t DEMSolver::GetOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + size_t capacity, + int destination_device) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + return dT->getOwnerContactForcesToDevice(ownerIDs, points, forces, nullptr, capacity, destination_device, false, + false); +} + +size_t DEMSolver::GetOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bool torque_in_local) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + return dT->getOwnerContactForcesToDevice(ownerIDs, points, forces, torques, capacity, destination_device, true, + torque_in_local); +} + std::vector DEMSolver::GetOwnerMass(bodyID_t ownerID, bodyID_t n) const { std::vector res(n); for (bodyID_t i = 0; i < n; i++) { @@ -679,34 +1167,134 @@ std::vector DEMSolver::GetOwnerMOI(bodyID_t ownerID, bodyID_t n) const { return res; } +void DEMSolver::GetOwnerMassToDevice(float* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::MASS, 0, validate); +} + +void DEMSolver::GetOwnerMOIToDevice(float3* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + bool validate) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->getOwnerDataToDevice(destination, capacity, destination_device, ownerID, n, OwnerDataField::MOI, 0, validate); +} + void DEMSolver::AddOwnerNextStepAcc(bodyID_t ownerID, const std::vector& acc) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->addOwnerNextStepAcc(ownerID, acc); } void DEMSolver::AddOwnerNextStepAngAcc(bodyID_t ownerID, const std::vector& angAcc) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->addOwnerNextStepAngAcc(ownerID, angAcc); } +void DEMSolver::AddOwnerNextStepAccFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("AddOwnerNextStepAccFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::NEXT_STEP_ACCELERATION, + validate); +} +void DEMSolver::AddOwnerNextStepAngAccFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("AddOwnerNextStepAngAccFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, + OwnerStateField::NEXT_STEP_ANGULAR_ACCELERATION_LOCAL, validate); +} void DEMSolver::SetOwnerPosition(bodyID_t ownerID, const std::vector& pos) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setOwnerPos(ownerID, pos); } void DEMSolver::SetOwnerAngVel(bodyID_t ownerID, const std::vector& angVel) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setOwnerAngVel(ownerID, angVel); } void DEMSolver::SetOwnerVelocity(bodyID_t ownerID, const std::vector& vel) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setOwnerVel(ownerID, vel); } void DEMSolver::SetOwnerOriQ(bodyID_t ownerID, const std::vector& oriQ) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setOwnerOriQ(ownerID, oriQ); } +void DEMSolver::SetOwnerPositionFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("SetOwnerPositionFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::POSITION, validate); +} +void DEMSolver::SetOwnerOriQFromDevice(bodyID_t ownerID, + const float4* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("SetOwnerOriQFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::ORIENTATION, validate); +} +void DEMSolver::SetOwnerVelocityFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("SetOwnerVelocityFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::VELOCITY, validate); +} +void DEMSolver::SetOwnerAngVelFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("SetOwnerAngVelFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::ANGULAR_VELOCITY_LOCAL, + validate); +} +void DEMSolver::SetOwnerAngVelGlobalFromDevice(bodyID_t ownerID, + const float3* source, + int source_device, + size_t count, + bool validate) { + assertSysInit("SetOwnerAngVelGlobalFromDevice"); + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerDataFromDevice(ownerID, source, count, source_device, OwnerStateField::ANGULAR_VELOCITY_GLOBAL, + validate); +} void DEMSolver::SetOwnerFamily(bodyID_t ownerID, unsigned int fam, bodyID_t n) { if (fam > std::numeric_limits::max()) { DEME_ERROR("You called SetOwnerFamily with family number %u, but family number should not be larger than %u.", fam, std::numeric_limits::max()); } - kT->setOwnerFamily(ownerID, static_cast(fam), n); - dT->setOwnerFamily(ownerID, static_cast(fam), n); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->setOwnerFamily(ownerID, static_cast(fam), n); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->setOwnerFamily(ownerID, static_cast(fam), n); + } } void DEMSolver::SetTriNodeRelPos(size_t owner, size_t triID, const std::vector& new_nodes) { + WaitForPendingOutput(); auto& mesh = m_meshes.at(m_owner_mesh_map.at(owner)); if (mesh->GetNumNodes() != new_nodes.size()) { DEME_ERROR( @@ -722,6 +1310,7 @@ void DEMSolver::SetTriNodeRelPos(size_t owner, size_t triID, const std::vectorGetNumTriangles(); i++) { new_triangles[i] = mesh->GetTriangle(i); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setTriNodeRelPos(triID, new_triangles); dT->solverFlags.willMeshDeform = true; @@ -729,6 +1318,7 @@ void DEMSolver::SetTriNodeRelPos(size_t owner, size_t triID, const std::vectorsetTriNodeRelPos(triID, new_triangles); } void DEMSolver::UpdateTriNodeRelPos(size_t owner, size_t triID, const std::vector& updates) { + WaitForPendingOutput(); auto& mesh = m_meshes.at(m_owner_mesh_map.at(owner)); if (mesh->GetNumNodes() != updates.size()) { DEME_ERROR( @@ -746,19 +1336,21 @@ void DEMSolver::UpdateTriNodeRelPos(size_t owner, size_t triID, const std::vecto new_triangles[i] = mesh->GetTriangle(i); } // This is correct to use setTriNodeRelPos, as mesh is already modified in this method + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setTriNodeRelPos(triID, new_triangles); dT->solverFlags.willMeshDeform = true; // kT just receives update from dT, to avoid mem hazards // kT->setTriNodeRelPos(triID, new_triangles); } -std::shared_ptr& DEMSolver::GetCachedMesh(bodyID_t ownerID) { +std::shared_ptr& DEMSolver::GetCachedMesh(bodyID_t ownerID) { if (m_owner_mesh_map.find(ownerID) == m_owner_mesh_map.end()) { DEME_ERROR("Owner %zu is not a mesh, you therefore cannot retrive a handle to mesh using it.", (size_t)ownerID); } return m_meshes.at(m_owner_mesh_map.at(ownerID)); } std::vector DEMSolver::GetMeshNodesGlobal(bodyID_t ownerID) { + ScopedCudaDevice device_scope(dT->streamInfo.device); if (m_owner_mesh_map.find(ownerID) == m_owner_mesh_map.end()) { DEME_ERROR("Owner %zu is not a mesh, you therefore cannot get its nodes' coordinates.", (size_t)ownerID); } @@ -772,14 +1364,135 @@ std::vector DEMSolver::GetMeshNodesGlobal(bodyID_t ownerID) { } double DEMSolver::GetSimTime() const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getSimTime(); } void DEMSolver::SetSimTime(double time) { + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setSimTime(time); } +// Separate immutable local geometry from owner motion so renderers can retain their vertex/instance buffers. +DEMVisualizationScene DEMSolver::GetVisualizationScene() const { + if (!sys_initialized) { + DEME_ERROR("GetVisualizationScene requires Initialize() first."); + } + ScopedCudaDevice device_scope(dT->streamInfo.device); + DEMVisualizationScene scene; + scene.revision = dT->visualizationRevision; + scene.spheres.reserve(dT->simParams->nSpheresGM); + for (size_t i = 0; i < dT->simParams->nSpheresGM; ++i) { + const size_t component = dT->solverFlags.useClumpJitify ? dT->clumpComponentOffsetExt[i] : i; + scene.spheres.push_back( + {make_float3(dT->relPosSphereX[component], dT->relPosSphereY[component], dT->relPosSphereZ[component]), + dT->radiiSphere[component], dT->ownerClumpBody[i]}); + } + scene.triangles.reserve(dT->simParams->nTriGM); + for (size_t i = 0; i < dT->simParams->nTriGM; ++i) { + scene.triangles.push_back({dT->relPosNode1[i], dT->relPosNode2[i], dT->relPosNode3[i], dT->ownerTriMesh[i]}); + } + return scene; +} + +// Decode each owner only once. Reusing the output arrays avoids allocation when the owner count is unchanged. +void DEMSolver::GetVisualizationFrame(DEMVisualizationFrame& frame, bool include_velocities) const { + if (!sys_initialized) { + DEME_ERROR("GetVisualizationFrame requires Initialize() first."); + } + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + if (include_velocities) { + dT->vX.toHost(); + dT->vY.toHost(); + dT->vZ.toHost(); + } + frame.simulation_time = GetSimTime(); + frame.revision = dT->visualizationRevision; + const size_t count = dT->simParams->nOwnerBodies; + frame.positions.resize(count); + frame.orientations.resize(count); + frame.families.resize(count); + frame.velocities.resize(include_velocities ? count : 0); + for (size_t i = 0; i < count; ++i) { + auto& p = frame.positions[i]; + voxelIDToPosition(p.x, p.y, p.z, dT->voxelID[i], dT->locX[i], dT->locY[i], + dT->locZ[i], dT->simParams->nvXp2, dT->simParams->nvYp2, + dT->simParams->voxelSize, dT->simParams->l); + p.x += dT->simParams->LBFX; + p.y += dT->simParams->LBFY; + p.z += dT->simParams->LBFZ; + frame.orientations[i] = make_float4(dT->oriQx[i], dT->oriQy[i], dT->oriQz[i], dT->oriQw[i]); + frame.families[i] = dT->familyID[i]; + if (include_velocities) { + frame.velocities[i] = make_float3(dT->vX[i], dT->vY[i], dT->vZ[i]); + } + } +} +DEMVisualizationSnapshot DEMSolver::GetVisualizationSnapshot(bool include_spheres, bool include_triangles) const { + if (!sys_initialized) { + DEME_ERROR("DEMSolver's method GetVisualizationSnapshot can only be called after calling Initialize()"); + } + + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + + DEMVisualizationSnapshot snapshot; + snapshot.simulation_time = GetSimTime(); + + // Convert DEME's voxel/sub-voxel owner representation once per requested geometry. The visualization boundary is + // deliberately host-side so renderers never depend on dT storage or worker synchronization details. + auto owner_pos = [this](bodyID_t owner) { + float3 position; + voxelIDToPosition( + position.x, position.y, position.z, dT->voxelID[owner], dT->locX[owner], dT->locY[owner], dT->locZ[owner], + dT->simParams->nvXp2, dT->simParams->nvYp2, dT->simParams->voxelSize, dT->simParams->l); + position.x += dT->simParams->LBFX; + position.y += dT->simParams->LBFY; + position.z += dT->simParams->LBFZ; + return position; + }; + auto owner_ori = [this](bodyID_t owner) { + return make_float4(dT->oriQx[owner], dT->oriQy[owner], dT->oriQz[owner], dT->oriQw[owner]); + }; + + if (include_spheres) { + snapshot.spheres.reserve(dT->simParams->nSpheresGM); + for (size_t sphere_id = 0; sphere_id < dT->simParams->nSpheresGM; ++sphere_id) { + const bodyID_t owner = dT->ownerClumpBody[sphere_id]; + const size_t component = + dT->solverFlags.useClumpJitify ? dT->clumpComponentOffsetExt[sphere_id] : sphere_id; + float3 position = + make_float3(dT->relPosSphereX[component], dT->relPosSphereY[component], dT->relPosSphereZ[component]); + applyFrameTransformLocalToGlobal(position, owner_pos(owner), owner_ori(owner)); + snapshot.spheres.push_back( + DEMVisualizationSphere{position, dT->radiiSphere[component], dT->familyID[owner], owner}); + } + } + + if (include_triangles) { + snapshot.triangles.reserve(dT->simParams->nTriGM); + for (size_t triangle_id = 0; triangle_id < dT->simParams->nTriGM; ++triangle_id) { + const bodyID_t owner = dT->ownerTriMesh[triangle_id]; + const float3 position = owner_pos(owner); + const float4 orientation = owner_ori(owner); + float3 a = dT->relPosNode1[triangle_id]; + float3 b = dT->relPosNode2[triangle_id]; + float3 c = dT->relPosNode3[triangle_id]; + applyFrameTransformLocalToGlobal(a, position, orientation); + applyFrameTransformLocalToGlobal(b, position, orientation); + applyFrameTransformLocalToGlobal(c, position, orientation); + snapshot.triangles.push_back(DEMVisualizationTriangle{a, b, c, dT->familyID[owner], owner}); + } + } + + return snapshot; +} + float DEMSolver::GetUpdateFreq() const { + ScopedCudaDevice device_scope(dT->streamInfo.device); return dT->getUpdateFreq(); } @@ -801,16 +1514,22 @@ void DEMSolver::SetIntegrator(const std::string& intg) { } void DEMSolver::SetAdaptiveTimeStepType(const std::string& type) { - DEME_WARNING( - "SetAdaptiveTimeStepType is currently not implemented and has no effect, time step size is still fixed."); switch (hash_charr(type.c_str())) { case ("none"_): adapt_ts_type = ADAPT_TS_TYPE::NONE; break; + case ("hertz_const"_): + DEME_WARNING( + "SetAdaptiveTimeStepType(\"hertz_const\") is a beta setup-time estimate. It computes one fixed " + "timestep from Hertzian stiffness, minimum clump mass, and minimum radius."); + adapt_ts_type = ADAPT_TS_TYPE::HERTZ_CONST; + break; case ("max_vel"_): + DEME_WARNING("SetAdaptiveTimeStepType(\"max_vel\") is not implemented yet and has no runtime effect."); adapt_ts_type = ADAPT_TS_TYPE::MAX_VEL; break; case ("int_diff"_): + DEME_WARNING("SetAdaptiveTimeStepType(\"int_diff\") is not implemented yet and has no runtime effect."); adapt_ts_type = ADAPT_TS_TYPE::INT_DIFF; break; default: @@ -833,12 +1552,166 @@ void DEMSolver::SetMaxVelocity(float max_vel) { m_approx_max_vel = max_vel; } -void DEMSolver::SetExpandSafetyType(const std::string& insp_type) { - if (insp_type == "auto") { - m_max_v_finder_type = MARGIN_FINDER_TYPE::DEFAULT; - use_user_defined_expand_factor = false; - } else { - DEME_ERROR("Unknown string input \"%s\" for SetExpandSafetyType.", insp_type.c_str()); +void DEMSolver::SetMaxTriTriPenetration(double max_margin) { + if (max_margin < 0.0) { + DEME_WARNING("SetMaxTriTriPenetration called with negative value %.6g. Setting to 0.", max_margin); + max_margin = 0.0; + } + m_max_tritri_penetration = max_margin; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->capTriTriPenetration = max_margin; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->capTriTriPenetration = max_margin; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetTriTriContactRejectionRatio(float ratio) { + m_triTriContactRejectionRatio = ratio; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->triTriContactRejectionRatio = ratio; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->triTriContactRejectionRatio = ratio; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetMaxSphereInBin(unsigned int max_sph) { + threshold_too_many_spheres_in_bin = max_sph; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->errOutBinSphNum = max_sph; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->errOutBinSphNum = max_sph; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetMaxTriangleInBin(unsigned int max_tri) { + threshold_too_many_tri_in_bin = max_tri; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->errOutBinTriNum = max_tri; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->errOutBinTriNum = max_tri; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetErrorOutVelocity(float vel) { + threshold_error_out_vel = vel; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->errOutVel = vel; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->errOutVel = vel; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetErrorOutAngularVelocity(float ang_vel) { + threshold_error_out_angvel = ang_vel; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->errOutAngVel = ang_vel; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->errOutAngVel = ang_vel; + kT->simParams.toDevice(); + } +} + +void DEMSolver::SetErrorOutAvgContacts(float num_cnts) { + threshold_error_out_num_cnts = num_cnts; + if (!sys_initialized) { + return; + } + dT->solverFlags.errOutAvgPrimitiveCnts = num_cnts; + kT->solverFlags.errOutAvgPrimitiveCnts = num_cnts; +} + +void DEMSolver::SetTriTriPenetration(double penetration) { + if (penetration < 0.0) { + DEME_WARNING("SetTriTriPenetration called with negative value %.6g. Setting to 0.", penetration); + penetration = 0.0; + } + if (!sys_initialized) { + DEME_WARNING(std::string( + "SetTriTriPenetration called before system initialization. This has no effect until after Initialize().")); + return; + } + size_t nTriGM = dT->maxTriTriPenetration.size(); + if (nTriGM == 0) { + return; + } + + std::vector hostBuf(nTriGM, static_cast(penetration)); + ScopedCudaDevice device_scope(dT->streamInfo.device); + DEME_GPU_CALL( + cudaMemcpy(dT->maxTriTriPenetration.data(), hostBuf.data(), nTriGM * sizeof(float), cudaMemcpyHostToDevice)); +} + +void DEMSolver::SetExpandSafetyType(const std::string& insp_type) { + if (insp_type == "auto") { + m_max_v_finder_type = MARGIN_FINDER_TYPE::DEFAULT; + use_user_defined_expand_factor = false; + } else { + DEME_ERROR("Unknown string input \"%s\" for SetExpandSafetyType.", insp_type.c_str()); + } +} + +void DEMSolver::SetUseAngularVelocityMargin(bool use) { + m_use_angvel_margin = use; + m_use_angvel_margin_user_set = true; + if (!sys_initialized) { + return; + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->useAngVelMargin = use ? 1 : 0; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->useAngVelMargin = use ? 1 : 0; + kT->simParams.toDevice(); } } @@ -973,8 +1846,14 @@ void DEMSolver::ChangeFamily(unsigned int ID_from, unsigned int ID_to) { ID_from, ID_to, std::numeric_limits::max()); } - dT->changeFamily(ID_from, ID_to); - kT->changeFamily(ID_from, ID_to); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->changeFamily(ID_from, ID_to); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->changeFamily(ID_from, ID_to); + } } void DEMSolver::SetFamilyFixed(unsigned int ID) { @@ -1334,8 +2213,8 @@ void DEMSolver::SetFamilyPrescribedQuaternion(unsigned int ID, const std::string // Make sure there is return if (!match_whole_word(q_formula, "return")) { DEME_ERROR( - "CorrectFamilyQuaternion call must supply a code string that returns a float4, like 'return " - "make_float4(0,0,0,1)'."); + std::string("CorrectFamilyQuaternion call must supply a code string that returns a float4, like 'return " + "make_float4(0,0,0,1)'.")); } preInfo.oriQ = q_formula; @@ -1497,8 +2376,8 @@ void DEMSolver::CorrectFamilyQuaternion(unsigned int ID, const std::string& q_fo // Make sure there is return if (!match_whole_word(q_formula, "return")) { DEME_ERROR( - "CorrectFamilyQuaternion call must supply a code string that returns a float4, like 'return " - "make_float4(0,0,0,1)'."); + std::string("CorrectFamilyQuaternion call must supply a code string that returns a float4, like 'return " + "make_float4(0,0,0,1)'.")); } preInfo.oriQ = q_formula; @@ -1509,39 +2388,6 @@ void DEMSolver::CorrectFamilyQuaternion(unsigned int ID, const std::string& q_fo m_input_family_prescription.push_back(preInfo); } -void DEMSolver::SetTriWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals) { - assertSysInit("SetTriWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - dT->setTriWildcardValue(geoID, m_geo_wc_num.at(name), vals); -} - -void DEMSolver::SetSphereWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals) { - assertSysInit("SetSphereWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - dT->setSphWildcardValue(geoID, m_geo_wc_num.at(name), vals); -} - -void DEMSolver::SetAnalWildcardValue(bodyID_t geoID, const std::string& name, const std::vector& vals) { - assertSysInit("SetAnalWildcardValue"); - if (m_geo_wc_num.find(name) == m_geo_wc_num.end()) { - DEME_ERROR( - "No geometry wildcard in the force model is named %s.\nIf you need to use it, declare it via " - "SetPerGeometryWildcards in the force model first.", - name.c_str()); - } - dT->setAnalWildcardValue(geoID, m_geo_wc_num.at(name), vals); -} - void DEMSolver::SetOwnerWildcardValue(bodyID_t ownerID, const std::string& name, const std::vector& vals) { assertSysInit("SetOwnerWildcardValue"); if (m_owner_wc_num.find(name) == m_owner_wc_num.end()) { @@ -1550,6 +2396,7 @@ void DEMSolver::SetOwnerWildcardValue(bodyID_t ownerID, const std::string& name, "SetPerOwnerWildcards in the force model first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setOwnerWildcardValue(ownerID, m_owner_wc_num.at(name), vals); } @@ -1561,6 +2408,7 @@ void DEMSolver::SetFamilyContactWildcardValueEither(unsigned int N, const std::s "SetPerContactWildcards in the force model first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyContactWildcardValueEither(N, m_cnt_wc_num.at(name), val); } void DEMSolver::SetFamilyContactWildcardValueBoth(unsigned int N, const std::string& name, float val) { @@ -1571,6 +2419,7 @@ void DEMSolver::SetFamilyContactWildcardValueBoth(unsigned int N, const std::str "SetPerContactWildcards in the force model first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyContactWildcardValueBoth(N, m_cnt_wc_num.at(name), val); } void DEMSolver::SetFamilyContactWildcardValue(unsigned int N1, unsigned int N2, const std::string& name, float val) { @@ -1581,6 +2430,7 @@ void DEMSolver::SetFamilyContactWildcardValue(unsigned int N1, unsigned int N2, "SetPerContactWildcards in the force model first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyContactWildcardValue(N1, N2, m_cnt_wc_num.at(name), val); } void DEMSolver::SetContactWildcardValue(const std::string& name, float val) { @@ -1591,15 +2441,18 @@ void DEMSolver::SetContactWildcardValue(const std::string& name, float val) { "SetPerContactWildcards in the force model first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setContactWildcardValue(m_cnt_wc_num.at(name), val); } void DEMSolver::SetFamilyClumpMaterial(unsigned int N, const std::shared_ptr& mat) { assertSysInit("SetFamilyClumpMaterial"); + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyClumpMaterial(N, mat->load_order); } void DEMSolver::SetFamilyMeshMaterial(unsigned int N, const std::shared_ptr& mat) { assertSysInit("SetFamilyMeshMaterial"); + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyMeshMaterial(N, mat->load_order); } @@ -1611,6 +2464,7 @@ void DEMSolver::SetFamilyOwnerWildcardValue(unsigned int N, const std::string& n "SetPerOwnerWildcards first.", name.c_str()); } + ScopedCudaDevice device_scope(dT->streamInfo.device); dT->setFamilyOwnerWildcardValue(N, m_owner_wc_num.at(name), vals); } @@ -1622,10 +2476,6 @@ void DEMSolver::SetOwnerWildcards(const std::set& wildcards) { m_force_model->SetPerOwnerWildcards(wildcards); } -void DEMSolver::SetGeometryWildcards(const std::set& wildcards) { - m_force_model->SetPerGeometryWildcards(wildcards); -} - void DEMSolver::DisableFamilyOutput(unsigned int ID) { assertSysNotInit("DisableFamilyOutput"); if (ID > std::numeric_limits::max()) { @@ -1821,6 +2671,16 @@ std::shared_ptr DEMSolver::LoadSphereType(float mass, std::vector>(1, material)); } +std::shared_ptr DEMSolver::LoadSphereType(float mass, + float moi, + float radius, + const std::shared_ptr& material) { + float3 I = make_float3(moi); + float3 pos = make_float3(0); + return LoadClumpType(mass, I, std::vector(1, radius), std::vector(1, pos), + std::vector>(1, material)); +} + std::shared_ptr DEMSolver::AddExternalObject() { DEMExternObj an_obj; std::shared_ptr ptr = std::make_shared(std::move(an_obj)); @@ -1856,8 +2716,14 @@ void DEMSolver::DisableContactBetweenFamilies(unsigned int ID1, unsigned int ID2 } else { // If initialized, directly pass this info to workers unsigned int posInMat = locateMaskPair(ID1, ID2); - kT->familyMaskMatrix.setVal(PREVENT_CONTACT, posInMat); - dT->familyMaskMatrix.setVal(PREVENT_CONTACT, posInMat); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->familyMaskMatrix.setVal(PREVENT_CONTACT, posInMat); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->familyMaskMatrix.setVal(PREVENT_CONTACT, posInMat); + } } } @@ -1869,14 +2735,20 @@ void DEMSolver::EnableContactBetweenFamilies(unsigned int ID1, unsigned int ID2) ID1, ID2, std::numeric_limits::max()); } if (!sys_initialized) { - DEME_ERROR( + DEME_ERROR(std::string( "There is no need to call EnableContactBetweenFamilies before system initialization.\nAll families have " - "contacts with each other by default. Just do not disable them if you need that contact."); + "contacts with each other by default. Just do not disable them if you need that contact.")); } else { // If initialized, directly pass this info to workers unsigned int posInMat = locateMaskPair(ID1, ID2); - kT->familyMaskMatrix.setVal(DONT_PREVENT_CONTACT, posInMat); - dT->familyMaskMatrix.setVal(DONT_PREVENT_CONTACT, posInMat); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->familyMaskMatrix.setVal(DONT_PREVENT_CONTACT, posInMat); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->familyMaskMatrix.setVal(DONT_PREVENT_CONTACT, posInMat); + } } } @@ -1889,14 +2761,21 @@ void DEMSolver::SetFamilyExtraMargin(unsigned int N, float extra_size) { DEME_ERROR("You are adding an extra margin of size %.7g, but the size should not be smaller than 0.", extra_size); } - kT->familyExtraMarginSize.setVal(extra_size, N); - dT->familyExtraMarginSize.setVal(extra_size, N); + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->familyExtraMarginSize.setVal(extra_size, N); + } + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->familyExtraMarginSize.setVal(extra_size, N); + } } void DEMSolver::ClearCache() { deallocate_array(cached_input_clump_batches); deallocate_array(cached_extern_objs); deallocate_array(cached_mesh_objs); + deallocate_array(cached_combined_instances); // m_input_no_contact_pairs can be removed, if the system is initialized. After initialization, family mask can be // directly transferred to workers on user call. @@ -1908,6 +2787,7 @@ void DEMSolver::ClearCache() { // Rigth now, there is no way to re-define the following arrays without re-starting the simulation // m_loaded_materials; // m_templates; + // m_mesh_templates; // m_input_family_prescription; // m_no_output_families; // m_family_change_pairs; @@ -1917,7 +2797,7 @@ void DEMSolver::ClearCache() { std::shared_ptr DEMSolver::AddClumps(DEMClumpBatch& input_batch) { // load_order should be its position in the cache array, not nBatchClumpsLoad input_batch.load_order = cached_input_clump_batches.size(); - // But we still need to record a batch loaded + // But we still need to record a batch load operation nBatchClumpsLoad++; cached_input_clump_batches.push_back(std::make_shared(std::move(input_batch))); @@ -1932,7 +2812,7 @@ std::shared_ptr DEMSolver::AddClumps(DEMClumpBatch& input_batch) std::shared_ptr DEMSolver::AddClumps(const std::vector>& input_types, const std::vector& input_xyz) { if (input_types.size() != input_xyz.size()) { - DEME_ERROR("Arrays in the call AddClumps must all have the same length."); + DEME_ERROR(std::string("Arrays in the call AddClumps must all have the same length.")); } size_t nClumps = input_types.size(); // We did not create defaults for families, and if the user did not specify families then they will be added at @@ -1944,26 +2824,71 @@ std::shared_ptr DEMSolver::AddClumps(const std::vector DEMSolver::AddWavefrontMeshObject(DEMMeshConnected& mesh) { +std::shared_ptr DEMSolver::AddMesh(DEMMesh& mesh) { if (mesh.GetNumTriangles() == 0) { - DEME_WARNING("It seems that a mesh contains 0 triangle facet at the time it is loaded."); + DEME_WARNING(std::string("It seems that a mesh contains 0 triangle facet at the time it is loaded.")); + } + if (mesh.mesh_template_mark == NULL_MESH_TEMPLATE_MARK) { + mesh.mesh_template_mark = nMeshTemplateMarks++; + } + if (use_deme2_mesh_behavior) { + mesh.SetEachTriangleAsPatch(); + } + if (!mesh.mass_specified || !mesh.moi_specified) { + double volume = 0.0; + float3 center = make_float3(0, 0, 0); + float3 unit_inertia = make_float3(0, 0, 0); + mesh.ComputeMassProperties(volume, center, unit_inertia); + if (volume > 1e-20 && std::isfinite(volume) && length(unit_inertia) > 1e-20 && + std::isfinite(length(unit_inertia))) { + if (!mesh.mass_specified) { + mesh.mass = static_cast(volume); + } + if (!mesh.moi_specified) { + const float scale = static_cast(mesh.mass / volume); + mesh.MOI = unit_inertia * scale; + } + } else if (!mesh.mass_specified || !mesh.moi_specified) { + DEME_WARNING( + "Mesh %s requested auto mass/MOI, but geometric mass properties could not be computed (volume: %.9g, " + "unit MOI magnitude: %.9g).", + mesh.filename.empty() ? "" : mesh.filename.c_str(), volume, length(unit_inertia)); + } + } + if (!mesh.IsShell()) { + size_t boundary_edges = 0; + size_t nonmanifold_edges = 0; + if (!mesh.IsWatertight(&boundary_edges, &nonmanifold_edges)) { + const char* mesh_name = mesh.filename.empty() ? "" : mesh.filename.c_str(); + DEME_WARNING( + "Mesh %s is not watertight (boundary edges: %zu, non-manifold edges: %zu). Volume/MOI may be " + "inaccurate.", + mesh_name, boundary_edges, nonmanifold_edges); + } } // load_order should be its position in the cache array, not nTriObjLoad mesh.load_order = cached_mesh_objs.size(); - // But we still need to record a tri-mesh loaded + // But we still need to record a tri-mesh load operation nTriObjLoad++; - cached_mesh_objs.push_back(std::make_shared(std::move(mesh))); + cached_mesh_objs.push_back(std::make_shared(std::move(mesh))); return cached_mesh_objs.back(); } -std::shared_ptr DEMSolver::AddWavefrontMeshObject(const std::string& filename, - const std::shared_ptr& mat, - bool load_normals, - bool load_uv) { - DEMMeshConnected mesh; - bool flag = mesh.LoadWavefrontMesh(filename, load_normals, load_uv); +std::shared_ptr DEMSolver::AddShellMesh(DEMMesh& mesh, float shell_thickness) { + // Shell meshes reuse the regular mesh loading/cache path; the finite thickness is stored on the mesh and later + // flattened into per-owner shell metadata for kT binning and dT primitive force kernels. + mesh.SetShellThickness(shell_thickness); + return AddMesh(mesh); +} + +std::shared_ptr DEMSolver::AddWavefrontMeshObject(const std::string& filename, + const std::shared_ptr& mat, + bool load_normals, + bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); if (!flag) { DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); } @@ -1971,129 +2896,575 @@ std::shared_ptr DEMSolver::AddWavefrontMeshObject(const std::s return AddWavefrontMeshObject(mesh); } -std::shared_ptr DEMSolver::AddWavefrontMeshObject(const std::string& filename, - bool load_normals, - bool load_uv) { - DEMMeshConnected mesh; - bool flag = mesh.LoadWavefrontMesh(filename, load_normals, load_uv); +std::shared_ptr DEMSolver::AddWavefrontShellObject(const std::string& filename, + const std::shared_ptr& mat, + float shell_thickness, + bool load_normals, + bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); + if (!flag) { + DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); + } + mesh.SetMaterial(mat); + return AddShellMesh(mesh, shell_thickness); +} + +std::shared_ptr DEMSolver::AddWavefrontMeshObject(const std::string& filename, + bool load_normals, + bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); if (!flag) { DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); } return AddWavefrontMeshObject(mesh); } +std::shared_ptr DEMSolver::AddWavefrontShellObject(const std::string& filename, + float shell_thickness, + bool load_normals, + bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); + if (!flag) { + DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); + } + return AddShellMesh(mesh, shell_thickness); +} + +std::shared_ptr DEMSolver::LoadMeshType(DEMMesh& mesh) { + if (mesh.GetNumTriangles() == 0) { + DEME_WARNING(std::string("It seems that a mesh template contains 0 triangle facet at the time it is loaded.")); + } + if (use_deme2_mesh_behavior) { + mesh.SetEachTriangleAsPatch(); + } + mesh.mesh_template_mark = nMeshTemplateMarks++; + + // Store as a template (not in cached_mesh_objs) + std::shared_ptr ptr = std::make_shared(std::move(mesh)); + m_mesh_templates.push_back(ptr); + nMeshTemplateLoad++; + return m_mesh_templates.back(); +} + +std::shared_ptr DEMSolver::LoadMeshType(const std::string& filename, + const std::shared_ptr& mat, + bool load_normals, + bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); + if (!flag) { + DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); + } + mesh.SetMaterial(mat); + return LoadMeshType(mesh); +} + +std::shared_ptr DEMSolver::LoadMeshType(const std::string& filename, bool load_normals, bool load_uv) { + DEMMesh mesh; + bool flag = loadMeshByExtension(mesh, filename, load_normals, load_uv); + if (!flag) { + DEME_ERROR("Failed to load in mesh file %s.", filename.c_str()); + } + return LoadMeshType(mesh); +} + +std::shared_ptr DEMSolver::AddMeshFromTemplate(const std::shared_ptr& mesh_template, + const float3& init_pos) { + if (!mesh_template) { + DEME_ERROR("AddMeshFromTemplate received a null mesh template."); + } + if (mesh_template->mesh_template_mark == NULL_MESH_TEMPLATE_MARK) { + DEME_ERROR( + "AddMeshFromTemplate expects a mesh template returned by LoadMeshType. Use LoadMeshType first, then add " + "mesh particles with AddMeshFromTemplate so repeated instances share template-level mass/MOI " + "jitification."); + } + // Create a copy of the template + DEMMesh mesh = *mesh_template; + + // Set the initial position + mesh.SetInitPos(init_pos); + + // Add the mesh instance to the simulation + return AddMesh(mesh); +} + +std::shared_ptr DEMSolver::LoadCombinedClumpType( + const std::vector>& component_templates, + const std::vector& component_rel_pos, + const std::vector& component_rel_oriQ, + size_t master_component) { + assertSysNotInit("LoadCombinedClumpType"); + if (component_templates.empty()) { + DEME_ERROR("LoadCombinedClumpType requires at least one component template."); + } + if (component_templates.size() != component_rel_pos.size()) { + DEME_ERROR( + "LoadCombinedClumpType requires component template and relative-position arrays to have equal size."); + } + if (!component_rel_oriQ.empty() && component_rel_oriQ.size() != component_templates.size()) { + DEME_ERROR("LoadCombinedClumpType requires relative-orientation array size to match component count."); + } + if (master_component >= component_templates.size()) { + DEME_ERROR("LoadCombinedClumpType master component index %zu is out of range [0, %zu).", master_component, + component_templates.size()); + } + + auto ptr = std::make_shared(); + ptr->member_type = OWNER_TYPE::CLUMP; + ptr->master_member = master_component; + ptr->clump_templates = component_templates; + ptr->rel_pos.resize(component_templates.size()); + ptr->rel_oriQ.resize(component_templates.size(), make_float4(0, 0, 0, 1)); + if (!component_rel_oriQ.empty()) { + ptr->rel_oriQ = component_rel_oriQ; + } + for (size_t i = 0; i < ptr->rel_oriQ.size(); i++) { + ptr->rel_oriQ[i] = quatNormalizeSafe(ptr->rel_oriQ[i]); + } + + const float3 master_pos = component_rel_pos[master_component]; + const float4 master_q = ptr->rel_oriQ[master_component]; + const float4 master_q_conj = quatConjugate(master_q); + for (size_t i = 0; i < component_rel_pos.size(); i++) { + float3 rel = component_rel_pos[i]; + rel -= master_pos; + applyOriQToVector3(rel, master_q_conj); + ptr->rel_pos[i] = rel; + ptr->rel_oriQ[i] = quatNormalizeSafe(hostHamiltonProduct(master_q_conj, ptr->rel_oriQ[i])); + } + ptr->rel_pos[master_component] = make_float3(0); + ptr->rel_oriQ[master_component] = make_float4(0, 0, 0, 1); + + ptr->load_order = m_combined_templates.size(); + m_combined_templates.push_back(ptr); + nCombinedTemplateLoad++; + m_combined_runtime_dirty = true; + return m_combined_templates.back(); +} + +std::shared_ptr DEMSolver::LoadCombinedMeshType( + const std::vector>& component_templates, + const std::vector& component_rel_pos, + const std::vector& component_rel_oriQ, + size_t master_component) { + assertSysNotInit("LoadCombinedMeshType"); + if (component_templates.empty()) { + DEME_ERROR("LoadCombinedMeshType requires at least one component template."); + } + if (component_templates.size() != component_rel_pos.size()) { + DEME_ERROR("LoadCombinedMeshType requires component template and relative-position arrays to have equal size."); + } + if (!component_rel_oriQ.empty() && component_rel_oriQ.size() != component_templates.size()) { + DEME_ERROR("LoadCombinedMeshType requires relative-orientation array size to match component count."); + } + if (master_component >= component_templates.size()) { + DEME_ERROR("LoadCombinedMeshType master component index %zu is out of range [0, %zu).", master_component, + component_templates.size()); + } + + auto ptr = std::make_shared(); + ptr->member_type = OWNER_TYPE::MESH; + ptr->master_member = master_component; + ptr->mesh_templates = component_templates; + ptr->rel_pos.resize(component_templates.size()); + ptr->rel_oriQ.resize(component_templates.size(), make_float4(0, 0, 0, 1)); + if (!component_rel_oriQ.empty()) { + ptr->rel_oriQ = component_rel_oriQ; + } + for (size_t i = 0; i < ptr->rel_oriQ.size(); i++) { + ptr->rel_oriQ[i] = quatNormalizeSafe(ptr->rel_oriQ[i]); + } + + const float3 master_pos = component_rel_pos[master_component]; + const float4 master_q = ptr->rel_oriQ[master_component]; + const float4 master_q_conj = quatConjugate(master_q); + for (size_t i = 0; i < component_rel_pos.size(); i++) { + float3 rel = component_rel_pos[i]; + rel -= master_pos; + applyOriQToVector3(rel, master_q_conj); + ptr->rel_pos[i] = rel; + ptr->rel_oriQ[i] = quatNormalizeSafe(hostHamiltonProduct(master_q_conj, ptr->rel_oriQ[i])); + } + ptr->rel_pos[master_component] = make_float3(0); + ptr->rel_oriQ[master_component] = make_float4(0, 0, 0, 1); + + ptr->load_order = m_combined_templates.size(); + m_combined_templates.push_back(ptr); + nCombinedTemplateLoad++; + m_combined_runtime_dirty = true; + return m_combined_templates.back(); +} + +std::shared_ptr DEMSolver::AddCombinedFromTemplate( + const std::shared_ptr& combined_template, + const std::vector& init_pos, + const std::vector& init_oriQ) { + assertSysNotInit("AddCombinedFromTemplate"); + if (!combined_template) { + DEME_ERROR("AddCombinedFromTemplate received a null combined template handle."); + } + if (init_pos.empty()) { + DEME_ERROR("AddCombinedFromTemplate received an empty position vector."); + } + + const size_t n_instances = init_pos.size(); + // If orientations not supplied, default to identity quaternion for each instance. + std::vector oriQ_vec = init_oriQ; + if (oriQ_vec.empty()) { + oriQ_vec.assign(n_instances, make_float4(0, 0, 0, 1)); + } + if (oriQ_vec.size() != n_instances) { + DEME_ERROR("AddCombinedFromTemplate: init_pos and init_oriQ must have the same length (got %zu and %zu).", + n_instances, oriQ_vec.size()); + } + + const size_t n_members = (combined_template->member_type == OWNER_TYPE::CLUMP) + ? combined_template->clump_templates.size() + : combined_template->mesh_templates.size(); + if (n_members == 0) { + DEME_ERROR("AddCombinedFromTemplate encountered a combined template with zero components."); + } + if (combined_template->rel_pos.size() != n_members || combined_template->rel_oriQ.size() != n_members) { + DEME_ERROR("AddCombinedFromTemplate found inconsistent fixed-transform arrays in the combined template."); + } + if (combined_template->master_member >= n_members) { + DEME_ERROR("AddCombinedFromTemplate master component index %zu is out of range [0, %zu).", + combined_template->master_member, n_members); + } + + auto inst = std::make_shared(); + inst->type = combined_template; + inst->n_instances = n_instances; + const size_t total_members = n_instances * n_members; + inst->member_mass.resize(total_members, 0.f); + inst->member_moi.resize(total_members, make_float3(0)); + inst->master_equiv_mass.resize(n_instances, 0.f); + inst->master_equiv_moi.resize(n_instances, make_float3(0)); + inst->master_owner_ids.resize(n_instances, NULL_BODYID); + inst->member_objs.reserve(total_members); + + // Outer: combined template instances; inner: template members. + // This way, one combined owner's member owners are together in memory. + for (size_t k = 0; k < n_instances; k++) { + const float4 init_q = quatNormalizeSafe(oriQ_vec[k]); + + for (size_t i = 0; i < n_members; i++) { + const size_t flat_idx = k * n_members + i; + float3 world_pos = combined_template->rel_pos[i]; + applyFrameTransformLocalToGlobal(world_pos, init_pos[k], init_q); + const float4 world_q = quatNormalizeSafe(hostHamiltonProduct(init_q, combined_template->rel_oriQ[i])); + + if (combined_template->member_type == OWNER_TYPE::CLUMP) { + auto clump_type = combined_template->clump_templates[i]; + auto batch = AddClumps(clump_type, world_pos); + batch->SetOriQ(world_q); + inst->member_objs.push_back(batch); + inst->member_mass[flat_idx] = clump_type->GetMass(); + inst->member_moi[flat_idx] = clump_type->GetMOI(); + } else if (combined_template->member_type == OWNER_TYPE::MESH) { + DEMMesh mesh = *(combined_template->mesh_templates[i]); + mesh.SetInitPos(world_pos); + mesh.SetInitQuat(world_q); + auto mesh_inst = AddMesh(mesh); + inst->member_objs.push_back(mesh_inst); + inst->member_mass[flat_idx] = mesh.mass; + inst->member_moi[flat_idx] = mesh.MOI; + } else { + DEME_ERROR("AddCombinedFromTemplate only supports same-type CLUMP or MESH templates."); + } + inst->master_equiv_mass[k] += inst->member_mass[flat_idx]; + inst->master_equiv_moi[k] += inst->member_moi[flat_idx]; + } + } + + cached_combined_instances.push_back(inst); + m_combined_runtime_dirty = true; + return cached_combined_instances.back(); +} + +void DEMSolver::SetAllowIntraCombinedOwnerContacts(bool allow) { + m_allow_intra_combined_owner_contacts = allow; + if (sys_initialized) { + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->simParams->allowIntraCombinedOwnerContacts = allow ? 1 : 0; + dT->simParams.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->simParams->allowIntraCombinedOwnerContacts = allow ? 1 : 0; + kT->simParams.toDevice(); + } + } +} + +bool DEMSolver::GetCombinedInstanceInfo(size_t combined_instance_id, + bodyID_t& master_owner_id, + std::vector& member_owner_ids, + std::vector& member_rel_pos, + std::vector& member_rel_oriQ) { + if (combined_instance_id >= cached_combined_instances.size()) { + return false; + } + // Ensure owner IDs are resolved (and runtime mapping refreshed if needed) before returning metadata. + resolveCombinedOwners(); + const auto& inst = cached_combined_instances[combined_instance_id]; + if (!inst->owners_resolved || inst->n_instances == 0) { + return false; + } + // Return info for the first instantiation in this batch entry. + master_owner_id = inst->master_owner_ids[0]; + const size_t n_members_per_inst = (inst->type->member_type == OWNER_TYPE::CLUMP) + ? inst->type->clump_templates.size() + : inst->type->mesh_templates.size(); + if (n_members_per_inst > inst->member_owner_ids.size()) { + return false; + } + member_owner_ids.assign(inst->member_owner_ids.begin(), + inst->member_owner_ids.begin() + static_cast(n_members_per_inst)); + member_rel_pos = inst->type->rel_pos; + member_rel_oriQ = inst->type->rel_oriQ; + return true; +} + std::shared_ptr DEMSolver::CreateInspector(const std::string& quantity) { - DEMInspector insp(this, this->dT.get(), quantity); - m_inspectors.push_back(std::make_shared(std::move(insp))); + m_inspectors.push_back(std::make_shared(this, dT.get(), quantity)); return m_inspectors.back(); } std::shared_ptr DEMSolver::CreateInspector(const std::string& quantity, const std::string& region) { - DEMInspector insp(this, this->dT.get(), quantity, region); - m_inspectors.push_back(std::make_shared(std::move(insp))); + m_inspectors.push_back(std::make_shared(this, dT.get(), quantity, region)); return m_inspectors.back(); } void DEMSolver::WriteSphereFile(const std::string& outfilename) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); switch (m_out_format) { -#ifdef DEME_USE_CHPF - case (OUTPUT_FORMAT::CHPF): { - std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); - dT->writeSpheresAsChpf(ptFile); - ptFile.close(); - break; - } -#endif case (OUTPUT_FORMAT::CSV): { - std::ofstream ptFile(outfilename, std::ios::out); - dT->writeSpheresAsCsv(ptFile); - ptFile.close(); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateClumpHighOrderInfoToHost(); + dT->migrateOwnerWildcardToHost(); + dT->migrateSphGeoWildcardToHost(); + m_output_thread = std::thread([this, outfilename]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeSpheresAsCsvFromHost(ptFile); + }); break; } case (OUTPUT_FORMAT::BINARY): { // std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); //// TODO: Implement it - std::ofstream ptFile(outfilename, std::ios::out); - DEME_WARNING("Binary sphere output is not implemented yet, using CSV..."); - dT->writeSpheresAsCsv(ptFile); - ptFile.close(); + DEME_WARNING(std::string("Binary sphere output is not implemented yet, using CSV...")); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateClumpHighOrderInfoToHost(); + dT->migrateOwnerWildcardToHost(); + dT->migrateSphGeoWildcardToHost(); + m_output_thread = std::thread([this, outfilename]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeSpheresAsCsvFromHost(ptFile); + }); + break; + } + case (OUTPUT_FORMAT::VTK): { + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateClumpHighOrderInfoToHost(); + dT->migrateOwnerWildcardToHost(); + dT->migrateSphGeoWildcardToHost(); + m_output_thread = std::thread([this, outfilename]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeSpheresAsVtkFromHost(ptFile); + }); break; } default: - DEME_ERROR("Sphere output file format is unknown. Please set it via SetOutputFormat."); + DEME_ERROR(std::string("Sphere output file format is unknown. Please set it via SetOutputFormat.")); } } +void DEMSolver::WriteAnalyticalFile(const std::string& outfilename, unsigned int circumferential_resolution) const { + if (circumferential_resolution < 3) { + DEME_ERROR("WriteAnalyticalFile circumferential resolution must be at least 3, not %u.", + circumferential_resolution); + } + + ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + + // Snapshot current owner transforms before launching the asynchronous writer. Component definitions are setup-time + // data, while an analytical owner's pose can change on every dynamics step. + std::vector components; + components.reserve(m_anal_output_definitions.size()); + for (size_t i = 0; i < m_anal_output_definitions.size(); i++) { + const auto& definition = m_anal_output_definitions[i]; + const bodyID_t owner = dT->ownerAnalBody[i]; + const family_t family = dT->familyID[owner]; + if (dT->familiesNoOutput.find(family) != dT->familiesNoOutput.end()) { + continue; + } + + float3 owner_pos; + voxelIDToPosition( + owner_pos.x, owner_pos.y, owner_pos.z, dT->voxelID[owner], dT->locX[owner], dT->locY[owner], + dT->locZ[owner], dT->simParams->nvXp2, dT->simParams->nvYp2, dT->simParams->voxelSize, dT->simParams->l); + owner_pos += make_float3(dT->simParams->LBFX, dT->simParams->LBFY, dT->simParams->LBFZ); + const float4 owner_ori = make_float4(dT->oriQx[owner], dT->oriQy[owner], dT->oriQz[owner], dT->oriQw[owner]); + float3 component_pos = definition.position; + applyFrameTransformLocalToGlobal(component_pos, owner_pos, owner_ori); + float3 component_axis = definition.axis; + applyOriQToVector3(component_axis.x, component_axis.y, component_axis.z, dT->oriQw[owner], + dT->oriQx[owner], dT->oriQy[owner], dT->oriQz[owner]); + components.push_back({definition.type, component_pos, component_axis, definition.size_1, definition.size_2, + definition.size_3, definition.normal_sign, owner, family}); + } + + const float3 domain_min = m_user_box_min; + const float3 domain_max = m_user_box_max; + m_output_thread = std::thread( + [outfilename, components = std::move(components), domain_min, domain_max, circumferential_resolution]() { + std::ofstream file(outfilename, std::ios::out); + writeAnalyticalAsVtk(file, components, domain_min, domain_max, circumferential_resolution); + }); +} + void DEMSolver::WriteClumpFile(const std::string& outfilename, unsigned int accuracy) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); switch (m_out_format) { -#ifdef DEME_USE_CHPF - case (OUTPUT_FORMAT::CHPF): { - std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); - dT->writeClumpsAsChpf(ptFile, accuracy); - ptFile.close(); - break; - } -#endif case (OUTPUT_FORMAT::CSV): { - std::ofstream ptFile(outfilename, std::ios::out); - dT->writeClumpsAsCsv(ptFile, accuracy); - ptFile.close(); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateClumpHighOrderInfoToHost(); + dT->migrateOwnerWildcardToHost(); + m_output_thread = std::thread([this, outfilename, accuracy]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeClumpsAsCsvFromHost(ptFile, accuracy); + }); break; } case (OUTPUT_FORMAT::BINARY): { // std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); //// TODO: Implement it - std::ofstream ptFile(outfilename, std::ios::out); - DEME_WARNING("Binary clump output is not implemented yet, using CSV..."); - dT->writeClumpsAsCsv(ptFile, accuracy); - ptFile.close(); + DEME_WARNING(std::string("Binary clump output is not implemented yet, using CSV...")); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateClumpHighOrderInfoToHost(); + dT->migrateOwnerWildcardToHost(); + m_output_thread = std::thread([this, outfilename, accuracy]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeClumpsAsCsvFromHost(ptFile, accuracy); + }); break; } default: - DEME_ERROR("Clump output file format is unknown. Please set it via SetOutputFormat."); + DEME_ERROR(std::string("Clump output file format is unknown. Please set it via SetOutputFormat.")); } } void DEMSolver::WriteContactFile(const std::string& outfilename, float force_thres) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); if (no_recording_contact_forces) { - DEME_WARNING( + DEME_WARNING(std::string( "The solver is instructed to not record contact force info, so no work is done in a WriteContactFile " - "call."); + "call.")); return; } switch (m_cnt_out_format) { case (OUTPUT_FORMAT::CSV): { - std::ofstream ptFile(outfilename, std::ios::out); - dT->writeContactsAsCsv(ptFile, force_thres); - ptFile.close(); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateContactInfoToHost(); + m_output_thread = std::thread([this, outfilename, force_thres]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeContactsAsCsvFromHost(ptFile, force_thres); + }); break; } case (OUTPUT_FORMAT::BINARY): { // std::ofstream ptFile(outfilename, std::ios::out | std::ios::binary); //// TODO: Implement it - DEME_WARNING("Binary contact pair output is not implemented yet, using CSV..."); - std::ofstream ptFile(outfilename, std::ios::out); - dT->writeContactsAsCsv(ptFile, force_thres); - ptFile.close(); + DEME_WARNING(std::string("Binary contact pair output is not implemented yet, using CSV...")); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + dT->migrateContactInfoToHost(); + m_output_thread = std::thread([this, outfilename, force_thres]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeContactsAsCsvFromHost(ptFile, force_thres); + }); break; } default: DEME_ERROR( - "Contact pair output file format is unknown or not implemented. Please re-set it via SetOutputFormat."); + std::string("Contact pair output file format is unknown or not implemented. Please re-set it via " + "SetOutputFormat.")); } } void DEMSolver::WriteMeshFile(const std::string& outfilename) const { + ScopedCudaDevice device_scope(dT->streamInfo.device); + WaitForPendingOutput(); switch (m_mesh_out_format) { case (MESH_FORMAT::VTK): { - std::ofstream ptFile(outfilename, std::ios::out); - dT->writeMeshesAsVtk(ptFile); - ptFile.close(); + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + const unsigned int flags = dT->solverFlags.meshOutFlags; + const unsigned int high_order_flags = static_cast(MESH_OUTPUT_CONTENT::ABSV) | + MESH_OUTPUT_CONTENT::VEL | MESH_OUTPUT_CONTENT::ANG_VEL | + MESH_OUTPUT_CONTENT::ABS_ACC | MESH_OUTPUT_CONTENT::ACC | + MESH_OUTPUT_CONTENT::ANG_ACC; + if (flags & high_order_flags) { + dT->migrateClumpHighOrderInfoToHost(); + } + if (flags & static_cast(MESH_OUTPUT_CONTENT::OWNER_WILDCARD)) { + dT->migrateOwnerWildcardToHost(); + } + if (flags & static_cast(MESH_OUTPUT_CONTENT::GEO_WILDCARD)) { + dT->migrateTriGeoWildcardToHost(); + } + m_output_thread = std::thread([this, outfilename]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeMeshesAsVtkFromHost(ptFile); + }); + break; + } + case (MESH_FORMAT::STL): { + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + m_output_thread = std::thread([this, outfilename]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeMeshesAsStlFromHost(ptFile); + }); + break; + } + case (MESH_FORMAT::PLY): { + dT->migrateFamilyToHost(); + dT->migrateClumpPosInfoToHost(); + const bool patch_colors = m_mesh_out_ply_patch_colors; + m_output_thread = std::thread([this, outfilename, patch_colors]() { + std::ofstream ptFile(outfilename, std::ios::out); + dT->writeMeshesAsPlyFromHost(ptFile, patch_colors); + }); break; } default: - DEME_ERROR( - "Mesh output file format is unknown or not implemented. Please re-set it via SetMeshOutputFormat."); + DEME_ERROR(std::string( + "Mesh output file format is unknown or not implemented. Please re-set it via SetMeshOutputFormat.")); + } +} + +void DEMSolver::WaitForPendingOutput() const { + if (m_output_thread.joinable()) { + m_output_thread.join(); } } @@ -2107,14 +3478,20 @@ size_t DEMSolver::ChangeClumpFamily(unsigned int fam_num, size_t count = 0; // And get those device-major data from device + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (dT->solverFlags.canFamilyChangeOnDevice) { + dT->familyID.toHost(); + } + dT->voxelID.toHost(); + dT->locX.toHost(); + dT->locY.toHost(); + dT->locZ.toHost(); + } if (dT->solverFlags.canFamilyChangeOnDevice) { - dT->familyID.toHost(); + ScopedCudaDevice device_scope(kT->streamInfo.device); kT->familyID.toHost(); } - dT->voxelID.toHost(); - dT->locX.toHost(); - dT->locY.toHost(); - dT->locZ.toHost(); for (bodyID_t ownerID = 0; ownerID < nOwnerBodies; ownerID++) { // ownerTypes has no way to change on device @@ -2150,8 +3527,14 @@ size_t DEMSolver::ChangeClumpFamily(unsigned int fam_num, } } - dT->familyID.toDevice(); - kT->familyID.toDevice(); + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->familyID.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->familyID.toDevice(); + } return count; } @@ -2159,6 +3542,7 @@ size_t DEMSolver::ChangeClumpFamily(unsigned int fam_num, // of the required simulation information such as the scale of the problem domain, and makes sure these info live in // GPU memory. void DEMSolver::Initialize(bool dry_run) { + ++dT->visualizationRevision; // A few checks first validateUserInputs(); @@ -2197,12 +3581,21 @@ void DEMSolver::Initialize(bool dry_run) { // Initialization is critical dT->announceCritical(); - // Always clear cache after init - ClearCache(); - //// TODO: Give a warning if sys_initialized is true and the system is re-initialized: in that case, the user should /// know what they are doing sys_initialized = true; + m_combined_runtime_dirty = true; + resolveCombinedOwners(0); + + // Always clear cache after init + ClearCache(); + + // m_gpu_timers_enabled defaults to true, so create the corresponding CUDA events once the initialized solver is + // ready to run. Otherwise the public flag can report enabled timing while StartGpuTimer/StopGpuTimer are no-ops, + // producing mostly zero dT GPU timing statistics unless the user explicitly calls SetGPUTimersEnabled(true). + if (m_gpu_timers_enabled) { + SetGPUTimersEnabled(true); + } if (dry_run) { // Do a dry-run: It establishes contact pairs. It helps to locate obvious problems at the start (like, too many @@ -2213,6 +3606,17 @@ void DEMSolver::Initialize(bool dry_run) { } void DEMSolver::ShowTimingStats() { + // If accumulation is deferred, flush any pending GPU timer spans before reading values. + if (m_gpu_timers_enabled) { + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->timers.FlushGpuTimers(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->timers.FlushGpuTimers(); + } + } std::vector kT_timer_names, dT_timer_names; std::vector kT_timer_vals, dT_timer_vals; double kT_total_time, dT_total_time; @@ -2239,6 +3643,31 @@ void DEMSolver::ShowTimingStats() { DEME_PRINTF("--------------------------\n"); } +void DEMSolver::SetGPUTimersEnabled(bool enabled) { + m_gpu_timers_enabled = enabled; + + // SolverTimers uses cudaEventCreate/Destroy, which are device-scoped. + if (enabled) { + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->timers.EnableGpuTimers(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->timers.EnableGpuTimers(); + } + } else { + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->timers.DestroyGpuEvents(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->timers.DestroyGpuEvents(); + } + } +} + void DEMSolver::ClearTimingStats() { kT->resetTimers(); dT->resetTimers(); @@ -2256,6 +3685,8 @@ void DEMSolver::ReleaseFlattenedArrays() { deallocate_array(m_input_mesh_obj_xyz); deallocate_array(m_input_mesh_obj_rot); deallocate_array(m_input_mesh_obj_family); + deallocate_array(m_input_mesh_obj_convex); + deallocate_array(m_input_mesh_obj_never_winner); deallocate_array(m_unique_family_prescription); deallocate_array(m_input_clump_family); @@ -2270,8 +3701,13 @@ void DEMSolver::ReleaseFlattenedArrays() { deallocate_array(m_anal_normals); deallocate_array(m_mesh_facet_owner); - deallocate_array(m_mesh_facet_materials); + deallocate_array(m_mesh_facet_patch); + deallocate_array(m_mesh_facet_neighbor1); + deallocate_array(m_mesh_facet_neighbor2); + deallocate_array(m_mesh_facet_neighbor3); deallocate_array(m_mesh_facets); + deallocate_array(m_mesh_patch_owner); + deallocate_array(m_mesh_patch_materials); deallocate_array(m_template_clump_mass); deallocate_array(m_template_clump_moi); @@ -2286,6 +3722,9 @@ void DEMSolver::ReleaseFlattenedArrays() { deallocate_array(m_mesh_obj_mass); deallocate_array(m_mesh_obj_moi); + deallocate_array(m_mesh_mass_jit); + deallocate_array(m_mesh_moi_jit); + deallocate_array(m_mesh_mass_offsets); nExtraContacts = 0; } @@ -2325,8 +3764,11 @@ void DEMSolver::UpdateSimParams() { std::thread dT_build([&]() { DEME_GPU_CALL(cudaSetDevice(dT->streamInfo.device)); // Jitify max vel finder, in case the policy there changed - m_approx_max_vel_func->Initialize(m_subs, m_jitify_options, true); - dT->approxMaxVelFunc = m_approx_max_vel_func; + m_approx_vel_func->Initialize(m_subs, m_jitify_options, true); + dT->approxVelFunc = m_approx_vel_func; + // Jitify angular velocity magnitude finder + m_approx_angvel_func->Initialize(m_subs, m_jitify_options, true); + dT->approxAngVelFunc = m_approx_angvel_func; // Updating sim environment is critical dT->announceCritical(); }); @@ -2334,43 +3776,586 @@ void DEMSolver::UpdateSimParams() { } void DEMSolver::UpdateStepSize(double ts) { - m_ts_size = ts; - // We for now store ts as float on devices... - dT->simParams->h = ts; - kT->simParams->h = ts; - // dT->simParams.syncMemberToDevice(offsetof(DEMSimParams, h)); - // kT->simParams.syncMemberToDevice(offsetof(DEMSimParams, h)); + SetTimeStepSize(ts); +} + +bool DEMSolver::findOwnerTriangleRange(bodyID_t ownerID, size_t& tri_start, size_t& tri_count) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + tri_start = 0; + tri_count = 0; + dT->ownerTriMesh.toHost(); + + bool found = false; + size_t last_tri = 0; + bool contiguous = true; + for (size_t tri = 0; tri < dT->ownerTriMesh.size(); tri++) { + if (dT->ownerTriMesh[tri] != ownerID) { + continue; + } + if (!found) { + found = true; + tri_start = tri; + last_tri = tri; + } else { + if (tri != last_tri + 1) { + contiguous = false; + } + last_tri = tri; + } + tri_count++; + } + + if (!found) { + return false; + } + if (!contiguous) { + DEME_ERROR("Mesh owner %zu has non-contiguous triangle IDs; mesh deformation update expects contiguous layout.", + (size_t)ownerID); + } + return true; +} + +void DEMSolver::refreshTrianglePVTrackingOwners() { + ScopedCudaDevice device_scope(dT->streamInfo.device); + std::vector merged; + merged.reserve(m_user_tri_pv_tracking_owners.size() + m_mesh_wear_models.size()); + std::unordered_set seen; + seen.reserve(merged.capacity() + 1); + + for (bodyID_t owner : m_user_tri_pv_tracking_owners) { + if (seen.insert(owner).second) { + merged.push_back(owner); + } + } + for (const auto& kv : m_mesh_wear_models) { + if (seen.insert(kv.first).second) { + merged.push_back(kv.first); + } + } + + if (merged.empty()) { + dT->disableTrianglePVTracking(); + } else { + dT->configureTrianglePVTracking(merged); + } + m_last_tri_pv_snapshot.clear(); +} + +void DEMSolver::cacheTrackedTrianglePVWindow() { + ScopedCudaDevice device_scope(dT->streamInfo.device); + m_last_tri_pv_snapshot.clear(); + if (!dT->triPVTrackingEnabled) { + return; + } + + if (dT->triPVWindowSteps > 0) { + dT->triPVAccumP.toHost(); + dT->triPVAccumPV.toHost(); + } + const float inv_steps = (dT->triPVWindowSteps > 0) ? (1.f / static_cast(dT->triPVWindowSteps)) : 0.f; + + for (const auto& kv : dT->triPVOwnerToSlot) { + const bodyID_t owner = kv.first; + const size_t slot = kv.second; + if (slot >= dT->triPVOwnerOffsets.size() || slot >= dT->triPVOwnerCounts.size()) { + continue; + } + const size_t offset = dT->triPVOwnerOffsets[slot]; + const size_t count = dT->triPVOwnerCounts[slot]; + TrianglePVSnapshot snap; + snap.avgP.assign(count, 0.f); + snap.avgV.assign(count, 0.f); + snap.avgPV.assign(count, 0.f); + if (count > 0 && dT->triPVWindowSteps > 0) { + for (size_t i = 0; i < count; i++) { + float p = dT->triPVAccumP[offset + i] * inv_steps; + float pv = dT->triPVAccumPV[offset + i] * inv_steps; + if (!std::isfinite(p) || p < 0.f) { + p = 0.f; + } + if (!std::isfinite(pv) || pv < 0.f) { + pv = 0.f; + } + float v = (p > DEME_TINY_FLOAT) ? (pv / p) : 0.f; + if (!std::isfinite(v) || v < 0.f) { + v = 0.f; + } + snap.avgP[i] = p; + snap.avgV[i] = v; + snap.avgPV[i] = pv; + } + } + m_last_tri_pv_snapshot.emplace(owner, std::move(snap)); + } +} + +bool DEMSolver::applyMeshWearModel(bodyID_t ownerID, MeshWearModelState& model) { + auto mesh_it = m_owner_mesh_map.find(ownerID); + if (mesh_it == m_owner_mesh_map.end()) { + DEME_ERROR("Wear model owner %zu is not a mesh owner.", (size_t)ownerID); + } + auto& mesh = m_meshes.at(mesh_it->second); + const auto& faces = mesh->GetIndicesVertexes(); + const size_t n_tri = faces.size(); + if (n_tri != model.tri_count) { + DEME_ERROR("Wear model triangle count mismatch for owner %zu (expected %zu, got %zu).", (size_t)ownerID, + model.tri_count, n_tri); + } + if (model.pending_depth.size() != n_tri) { + DEME_ERROR("Wear depth buffer size mismatch for owner %zu (expected %zu, got %zu).", (size_t)ownerID, n_tri, + model.pending_depth.size()); + } + + auto& vertices = mesh->GetCoordsVertices(); + const bool use_canon_groups = (model.vertex_to_canon.size() == vertices.size()) && (model.n_canon_vertices > 0); + const size_t n_vertex_groups = use_canon_groups ? model.n_canon_vertices : vertices.size(); + const float depth_cap = (model.max_depth_per_update > 0.f && std::isfinite(model.max_depth_per_update)) + ? model.max_depth_per_update + : std::numeric_limits::infinity(); + std::vector weighted_disp(n_vertex_groups, make_float3(0.f)); + std::vector weighted_sum(n_vertex_groups, 0.f); + std::vector node_updates(vertices.size(), make_float3(0.f)); + + auto cross3 = [](const float3& a, const float3& b) { + return make_float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); + }; + auto group_of_vertex = [&](size_t vid) -> size_t { return use_canon_groups ? model.vertex_to_canon[vid] : vid; }; + + size_t n_cap_clipped = 0; + float max_requested_depth = 0.f; + float max_discarded_depth = 0.f; + bool has_any_wear = false; + for (size_t tri = 0; tri < n_tri; tri++) { + const float pending_depth = model.pending_depth[tri]; + if (!(pending_depth > 0.f) || !std::isfinite(pending_depth)) { + continue; + } + max_requested_depth = std::max(max_requested_depth, pending_depth); + const float depth_chunk = std::min(pending_depth, depth_cap); + if (!(depth_chunk > 0.f) || !std::isfinite(depth_chunk)) { + continue; + } + if (depth_chunk + DEME_TINY_FLOAT < pending_depth) { + n_cap_clipped++; + max_discarded_depth = std::max(max_discarded_depth, pending_depth - depth_chunk); + } + + const int3& f = faces[tri]; + if (f.x < 0 || f.y < 0 || f.z < 0) { + continue; + } + const size_t i0 = static_cast(f.x); + const size_t i1 = static_cast(f.y); + const size_t i2 = static_cast(f.z); + if (i0 >= vertices.size() || i1 >= vertices.size() || i2 >= vertices.size()) { + continue; + } + + const float3 v0 = vertices[i0]; + const float3 v1 = vertices[i1]; + const float3 v2 = vertices[i2]; + const float3 n_raw = cross3(v1 - v0, v2 - v0); + const float n_len = length(n_raw); + if (!(n_len > DEME_TINY_FLOAT) || !std::isfinite(n_len)) { + continue; + } + float3 n_unit = n_raw / n_len; + if (model.ref_tri_normals.size() == n_tri) { + const float3 n_ref = model.ref_tri_normals[tri]; + const float n_ref_len = length(n_ref); + if (n_ref_len > DEME_TINY_FLOAT && std::isfinite(n_ref_len)) { + n_unit = n_ref / n_ref_len; + } + } + const float3 disp = n_unit * (depth_chunk * model.normal_sign); + const float tri_area = 0.5f * n_len; + const size_t g0 = group_of_vertex(i0); + const size_t g1 = group_of_vertex(i1); + const size_t g2 = group_of_vertex(i2); + if (g0 >= n_vertex_groups || g1 >= n_vertex_groups || g2 >= n_vertex_groups) { + DEME_ERROR("Wear canonical-vertex map out of range for owner %zu.", (size_t)ownerID); + } + + weighted_disp[g0] += disp * tri_area; + weighted_disp[g1] += disp * tri_area; + weighted_disp[g2] += disp * tri_area; + weighted_sum[g0] += tri_area; + weighted_sum[g1] += tri_area; + weighted_sum[g2] += tri_area; + has_any_wear = true; + } + + if (n_cap_clipped > 0) { + model.cap_warning_count++; + DEME_WARNING( + "Mesh-wear cap reached for owner %zu: %zu triangle(s) exceeded the per-update cap. Excess wear depth is " + "discarded. Current cap: %.6g (%.6g * median edge %.6g). Max requested depth: %.6g; max discarded depth: " + "%.6g.", + (size_t)ownerID, n_cap_clipped, (double)model.max_depth_per_update, + (double)model.max_depth_fraction_of_median_edge, (double)model.median_edge_length, + (double)max_requested_depth, (double)max_discarded_depth); + } + + auto clear_pending_depth = [&]() { std::fill(model.pending_depth.begin(), model.pending_depth.end(), 0.f); }; + if (!has_any_wear) { + clear_pending_depth(); + return false; + } + + bool has_any_node_update = false; + for (size_t i = 0; i < vertices.size(); i++) { + const size_t g = group_of_vertex(i); + if (g >= n_vertex_groups || !(weighted_sum[g] > DEME_TINY_FLOAT)) { + continue; + } + node_updates[i] = weighted_disp[g] / weighted_sum[g]; + if (length(node_updates[i]) > DEME_TINY_FLOAT) { + has_any_node_update = true; + } + } + + if (!has_any_node_update) { + clear_pending_depth(); + return false; + } + UpdateTriNodeRelPos(ownerID, model.tri_start, node_updates); + clear_pending_depth(); + return true; +} + +void DEMSolver::updateMeshWearModels(double call_start_time, double call_end_time) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + if (m_mesh_wear_models.empty() || !(call_end_time > call_start_time)) { + return; + } + if (!dT->triPVTrackingEnabled || dT->triPVWindowSteps == 0) { + return; + } + + struct WearEval { + bodyID_t owner = NULL_BODYID; + MeshWearModelState* model = nullptr; + size_t offset = 0; + size_t count = 0; + double active_dt = 0.0; + bool ended_this_call = false; + }; + std::vector evals; + evals.reserve(m_mesh_wear_models.size()); + bool has_active_wear = false; + + for (auto& kv : m_mesh_wear_models) { + const bodyID_t owner = kv.first; + MeshWearModelState& model = kv.second; + auto it_slot = dT->triPVOwnerToSlot.find(owner); + if (it_slot == dT->triPVOwnerToSlot.end()) { + continue; + } + const size_t slot = it_slot->second; + const size_t offset = dT->triPVOwnerOffsets[slot]; + const size_t count = dT->triPVOwnerCounts[slot]; + if (count != model.tri_count) { + DEME_ERROR("Wear model tracked-triangle mismatch for owner %zu (expected %zu, got %zu).", (size_t)owner, + model.tri_count, count); + } + + double active_dt = 0.0; + bool ended_this_call = false; + if (model.end_time >= 0.0) { + active_dt = overlapDuration(call_start_time, call_end_time, model.start_time, model.end_time); + ended_this_call = (call_start_time < model.end_time && call_end_time >= model.end_time); + } else if (call_end_time > model.start_time) { + active_dt = call_end_time - std::max(call_start_time, model.start_time); + } + if (active_dt > 0.0) { + has_active_wear = true; + } + + evals.push_back(WearEval{owner, &model, offset, count, active_dt, ended_this_call}); + } + + if (evals.empty() || !has_active_wear) { + cacheTrackedTrianglePVWindow(); + dT->resetTrackedTrianglePVWindow(); + return; + } + + dT->triPVAccumPV.toHost(); + const float inv_steps = 1.f / static_cast(dT->triPVWindowSteps); + std::vector owners_to_apply; + owners_to_apply.reserve(evals.size()); + + for (const auto& ev : evals) { + MeshWearModelState& model = *ev.model; + if (ev.active_dt > 0.0) { + const float depth_scale = static_cast(model.wear_rate * ev.active_dt) * inv_steps; + for (size_t i = 0; i < ev.count; i++) { + const float avg_pv = dT->triPVAccumPV[ev.offset + i]; + const float depth_inc = avg_pv * depth_scale; + if (depth_inc > 0.f && std::isfinite(depth_inc)) { + model.pending_depth[i] += depth_inc; + } + } + model.pending_time += ev.active_dt; + } + + const bool due = (model.pending_time + 1e-15 >= model.update_interval); + if (due || ev.ended_this_call) { + if (hasPendingWear(model.pending_depth)) { + owners_to_apply.push_back(ev.owner); + } else { + model.pending_time = 0.0; + } + } + } + + cacheTrackedTrianglePVWindow(); + dT->resetTrackedTrianglePVWindow(); + + if (!owners_to_apply.empty()) { + WaitForPendingOutput(); + } + for (bodyID_t owner : owners_to_apply) { + auto it = m_mesh_wear_models.find(owner); + if (it == m_mesh_wear_models.end()) { + continue; + } + applyMeshWearModel(owner, it->second); + it->second.pending_time = 0.0; + } +} + +void DEMSolver::resolveCombinedOwners(size_t nExistOwners) { + // Combined-member owner IDs are only meaningful after Initialize/Update assigned owner numbering. + // This method resolves cached member initializer handles to stable owner IDs exactly once per combined instance, + // then triggers a runtime metadata refresh for kT/dT if anything new was resolved. + if (!sys_initialized) { + return; + } + + std::vector prescans_batch_size; + prescans_batch_size.push_back(0); + for (const auto& a_batch : cached_input_clump_batches) { + prescans_batch_size.push_back(prescans_batch_size.back() + a_batch->GetNumClumps()); + } + + for (auto& inst : cached_combined_instances) { + if (!inst || inst->owners_resolved || !inst->type) { + continue; + } + const size_t total_members = inst->member_objs.size(); + const size_t n_members_per_inst = (inst->type->member_type == OWNER_TYPE::CLUMP) + ? inst->type->clump_templates.size() + : inst->type->mesh_templates.size(); + inst->member_owner_ids.resize(total_members, NULL_BODYID); + + bool ok = true; + for (size_t i = 0; i < total_members; i++) { + const auto& member_obj = inst->member_objs[i]; + if (!member_obj) { + ok = false; + break; + } + if (member_obj->obj_type == OWNER_TYPE::CLUMP) { + const size_t load_order = member_obj->load_order; + if (load_order >= cached_input_clump_batches.size()) { + ok = false; + break; + } + inst->member_owner_ids[i] = static_cast(nExistOwners + prescans_batch_size[load_order]); + } else if (member_obj->obj_type == OWNER_TYPE::MESH) { + const size_t load_order = member_obj->load_order; + if (load_order >= cached_mesh_objs.size()) { + ok = false; + break; + } + const size_t owner_id = + nExistOwners + cached_extern_objs.size() + prescans_batch_size.back() + load_order; + inst->member_owner_ids[i] = static_cast(owner_id); + } else { + ok = false; + break; + } + } + + if (!ok || n_members_per_inst == 0 || inst->type->master_member >= n_members_per_inst) { + continue; + } + // Resolve master owner IDs for each instantiation in the batch. + for (size_t k = 0; k < inst->n_instances; k++) { + inst->master_owner_ids[k] = inst->member_owner_ids[k * n_members_per_inst + inst->type->master_member]; + } + inst->owners_resolved = true; + m_combined_runtime_dirty = true; + } + + // If no new instance was resolved, this call is a no-op due to m_combined_runtime_dirty check. + refreshCombinedRuntimeResources(); +} + +void DEMSolver::refreshCombinedRuntimeResources() { + // Refresh flattened combined-owner runtime arrays that device-side code consumes every step. + // This is intentionally host-driven and batched (Initialize/Update/getter resolution), + // not re-built inside high-frequency DoDynamics calls. + if (!sys_initialized || !m_combined_runtime_dirty) { + return; + } + + const auto clear_combined_runtime_resources = [&]() { + { + // DualArray allocations do not track their owning CUDA device, so free and repack each worker's resources + // while that worker's device is current. + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->ownerCombinedMaster.free(); + dT->ownerCombinedRelPos.free(); + dT->ownerCombinedRelOriQ.free(); + dT->ownerCombinedMasterMass.free(); + dT->ownerCombinedMasterMOI.free(); + dT->granData.toDevice(); + } + { + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->ownerCombinedMaster.free(); + kT->granData.toDevice(); + } + + dT->simParams->nCombinedOwners = 0; + kT->simParams->nCombinedOwners = 0; + dT->simParams->allowIntraCombinedOwnerContacts = m_allow_intra_combined_owner_contacts ? 1 : 0; + kT->simParams->allowIntraCombinedOwnerContacts = m_allow_intra_combined_owner_contacts ? 1 : 0; + dT->simParams.toDevice(); + kT->simParams.toDevice(); + }; + + if (cached_combined_instances.empty()) { + clear_combined_runtime_resources(); + m_combined_runtime_dirty = false; + return; + } + + // Reset per-owner mapping to the "not in a combined group" default state. + std::vector owner_combined_master(nOwnerBodies, NULL_BODYID); + std::vector owner_combined_rel_pos(nOwnerBodies, make_float3(0)); + std::vector owner_combined_rel_oriQ(nOwnerBodies, make_float4(0, 0, 0, 1)); + std::vector owner_combined_master_mass(nOwnerBodies, 0.f); + std::vector owner_combined_master_moi(nOwnerBodies, make_float3(0)); + + bodyID_t n_combined_owners = 0; + constexpr float DEFAULT_COMBINED_MASTER_MASS = 1.f; + for (const auto& inst : cached_combined_instances) { + if (!inst || !inst->owners_resolved || !inst->type) { + continue; + } + const size_t total_members = inst->member_owner_ids.size(); + const size_t n_members_per_inst = (inst->type->member_type == OWNER_TYPE::CLUMP) + ? inst->type->clump_templates.size() + : inst->type->mesh_templates.size(); + if (total_members == 0 || n_members_per_inst == 0 || inst->type->master_member >= n_members_per_inst) { + continue; + } + + for (size_t k = 0; k < inst->n_instances; k++) { + const bodyID_t master = inst->master_owner_ids[k]; + if (master == NULL_BODYID || master >= nOwnerBodies) { + continue; + } + + // Cache equivalent group mass/MOI on the master owner slot for device-side aggregation. + const float safe_mass = (inst->master_equiv_mass[k] > DEME_TINY_FLOAT) ? inst->master_equiv_mass[k] + : DEFAULT_COMBINED_MASTER_MASS; + owner_combined_master_mass[master] = safe_mass; + + for (size_t i = 0; i < n_members_per_inst; i++) { + const size_t flat_idx = k * n_members_per_inst + i; + const bodyID_t member = inst->member_owner_ids[flat_idx]; + if (member == NULL_BODYID || member >= nOwnerBodies) { + continue; + } + // Membership map: every member points to its master; this is the key used by contact suppression. + owner_combined_master[member] = master; + owner_combined_master_moi[master] += + rotateDiagonalMOIToFrame(inst->member_moi[flat_idx], inst->type->rel_oriQ[i]) + + parallelAxisDiagonal(inst->member_mass[flat_idx], inst->type->rel_pos[i]); + if (member != master) { + // Only non-master members need fixed transforms for rigid re-imposition. + owner_combined_rel_pos[member] = inst->type->rel_pos[i]; + owner_combined_rel_oriQ[member] = inst->type->rel_oriQ[i]; + n_combined_owners++; + } + } + } + } + + if (n_combined_owners == 0) { + clear_combined_runtime_resources(); + m_combined_runtime_dirty = false; + return; + } + + // DualArray uses the current CUDA device for allocation and migration. Scope each worker's runtime resources to its + // own device so the kT contact-suppression kernel never receives a dT-device membership pointer. + { + ScopedCudaDevice device_scope(dT->streamInfo.device); + dT->ownerCombinedMaster.resize(nOwnerBodies, NULL_BODYID); + dT->ownerCombinedRelPos.resize(nOwnerBodies, make_float3(0)); + dT->ownerCombinedRelOriQ.resize(nOwnerBodies, make_float4(0, 0, 0, 1)); + dT->ownerCombinedMasterMass.resize(nOwnerBodies, 0.f); + dT->ownerCombinedMasterMOI.resize(nOwnerBodies, make_float3(0)); + dT->granData.toDevice(); + + dT->ownerCombinedMaster.setVal(owner_combined_master, 0); + dT->ownerCombinedRelPos.setVal(owner_combined_rel_pos, 0); + dT->ownerCombinedRelOriQ.setVal(owner_combined_rel_oriQ, 0); + dT->ownerCombinedMasterMass.setVal(owner_combined_master_mass, 0); + dT->ownerCombinedMasterMOI.setVal(owner_combined_master_moi, 0); + } + { + // kT only needs the membership mapping for contact suppression. + ScopedCudaDevice device_scope(kT->streamInfo.device); + kT->ownerCombinedMaster.resize(nOwnerBodies, NULL_BODYID); + kT->granData.toDevice(); + kT->ownerCombinedMaster.setVal(owner_combined_master, 0); + } + + // Enable/disable combined-specific device logic by count. Zero count means no extra overhead in kernels. + dT->simParams->nCombinedOwners = n_combined_owners; + kT->simParams->nCombinedOwners = n_combined_owners; + dT->simParams->allowIntraCombinedOwnerContacts = m_allow_intra_combined_owner_contacts ? 1 : 0; + kT->simParams->allowIntraCombinedOwnerContacts = m_allow_intra_combined_owner_contacts ? 1 : 0; dT->simParams.toDevice(); kT->simParams.toDevice(); + + m_combined_runtime_dirty = false; } -void DEMSolver::UpdateClumps() { +void DEMSolver::Update() { + ++dT->visualizationRevision; if (!sys_initialized) { - DEME_ERROR( - "Please call UpdateClumps only after the system is initialized, because it is for adding additional clumps " - "to an initialized DEM system."); + DEME_ERROR(std::string( + "Please call Update only after the system is initialized, because it is for adding additional clumps " + "to an initialized DEM system.")); } if (nLastTimeExtObjLoad != nExtObjLoad) { DEME_ERROR( - "UpdateClumps cannot be used after loading new analytical objects. Consider re-initializing at this " + "Update cannot be used after loading new analytical objects. Consider re-initializing at this " "point.\nNumber of analytical objects at last initialization: %u\nNumber of analytical objects now: %u", nLastTimeExtObjLoad, nExtObjLoad); } if (nLastTimeClumpTemplateLoad != nClumpTemplateLoad) { DEME_ERROR( - "UpdateClumps cannot be used after loading new clump templates. Consider re-initializing at this " + "Update cannot be used after loading new clump templates. Consider re-initializing at this " "point.\nNumber of clump templates at last initialization: %zu\nNumber of clump templates now: %zu", nLastTimeClumpTemplateLoad, nClumpTemplateLoad); } // DEME_WARNING( - // "UpdateClumps will add all currently cached clumps to the simulation.\nYou may want to ClearCache first," + // "Update will add all currently cached clumps to the simulation.\nYou may want to ClearCache first," // "then AddClumps, then call this method, so the clumps cached earlier are forgotten before this method takes" // "place."); // This method requires kT and dT are sync-ed // resetWorkerThreads(); - // NOTE!!! This step in UpdateClumps is extremely important, as we'll soon modify device-major arrays on host! + // NOTE!!! This step in Update is extremely important, as we'll soon modify device-major arrays on host! migrateArrayDataToHost(); // Record the number of entities, before adding to the system @@ -2379,6 +4364,8 @@ void DEMSolver::UpdateClumps() { size_t nSpheres_old = nSpheresGM; size_t nTriMesh_old = nTriMeshes; size_t nFacets_old = nTriGM; + size_t nTriNeighbors_old = nTriNeighbors; + size_t nPatch_old = nMeshPatches; unsigned int nAnalGM_old = nAnalGM; unsigned int nExtObj_old = nExtObj; @@ -2388,7 +4375,8 @@ void DEMSolver::UpdateClumps() { updateTotalEntityNum(); allocateGPUArrays(); // `Update' method needs to know the number of existing clumps and spheres (before this addition) - updateClumpMeshArrays(nOwners_old, nClumps_old, nSpheres_old, nTriMesh_old, nFacets_old, nExtObj_old, nAnalGM_old); + updateClumpMeshArrays(nOwners_old, nClumps_old, nSpheres_old, nTriMesh_old, nFacets_old, nTriNeighbors_old, + nPatch_old, nExtObj_old, nAnalGM_old); packDataPointers(); // Now that all params prepared, and all data pointers packed on host side, we need to migrate that imformation to @@ -2403,38 +4391,17 @@ void DEMSolver::UpdateClumps() { // This method should not introduce new material or clump template or family prescription, let's check that if (nLastTimeMatNum != m_loaded_materials.size() || nLastTimeFamilyPreNum != m_input_family_prescription.size()) { DEME_ERROR( - "UpdateClumps should not be used if you introduce new material types or family prescription (which will " + "Update should not be used if you introduce new material types or family prescription (which will " "need re-jitification).\nWe used to have %u materials, now we have %zu.\nWe used to have %u family " "prescription, now we have %zu.", nLastTimeMatNum, m_loaded_materials.size(), nLastTimeFamilyPreNum, m_input_family_prescription.size()); } - // After Initialize or UpdateClumps, we should clear host-side initialization object cache - ClearCache(); -} - -void DEMSolver::ChangeClumpSizes(const std::vector& IDs, const std::vector& factors) { - if (!sys_initialized) { - DEME_ERROR( - "ChangeClumpSizes operates on device-side arrays directly, so it requires the system to be initialized " - "first."); - } - if (jitify_clump_templates || jitify_mass_moi) { - DEME_ERROR( - "ChangeClumpSizes only works when the clump components are flattened (not jitified).\nConsider calling " - "SetJitifyClumpTemplates(false) and SetJitifyMassProperties(false)."); - } - - // This method requires kT and dT are sync-ed - // resetWorkerThreads(); - - std::thread dThread = std::move(std::thread([this, IDs, factors]() { this->dT->changeOwnerSizes(IDs, factors); })); - std::thread kThread = std::move(std::thread([this, IDs, factors]() { this->kT->changeOwnerSizes(IDs, factors); })); - dThread.join(); - kThread.join(); + m_combined_runtime_dirty = true; + resolveCombinedOwners(nOwners_old); - // Size changes are critical - dT->announceCritical(); + // After Initialize or Update, we should clear host-side initialization object cache + ClearCache(); } /// Removes all entities associated with a family from the arrays (to save memory space). This method should only be @@ -2447,6 +4414,13 @@ void DEMSolver::DoDynamics(double thisCallDuration) { if (!sys_initialized) { Initialize(); } + if (m_combined_runtime_dirty) { + DEME_ERROR( + "Combined-owner runtime metadata is marked dirty before DoDynamics.\n" + "Combined templates/instances are setup data and must be resolved by Initialize or Update before " + "dynamics."); + } + const double sim_time_start = dT->getSimTime(); // Tell dT how long this call is dT->setCycleDuration(thisCallDuration); @@ -2464,6 +4438,9 @@ void DEMSolver::DoDynamics(double thisCallDuration) { // since that's only used when kT and dT sync. dTMain_InteractionManager->userCallDone = false; } + + const double sim_time_end = dT->getSimTime(); + updateMeshWearModels(sim_time_start, sim_time_end); } void DEMSolver::DoDynamicsThenSync(double thisCallDuration) { @@ -2506,13 +4483,14 @@ void DEMSolver::ShowThreadCollaborationStats() { void DEMSolver::ShowAnomalies() { DEME_PRINTF("\n~~ Simulation anomaly report ~~\n"); - bool there_is_anomaly = goThroughWorkerAnomalies(); - if (!there_is_anomaly) { - DEME_PRINTF("There is no simulation anomalies on record.\n"); - } + Logger::GetInstance().PrintStatusMessages(std::cerr); + DEME_PRINTF("-----------------------------\n"); +} + +void DEMSolver::ShowWarnings() { + DEME_PRINTF("\n~~ Simulation warning report ~~\n"); + Logger::GetInstance().PrintWarningsAndErrors(std::cerr); DEME_PRINTF("-----------------------------\n"); - kT->anomalies.Clear(); - dT->anomalies.Clear(); } void DEMSolver::ClearThreadCollaborationStats() { @@ -2526,25 +4504,55 @@ void DEMSolver::ClearThreadCollaborationStats() { dT->nTotalSteps = 0; } -float DEMSolver::dTInspectReduce(const std::shared_ptr& inspection_kernel, +float DEMSolver::dTInspectReduce(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, - bool all_domain) { - // Note they are currently running in the device associated with the main, but it's not a big issue - //// TODO: Think about the implication on using more than 2 GPUs - float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain); + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + reduceResArr, reduceRes, false); return (float)(*pRes); } -float* DEMSolver::dTInspectNoReduce(const std::shared_ptr& inspection_kernel, +float* DEMSolver::dTInspectNoReduce(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, - bool all_domain) { - // Note they are currently running in the device associated with the main, but it's not a big issue - //// TODO: Think about the implication on using more than 2 GPUs - float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain); + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + reduceResArr, reduceRes, false); + return pRes; +} + +float DEMSolver::dTInspectReduceDevice(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + reduceResArr, reduceRes, true); + return (float)(*pRes); +} + +float* DEMSolver::dTInspectNoReduceDevice(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes) { + ScopedCudaDevice device_scope(dT->streamInfo.device); + float* pRes = dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + reduceResArr, reduceRes, true); return pRes; } diff --git a/src/DEM/AuxClasses.cpp b/src/DEM/AuxClasses.cpp index a000f38a..b9f6cfc0 100644 --- a/src/DEM/AuxClasses.cpp +++ b/src/DEM/AuxClasses.cpp @@ -7,7 +7,7 @@ #include "API.h" #include "AuxClasses.h" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" #include "Models.h" namespace deme { @@ -60,6 +60,27 @@ const std::string INSP_CODE_EVERYTHING_ABSV = R"V0G0N( quantity[myOwner] = myABSV; )V0G0N"; +const std::string INSP_CODE_EVERYTHING_ABSANGVEL = R"V0G0N( + double myOmegaX = granData->omgBarX[myOwner]; + double myOmegaY = granData->omgBarY[myOwner]; + double myOmegaZ = granData->omgBarZ[myOwner]; + double myABSANGVEL = sqrt(myOmegaX * myOmegaX + myOmegaY * myOmegaY + myOmegaZ * myOmegaZ); + + quantity[myOwner] = myABSANGVEL; +)V0G0N"; + +const std::string INSP_CODE_ANGVEL_X = R"V0G0N( + quantity[myOwner] = granData->omgBarX[myOwner]; +)V0G0N"; + +const std::string INSP_CODE_ANGVEL_Y = R"V0G0N( + quantity[myOwner] = granData->omgBarY[myOwner]; +)V0G0N"; + +const std::string INSP_CODE_ANGVEL_Z = R"V0G0N( + quantity[myOwner] = granData->omgBarZ[myOwner]; +)V0G0N"; + const std::string INSP_CODE_CLUMP_KE = R"V0G0N( // First lin energy double myVX = granData->vX[myOwner]; @@ -116,8 +137,8 @@ void DEMInspector::switch_quantity_type(const std::string& quantity) { // reduce_flavor = CUB_REDUCE_FLAVOR::MAX; // kernel_name = "inspectOwnerProperty"; // break; - //// TODO: Void ration here is a very rough approximation and should only work when domain is large and - /// particles are small + //// TODO: Void ratio here is a very rough approximation and should only work when domain is large and particles + /// are small case ("clump_volume"_): inspection_code = INSP_CODE_CLUMP_APPROX_VOL; reduce_flavor = CUB_REDUCE_FLAVOR::SUM; @@ -148,6 +169,41 @@ void DEMInspector::switch_quantity_type(const std::string& quantity) { thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; index_name = "myOwner"; break; + case ("absangvel"_): + inspection_code = INSP_CODE_EVERYTHING_ABSANGVEL; + reduce_flavor = CUB_REDUCE_FLAVOR::NONE; + kernel_name = "inspectOwnerProperty"; + thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; + index_name = "myOwner"; + break; + case ("absangvelx"_): + inspection_code = INSP_CODE_ANGVEL_X; + reduce_flavor = CUB_REDUCE_FLAVOR::NONE; + kernel_name = "inspectOwnerProperty"; + thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; + index_name = "myOwner"; + break; + case ("absangvely"_): + inspection_code = INSP_CODE_ANGVEL_Y; + reduce_flavor = CUB_REDUCE_FLAVOR::NONE; + kernel_name = "inspectOwnerProperty"; + thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; + index_name = "myOwner"; + break; + case ("absangvelz"_): + inspection_code = INSP_CODE_ANGVEL_Z; + reduce_flavor = CUB_REDUCE_FLAVOR::NONE; + kernel_name = "inspectOwnerProperty"; + thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; + index_name = "myOwner"; + break; + case ("max_absangvel"_): + inspection_code = INSP_CODE_EVERYTHING_ABSANGVEL; + reduce_flavor = CUB_REDUCE_FLAVOR::MAX; + kernel_name = "inspectOwnerProperty"; + thing_to_insp = INSPECT_ENTITY_TYPE::EVERYTHING; + index_name = "myOwner"; + break; case ("clump_kinetic_energy"_): inspection_code = INSP_CODE_CLUMP_KE; reduce_flavor = CUB_REDUCE_FLAVOR::SUM; @@ -157,23 +213,35 @@ void DEMInspector::switch_quantity_type(const std::string& quantity) { all_domain = false; // Only clumps, so not all domain owners break; default: - std::stringstream ss; - ss << quantity << " is not a known query type." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("%s is not a known query type.", quantity.c_str()); } } float DEMInspector::GetValue() { assertInit(); - float reduce_result = - sys->dTInspectReduce(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain); + float reduce_result = sys->dTInspectReduce(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + m_reduceResArr, m_reduceRes); return reduce_result; } float* DEMInspector::GetValues() { assertInit(); - float* reduce_result = - sys->dTInspectNoReduce(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain); + float* reduce_result = sys->dTInspectNoReduce(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, + all_domain, m_reduceResArr, m_reduceRes); + return reduce_result; +} + +float DEMInspector::GetDeviceValue() { + assertInit(); + float reduce_result = sys->dTInspectReduceDevice(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, + all_domain, m_reduceResArr, m_reduceRes); + return reduce_result; +} + +float* DEMInspector::GetDeviceValues() { + assertInit(); + float* reduce_result = sys->dTInspectNoReduceDevice(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, + all_domain, m_reduceResArr, m_reduceRes); return reduce_result; } @@ -181,13 +249,19 @@ float* DEMInspector::dT_GetValue() { // assertInit(); // This one the user should not use // Also, this call breaks the chain-that-bind, but I'm not too worried, as it's used in dT's workerThread only, // meaning the device number is well-defined. - return dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain); + return dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, m_reduceResArr, + m_reduceRes, false); } -float* DEMInspector::dT_GetDeviceValue() { - // assertInit(); // This one the user should not use - // This returns a device pointer instead of a host pointer - return dT->inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, true); +float* DEMInspector::dT_GetDeviceValues() { + // Device-only inspection path to avoid host syncs in dT's worker thread. + return dT->inspectCallDeviceNoReduce(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, + m_reduceResArr, m_reduceRes); +} + +void DEMInspector::ReleaseData() { + m_reduceResArr.free(); + m_reduceRes.free(); } void DEMInspector::assertInit() { @@ -200,11 +274,9 @@ void DEMInspector::Initialize(const std::unordered_map const std::vector& options, bool force) { if (!(sys->GetInitStatus()) && !force) { - std::stringstream ss; - ss << "Inspector should only be initialized or used after the simulation system is initialized (because it " - "uses device-side data)!" - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR(std::string( + "Inspector should only be initialized or used after the simulation system is initialized (because " + "it uses device-side data)!")); } // We want to make sure if the in_region_code is legit, if it is not an all_domain query std::string in_region_specifier = in_region_code, placeholder; @@ -212,12 +284,11 @@ void DEMInspector::Initialize(const std::unordered_map if ((!all_domain) && (!is_all_spaces(in_region_code))) { if (!any_whole_word_match(in_region_code, {"X", "Y", "Z"}) || !all_whole_word_match(in_region_code, {"return"}, placeholder)) { - std::stringstream ss; - ss << "One of your insepctors is set to query a specific region, but the domian is not properly " - "defined.\nIt needs to return a bool variable that is a result of logical operations involving X, Y " - "and Z.\nYou can remove the region argument if all simulation entities should be considered." - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR(std::string( + "One of your insepctors is set to query a specific region, but the domian is not properly " + "defined.\nIt needs to return a bool variable that is a result of logical operations involving " + "X, Y and Z.\nYou can remove the region argument if all simulation entities should be " + "considered.")); } // Replace the return with our own variable in_region_specifier = replace_pattern(in_region_specifier, "return", "bool isInRegion = "); @@ -229,17 +300,15 @@ void DEMInspector::Initialize(const std::unordered_map my_subs["_inRegionPolicy_"] = in_region_specifier; my_subs["_quantityQueryProcess_"] = inspection_code; if (thing_to_insp == INSPECT_ENTITY_TYPE::SPHERE) { - inspection_kernel = std::make_shared(std::move(JitHelper::buildProgram( + inspection_kernel = std::make_shared(std::move(JitHelper::buildProgram( "DEMSphereQueryKernels", JitHelper::KERNEL_DIR / "DEMSphereQueryKernels.cu", my_subs, options))); } else if (thing_to_insp == INSPECT_ENTITY_TYPE::CLUMP || thing_to_insp == INSPECT_ENTITY_TYPE::EVERYTHING) { - inspection_kernel = std::make_shared(std::move(JitHelper::buildProgram( + inspection_kernel = std::make_shared(std::move(JitHelper::buildProgram( "DEMOwnerQueryKernels", JitHelper::KERNEL_DIR / "DEMOwnerQueryKernels.cu", my_subs, options))); } else { - std::stringstream ss; - ss << "Sorry, an inspector object you are using is not implemented yet.\nConsider letting the developers know " - "this and they may help you." - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR(std::string( + "Sorry, an inspector object you are using is not implemented yet.\nConsider letting the developers " + "know this and they may help you.")); } initialized = true; } @@ -250,56 +319,48 @@ void DEMInspector::Initialize(const std::unordered_map void DEMTracker::assertThereIsForcePairs(const std::string& name) { if (sys->GetWhetherForceCollectInKernel()) { - std::stringstream ss; - ss << "The solver is currently set to not record force pair info, so you cannot query force pairs using " - << name - << ".\nYou can call SetCollectAccRightAfterForceCalc(false) before system initialization and try " - "again." - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "The solver is currently set to not record force pair info, so you cannot query force pairs using " + "%s.\nYou can call SetCollectAccRightAfterForceCalc(false) before system initialization and try " + "again.", + name.c_str()); } } void DEMTracker::assertMesh(const std::string& name) { if (obj->obj_type != OWNER_TYPE::MESH) { - std::stringstream ss; - ss << name << " is only callable for trackers tracking a mesh!" << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("%s is only callable for trackers tracking a mesh!", name.c_str()); } } void DEMTracker::assertGeoSize(size_t input_length, const std::string& name, const std::string& geo_type) { if (input_length != obj->nGeos) { - std::stringstream ss; - ss << name << " is called with an input not the same size (number of " << geo_type - << ") as the original tracked object!\nThe input has " << input_length << " " << geo_type - << " while the tracked object has " << obj->nGeos << "." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s is called with an input not the same size (number of %s) as the original tracked object!\nThe " + "input has %zu %s while the tracked object has %zu.", + name.c_str(), geo_type.c_str(), input_length, geo_type.c_str(), obj->nGeos); } } void DEMTracker::assertOwnerSize(size_t input_length, const std::string& name) { if (input_length != obj->nSpanOwners) { - std::stringstream ss; - ss << name << " is called with an input not the same size of the number of owners it tracks!\nThe input has " - << input_length << " elements while the tracker tracks " << obj->nSpanOwners << " entities." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s is called with an input not the same size of the number of owners it tracks!\nThe input has " + "%zu elements while the tracker tracks %zu entities.", + name.c_str(), input_length, obj->nSpanOwners); } } void DEMTracker::assertOwnerOffsetValid(size_t offset, const std::string& name) { if (offset >= obj->nSpanOwners) { - std::stringstream ss; - ss << name - << " is called with an offset larger than the number of owners (minus 1, to be precise) it tracks!\nThe " - "offset is " - << offset << " while the tracker tracks " << obj->nSpanOwners << " entities." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s is called with an offset larger than the number of owners (minus 1, to be precise) it " + "tracks!\nThe offset is %zu while the tracker tracks %zu entities.", + name.c_str(), offset, obj->nSpanOwners); } } void DEMTracker::assertGeoOffsetValid(size_t offset, const std::string& name, const std::string& geo_type) { if (offset >= obj->nGeos) { - std::stringstream ss; - ss << name << " is called with an offset larger than the number of " << geo_type - << " (minus 1, to be precise) it tracks!\nThe offset is " << offset << " " << geo_type - << " while the tracked object has " << obj->nGeos << "." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s is called with an offset larger than the number of %s (minus 1, to be precise) it tracks!\nThe " + "offset is %zu %s while the tracked object has %zu.", + name.c_str(), geo_type.c_str(), offset, geo_type.c_str(), obj->nGeos); } } @@ -334,6 +395,12 @@ std::vector> DEMTracker::GetPositions() { std::vector res = Positions(); return Real3VectorToVecOfVec(res); } +void DEMTracker::PositionsToDevice(float3* destination, size_t capacity, int destination_device, bool validate) { + sys->GetOwnerPositionToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} +void DEMTracker::SetPositionsFromDevice(const float3* source, int source_device, bool validate) { + sys->SetOwnerPositionFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} float3 DEMTracker::AngVelLocal(size_t offset) { assertOwnerOffsetValid(offset, "AngVelLocal"); @@ -350,6 +417,16 @@ std::vector> DEMTracker::GetAngularVelocitiesLocal() { std::vector res = AngularVelocitiesLocal(); return Real3VectorToVecOfVec(res); } +void DEMTracker::AngularVelocitiesLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerAngVelLocalToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, + validate); +} +void DEMTracker::SetAngularVelocitiesFromDevice(const float3* source, int source_device, bool validate) { + sys->SetOwnerAngVelFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} float3 DEMTracker::AngVelGlobal(size_t offset) { assertOwnerOffsetValid(offset, "AngVelGlobal"); @@ -374,6 +451,16 @@ std::vector> DEMTracker::GetAngularVelocitiesGlobal() { std::vector res = AngularVelocitiesGlobal(); return Real3VectorToVecOfVec(res); } +void DEMTracker::AngularVelocitiesGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerAngVelGlobalToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, + validate); +} +void DEMTracker::SetAngularVelocitiesGlobalFromDevice(const float3* source, int source_device, bool validate) { + sys->SetOwnerAngVelGlobalFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} float3 DEMTracker::Vel(size_t offset) { assertOwnerOffsetValid(offset, "Vel"); @@ -390,6 +477,12 @@ std::vector> DEMTracker::GetVelocities() { std::vector res = Velocities(); return Real3VectorToVecOfVec(res); } +void DEMTracker::VelocitiesToDevice(float3* destination, size_t capacity, int destination_device, bool validate) { + sys->GetOwnerVelocityToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} +void DEMTracker::SetVelocitiesFromDevice(const float3* source, int source_device, bool validate) { + sys->SetOwnerVelocityFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} float4 DEMTracker::OriQ(size_t offset) { assertOwnerOffsetValid(offset, "OriQ"); @@ -406,6 +499,15 @@ std::vector> DEMTracker::GetOrientationQuaternions() { std::vector res = OrientationQuaternions(); return Real4VectorToVecOfVec(res); } +void DEMTracker::OrientationQuaternionsToDevice(float4* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerOriQToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} +void DEMTracker::SetOrientationQuaternionsFromDevice(const float4* source, int source_device, bool validate) { + sys->SetOwnerOriQFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} unsigned int DEMTracker::GetFamily(size_t offset) { assertOwnerOffsetValid(offset, "GetFamily"); @@ -414,6 +516,9 @@ unsigned int DEMTracker::GetFamily(size_t offset) { std::vector DEMTracker::GetFamilies() { return sys->GetOwnerFamily(obj->ownerID, obj->nSpanOwners); } +void DEMTracker::FamiliesToDevice(unsigned int* destination, size_t capacity, int destination_device, bool validate) { + sys->GetOwnerFamilyToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} float DEMTracker::Mass(size_t offset) { assertOwnerOffsetValid(offset, "Mass"); @@ -422,6 +527,9 @@ float DEMTracker::Mass(size_t offset) { std::vector DEMTracker::Masses() { return sys->GetOwnerMass(obj->ownerID, obj->nSpanOwners); } +void DEMTracker::MassesToDevice(float* destination, size_t capacity, int destination_device, bool validate) { + sys->GetOwnerMassToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} float3 DEMTracker::MOI(size_t offset) { assertOwnerOffsetValid(offset, "MOI"); @@ -438,6 +546,9 @@ std::vector> DEMTracker::GetMOIs() { std::vector res = MOIs(); return Real3VectorToVecOfVec(res); } +void DEMTracker::MOIsToDevice(float3* destination, size_t capacity, int destination_device, bool validate) { + sys->GetOwnerMOIToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} // float3 DEMTracker::Acc(size_t offset) { // float3 contact_acc = sys->GetOwnerAcc(obj->ownerID + offset); @@ -465,6 +576,12 @@ std::vector> DEMTracker::GetContactAccelerations() { std::vector res = ContactAccelerations(); return Real3VectorToVecOfVec(res); } +void DEMTracker::ContactAccelerationsToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerAccToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, validate); +} float3 DEMTracker::ContactAngAccLocal(size_t offset) { assertOwnerOffsetValid(offset, "ContactAngAccLocal"); @@ -481,6 +598,13 @@ std::vector> DEMTracker::GetContactAngularAccelerationsLocal( std::vector res = ContactAngularAccelerationsLocal(); return Real3VectorToVecOfVec(res); } +void DEMTracker::ContactAngularAccelerationsLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerAngAccLocalToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, + validate); +} float3 DEMTracker::ContactAngAccGlobal(size_t offset) { assertOwnerOffsetValid(offset, "ContactAngAccGlobal"); @@ -505,6 +629,23 @@ std::vector> DEMTracker::GetContactAngularAccelerationsGlobal std::vector res = ContactAngularAccelerationsGlobal(); return Real3VectorToVecOfVec(res); } +void DEMTracker::ContactAngularAccelerationsGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerAngAccGlobalToDevice(destination, capacity, destination_device, obj->ownerID, obj->nSpanOwners, + validate); +} +void DEMTracker::ContactWrenches(std::vector& forces, std::vector& torques) { + sys->GetOwnerContactWrench(forces, torques, obj->ownerID, obj->nSpanOwners); +} +void DEMTracker::ContactWrenchesToDevice(float3* force_destination, + float3* torque_destination, + size_t capacity, + int destination_device) { + sys->GetOwnerContactWrenchToDevice(force_destination, torque_destination, capacity, destination_device, + obj->ownerID, obj->nSpanOwners); +} float DEMTracker::GetOwnerWildcardValue(const std::string& name, size_t offset) { assertOwnerOffsetValid(offset, "GetOwnerWildcardValue"); @@ -514,40 +655,13 @@ float DEMTracker::GetOwnerWildcardValue(const std::string& name, size_t offset) std::vector DEMTracker::GetOwnerWildcardValues(const std::string& name) { return sys->GetOwnerWildcardValue(obj->ownerID, name, obj->nSpanOwners); } - -float DEMTracker::GetGeometryWildcardValue(const std::string& name, size_t offset) { - std::vector res; - switch (obj->obj_type) { - case (OWNER_TYPE::CLUMP): - assertGeoOffsetValid(offset, "GetGeometryWildcardValue", "spheres"); - res = sys->GetSphereWildcardValue(obj->geoID + offset, name, 1); - break; - case (OWNER_TYPE::ANALYTICAL): - assertGeoOffsetValid(offset, "GetGeometryWildcardValue", "analytical components"); - res = sys->GetAnalWildcardValue(obj->geoID + offset, name, 1); - break; - case (OWNER_TYPE::MESH): - assertGeoOffsetValid(offset, "GetGeometryWildcardValue", "triangles"); - res = sys->GetTriWildcardValue(obj->geoID + offset, name, 1); - break; - } - return res[0]; -} - -std::vector DEMTracker::GetGeometryWildcardValues(const std::string& name) { - std::vector res; - switch (obj->obj_type) { - case (OWNER_TYPE::CLUMP): - res = sys->GetSphereWildcardValue(obj->geoID, name, obj->nGeos); - break; - case (OWNER_TYPE::ANALYTICAL): - res = sys->GetAnalWildcardValue(obj->geoID, name, obj->nGeos); - break; - case (OWNER_TYPE::MESH): - res = sys->GetTriWildcardValue(obj->geoID, name, obj->nGeos); - break; - } - return res; +void DEMTracker::OwnerWildcardValuesToDevice(const std::string& name, + float* destination, + size_t capacity, + int destination_device, + bool validate) { + sys->GetOwnerWildcardValueToDevice(destination, capacity, destination_device, obj->ownerID, name, obj->nSpanOwners, + validate); } size_t DEMTracker::GetContactForces(std::vector& points, std::vector& forces, size_t offset) { @@ -557,6 +671,16 @@ size_t DEMTracker::GetContactForces(std::vector& points, std::vectorGetOwnerContactForces({obj->ownerID + (bodyID_t)offset}, points, forces); } +size_t DEMTracker::GetContactForcesToDevice(float3* points, + float3* forces, + size_t capacity, + int destination_device, + size_t offset) { + assertThereIsForcePairs("GetContactForcesToDevice"); + assertOwnerOffsetValid(offset, "GetContactForcesToDevice"); + return sys->GetOwnerContactForcesToDevice({obj->ownerID + static_cast(offset)}, points, forces, capacity, + destination_device); +} size_t DEMTracker::GetContactForcesForAll(std::vector& points, std::vector& forces) { assertThereIsForcePairs("GetContactForcesForAll"); @@ -564,6 +688,13 @@ size_t DEMTracker::GetContactForcesForAll(std::vector& points, std::vect forces.clear(); return sys->GetOwnerContactForces(GetOwnerIDs(), points, forces); } +size_t DEMTracker::GetContactForcesForAllToDevice(float3* points, + float3* forces, + size_t capacity, + int destination_device) { + assertThereIsForcePairs("GetContactForcesForAllToDevice"); + return sys->GetOwnerContactForcesToDevice(GetOwnerIDs(), points, forces, capacity, destination_device); +} size_t DEMTracker::GetContactForcesAndLocalTorque(std::vector& points, std::vector& forces, @@ -576,6 +707,17 @@ size_t DEMTracker::GetContactForcesAndLocalTorque(std::vector& points, torques.clear(); return sys->GetOwnerContactForces({obj->ownerID + (bodyID_t)offset}, points, forces, torques, true); } +size_t DEMTracker::GetContactForcesAndLocalTorqueToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + size_t offset) { + assertThereIsForcePairs("GetContactForcesAndLocalTorqueToDevice"); + assertOwnerOffsetValid(offset, "GetContactForcesAndLocalTorqueToDevice"); + return sys->GetOwnerContactForcesToDevice({obj->ownerID + static_cast(offset)}, points, forces, torques, + capacity, destination_device, true); +} size_t DEMTracker::GetContactForcesAndLocalTorqueForAll(std::vector& points, std::vector& forces, @@ -586,6 +728,15 @@ size_t DEMTracker::GetContactForcesAndLocalTorqueForAll(std::vector& poi torques.clear(); return sys->GetOwnerContactForces(GetOwnerIDs(), points, forces, torques, true); } +size_t DEMTracker::GetContactForcesAndLocalTorqueForAllToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device) { + assertThereIsForcePairs("GetContactForcesAndLocalTorqueForAllToDevice"); + return sys->GetOwnerContactForcesToDevice(GetOwnerIDs(), points, forces, torques, capacity, destination_device, + true); +} size_t DEMTracker::GetContactForcesAndGlobalTorque(std::vector& points, std::vector& forces, @@ -598,6 +749,17 @@ size_t DEMTracker::GetContactForcesAndGlobalTorque(std::vector& points, torques.clear(); return sys->GetOwnerContactForces({obj->ownerID + (bodyID_t)offset}, points, forces, torques, false); } +size_t DEMTracker::GetContactForcesAndGlobalTorqueToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + size_t offset) { + assertThereIsForcePairs("GetContactForcesAndGlobalTorqueToDevice"); + assertOwnerOffsetValid(offset, "GetContactForcesAndGlobalTorqueToDevice"); + return sys->GetOwnerContactForcesToDevice({obj->ownerID + static_cast(offset)}, points, forces, torques, + capacity, destination_device, false); +} size_t DEMTracker::GetContactForcesAndGlobalTorqueForAll(std::vector& points, std::vector& forces, @@ -608,6 +770,15 @@ size_t DEMTracker::GetContactForcesAndGlobalTorqueForAll(std::vector& po torques.clear(); return sys->GetOwnerContactForces(GetOwnerIDs(), points, forces, torques, false); } +size_t DEMTracker::GetContactForcesAndGlobalTorqueForAllToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device) { + assertThereIsForcePairs("GetContactForcesAndGlobalTorqueForAllToDevice"); + return sys->GetOwnerContactForcesToDevice(GetOwnerIDs(), points, forces, torques, capacity, destination_device, + false); +} void DEMTracker::AddAcc(float3 acc, size_t offset) { assertOwnerOffsetValid(offset, "AddAcc"); @@ -617,6 +788,9 @@ void DEMTracker::AddAcc(const std::vector& acc) { assertOwnerSize(acc.size(), "AddAcc"); sys->AddOwnerNextStepAcc(obj->ownerID, acc); } +void DEMTracker::AddAccFromDevice(const float3* source, int source_device, bool validate) { + sys->AddOwnerNextStepAccFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} void DEMTracker::AddAngAcc(float3 angAcc, size_t offset) { assertOwnerOffsetValid(offset, "AddAngAcc"); @@ -626,6 +800,9 @@ void DEMTracker::AddAngAcc(const std::vector& angAcc) { assertOwnerSize(angAcc.size(), "AddAngAcc"); sys->AddOwnerNextStepAngAcc(obj->ownerID, angAcc); } +void DEMTracker::AddAngAccFromDevice(const float3* source, int source_device, bool validate) { + sys->AddOwnerNextStepAngAccFromDevice(obj->ownerID, source, source_device, obj->nSpanOwners, validate); +} void DEMTracker::SetPos(float3 pos, size_t offset) { assertOwnerOffsetValid(offset, "SetPos"); @@ -671,16 +848,8 @@ void DEMTracker::SetFamily(unsigned int fam_num, size_t offset) { sys->SetOwnerFamily(obj->ownerID + offset, fam_num); } -void DEMTracker::ChangeClumpSizes(const std::vector& IDs, const std::vector& factors) { - std::vector offsetted_IDs(IDs); - size_t offset = obj->ownerID; - std::for_each(offsetted_IDs.begin(), offsetted_IDs.end(), [offset](bodyID_t& x) { x += offset; }); - sys->ChangeClumpSizes(offsetted_IDs, factors); -} - void DEMTracker::UpdateMesh(const std::vector& new_nodes) { assertMesh("UpdateMesh"); - // assertGeoSize(new_mesh->GetNumTriangles(), "UpdateMesh", "triangles"); // Outsource to API system to handle... sys->SetTriNodeRelPos(obj->ownerID, obj->geoID, new_nodes); } @@ -692,7 +861,7 @@ void DEMTracker::UpdateMeshByIncrement(const std::vector& deformation) { sys->UpdateTriNodeRelPos(obj->ownerID, obj->geoID, deformation); } -std::shared_ptr& DEMTracker::GetMesh() { +std::shared_ptr& DEMTracker::GetMesh() { assertMesh("GetMesh"); return sys->GetCachedMesh(obj->ownerID); } @@ -712,40 +881,6 @@ void DEMTracker::SetOwnerWildcardValues(const std::string& name, const std::vect sys->SetOwnerWildcardValue(obj->ownerID, name, wc); } -void DEMTracker::SetGeometryWildcardValue(const std::string& name, float wc, size_t offset) { - switch (obj->obj_type) { - case (OWNER_TYPE::CLUMP): - assertGeoOffsetValid(offset, "SetGeometryWildcardValue", "spheres"); - sys->SetSphereWildcardValue(obj->geoID + offset, name, std::vector(1, wc)); - break; - case (OWNER_TYPE::ANALYTICAL): - assertGeoOffsetValid(offset, "SetGeometryWildcardValue", "analytical components"); - sys->SetAnalWildcardValue(obj->geoID + offset, name, std::vector(1, wc)); - break; - case (OWNER_TYPE::MESH): - assertGeoOffsetValid(offset, "SetGeometryWildcardValue", "triangles"); - sys->SetTriWildcardValue(obj->geoID + offset, name, std::vector(1, wc)); - break; - } -} - -void DEMTracker::SetGeometryWildcardValues(const std::string& name, const std::vector& wc) { - switch (obj->obj_type) { - case (OWNER_TYPE::CLUMP): - assertGeoSize(wc.size(), "SetGeometryWildcardValues", "spheres"); - sys->SetSphereWildcardValue(obj->geoID, name, wc); - break; - case (OWNER_TYPE::ANALYTICAL): - assertGeoSize(wc.size(), "SetGeometryWildcardValues", "analytical components"); - sys->SetAnalWildcardValue(obj->geoID, name, wc); - break; - case (OWNER_TYPE::MESH): - assertGeoSize(wc.size(), "SetGeometryWildcardValues", "triangles"); - sys->SetTriWildcardValue(obj->geoID, name, wc); - break; - } -} - // ============================================================================= // DEMForceModel class // ============================================================================= @@ -806,9 +941,7 @@ void DEMForceModel::SetPerContactWildcards(const std::set& wildcard for (const auto& a_str : wildcards) { if (match_pattern(a_str, " ")) { // Wildcard array names cannot have spaces in them - std::stringstream ss; - ss << "Contact wildcard " << a_str << " is not valid: no spaces allowed in its name." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("Contact wildcard %s is not valid: no spaces allowed in its name.", a_str.c_str()); } } m_contact_wildcards = wildcards; @@ -817,23 +950,10 @@ void DEMForceModel::SetPerContactWildcards(const std::set& wildcard void DEMForceModel::SetPerOwnerWildcards(const std::set& wildcards) { for (const auto& a_str : wildcards) { if (match_pattern(a_str, " ")) { - std::stringstream ss; - ss << "Owner wildcard " << a_str << " is not valid: no spaces allowed in its name." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("Owner wildcard %s is not valid: no spaces allowed in its name.", a_str.c_str()); } } m_owner_wildcards = wildcards; } -void DEMForceModel::SetPerGeometryWildcards(const std::set& wildcards) { - for (const auto& a_str : wildcards) { - if (match_pattern(a_str, " ")) { - std::stringstream ss; - ss << "Geometry wildcard " << a_str << " is not valid: no spaces allowed in its name." << std::endl; - throw std::runtime_error(ss.str()); - } - } - m_geo_wildcards = wildcards; -} - } // END namespace deme diff --git a/src/DEM/AuxClasses.h b/src/DEM/AuxClasses.h index 09baada1..6809a1c0 100644 --- a/src/DEM/AuxClasses.h +++ b/src/DEM/AuxClasses.h @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: BSD-3-Clause -#ifndef DEME_INSPECTOR_HPP -#define DEME_INSPECTOR_HPP +#ifndef DEME_AUX_CLASSES_HPP +#define DEME_AUX_CLASSES_HPP #include @@ -20,7 +20,7 @@ class DEMDynamicThread; /// their simulation entites, in a given region. class DEMInspector { private: - std::shared_ptr inspection_kernel; + std::shared_ptr inspection_kernel; std::string inspection_code; std::string in_region_code; @@ -38,6 +38,10 @@ class DEMInspector { DEMSolver* sys; DEMDynamicThread* dT; + // Result storage arrays (moved from dT to make inspector self-contained and thread-safe) + DualArray m_reduceResArr; + DualArray m_reduceRes; + // Based on user input... void switch_quantity_type(const std::string& quantity); @@ -59,7 +63,7 @@ class DEMInspector { all_domain = false; } //// TODO: Another overload for region definition - ~DEMInspector() {} + ~DEMInspector() { ReleaseData(); } void SetInspectionCode(const std::string& code) { inspection_code = code; } @@ -74,17 +78,31 @@ class DEMInspector { /// Get the values of the quantity that you wish to inspect (for non-reduced inspections). float* GetValues(); + /// Get the reduce value of the quantity that you wish to inspect (returns device pointer). + float GetDeviceValue(); + + /// Get the values of the quantity that you wish to inspect (for non-reduced inspections, returns device pointer). + float* GetDeviceValues(); + /// Get the value (as a vector) of the quantity that you wish to inspect // std::vector GetVector(); /// Get value directly within dT (returns host pointer by default) float* dT_GetValue(); - /// Get value directly within dT (returns device pointer) - float* dT_GetDeviceValue(); + /// Get values on device without forcing a host sync (internal dT worker use). + float* dT_GetDeviceValues(); + + /// Manually release the data arrays + void ReleaseData(); }; -// A struct to get or set tracked owner entities, mainly for co-simulation +/// A helper for querying or modifying tracked owners, mainly for co-simulation. +/// +/// Fixed-size `ToDevice` and `FromDevice` methods validate owner ranges, capacities, and CUDA pointer metadata by +/// default. Their trailing `validate` argument may be set to false when the caller guarantees those preconditions and +/// needs to avoid validation overhead. Required device-routing constraints and the documented data transformations, +/// including quaternion normalization, still apply when validation is disabled. class DEMTracker { private: void assertMesh(const std::string& name); @@ -114,6 +132,10 @@ class DEMTracker { /// Get the positions of all tracked objects. std::vector Positions(); std::vector> GetPositions(); + /// Fill CUDA-accessible memory with all tracked positions. Capacity is measured in float3 elements. + void PositionsToDevice(float3* destination, size_t capacity, int destination_device, bool validate = true); + /// Synchronously set all tracked global positions from CUDA memory. + void SetPositionsFromDevice(const float3* source, int source_device, bool validate = true); /// Get the angular velocity of this tracked object in its own local coordinate system. Applying OriQ to it would /// give you the ang vel in global frame. @@ -123,6 +145,13 @@ class DEMTracker { /// give you the ang vel in global frame. std::vector AngularVelocitiesLocal(); std::vector> GetAngularVelocitiesLocal(); + /// Fill CUDA-accessible memory with all tracked local-frame angular velocities. + void AngularVelocitiesLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate = true); + /// Synchronously set all tracked local-frame angular velocities from CUDA memory. + void SetAngularVelocitiesFromDevice(const float3* source, int source_device, bool validate = true); /// Get the angular velocity of this tracked object in global coordinate system. float3 AngVelGlobal(size_t offset = 0); @@ -130,6 +159,13 @@ class DEMTracker { /// Get the angular velocity of all objects tracked by this tracker, in global coordinate system. std::vector AngularVelocitiesGlobal(); std::vector> GetAngularVelocitiesGlobal(); + /// Fill CUDA-accessible memory with all tracked global-frame angular velocities. + void AngularVelocitiesGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate = true); + /// Synchronously set all tracked global angular velocities from CUDA memory. + void SetAngularVelocitiesGlobalFromDevice(const float3* source, int source_device, bool validate = true); /// Get the velocity of this tracked object in global frame. float3 Vel(size_t offset = 0); @@ -137,6 +173,10 @@ class DEMTracker { /// Get the velocities of all objects tracked by this tracker, in global frame. std::vector Velocities(); std::vector> GetVelocities(); + /// Fill CUDA-accessible memory with all tracked global-frame velocities. + void VelocitiesToDevice(float3* destination, size_t capacity, int destination_device, bool validate = true); + /// Synchronously set all tracked global linear velocities from CUDA memory. + void SetVelocitiesFromDevice(const float3* source, int source_device, bool validate = true); /// Get the quaternion that represents the orientation of this tracked object's own coordinate system. float4 OriQ(size_t offset = 0); @@ -150,6 +190,13 @@ class DEMTracker { /// @return A vector of 4-float vectors. The order is (x, y, z, w). If compared against Chrono naming convention, /// then it is saying our ordering here is (e1, e2, e3, e0). std::vector> GetOrientationQuaternions(); + /// Fill CUDA-accessible memory with all tracked public-order (x, y, z, w) quaternions. + void OrientationQuaternionsToDevice(float4* destination, + size_t capacity, + int destination_device, + bool validate = true); + /// Synchronously set and normalize all tracked public-order (x, y, z, w) quaternions from CUDA memory. + void SetOrientationQuaternionsFromDevice(const float4* source, int source_device, bool validate = true); /// @brief Get the family number of the tracked object. /// @param offset The offset of the entites to get family number out of. @@ -158,6 +205,8 @@ class DEMTracker { /// @brief Get the family numbers of all the tracked object. /// @return The family numbers as a vector. std::vector GetFamilies(); + /// Fill CUDA-accessible memory with all tracked family numbers as unsigned integers. + void FamiliesToDevice(unsigned int* destination, size_t capacity, int destination_device, bool validate = true); /// @brief Get the clumps that are in contact with this tracked owner as a vector. /// @details No bulk version that gets the contacting clumps for all the entities tracked by this tracker. This is @@ -178,6 +227,11 @@ class DEMTracker { /// @details In most cases, this means the acceleration excluding the gravitational acceleration. std::vector ContactAccelerations(); std::vector> GetContactAccelerations(); + /// Fill CUDA-accessible memory with all tracked global-frame contact accelerations. + void ContactAccelerationsToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate = true); /// @brief Get the a portion of the angular acceleration of this tracked object, that is the result of its contact /// with other simulation entities. The acceleration is in this object's local frame. @@ -189,6 +243,11 @@ class DEMTracker { /// @details In most cases, this means the angular acceleration excluding the gravitational acceleration. std::vector ContactAngularAccelerationsLocal(); std::vector> GetContactAngularAccelerationsLocal(); + /// Fill CUDA-accessible memory with all tracked local-frame contact angular accelerations. + void ContactAngularAccelerationsLocalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate = true); /// @brief Get the a portion of the angular acceleration of this tracked object, that is the result of its contact /// with other simulation entities. The acceleration is in this object's global frame. @@ -200,6 +259,35 @@ class DEMTracker { /// @details In most cases, this means the angular acceleration excluding the gravitational acceleration. std::vector ContactAngularAccelerationsGlobal(); std::vector> GetContactAngularAccelerationsGlobal(); + /// Fill CUDA-accessible memory with all tracked global-frame contact angular accelerations. + void ContactAngularAccelerationsGlobalToDevice(float3* destination, + size_t capacity, + int destination_device, + bool validate = true); + /// @brief Get one reduced contact wrench for every owner tracked by this tracker. + /// @details Forces are global-frame contact-force resultants. Torques are global-frame moments about each owner's + /// current DEME position and include force-generated moments plus force-model-only torque such as rolling + /// resistance. The vectors preserve tracker owner order and contain zero for owners without recorded contact. This + /// reads the current dT force records without triggering contact detection or force evaluation. Contact recording + /// must remain enabled (the default). + /// @param forces Output resultant forces, resized to the number of tracked owners. + /// @param torques Output resultant torques, resized to the number of tracked owners. + void ContactWrenches(std::vector& forces, std::vector& torques); + /// @brief Write one reduced contact wrench per tracked owner directly to CUDA memory. + /// @details This is the device-output counterpart of ContactWrenches and preserves tracker owner order. Forces are + /// global-frame contact-force resultants. Torques are global-frame moments about each owner's current DEME position + /// and include force-generated moments plus force-model-only torque. Owners without recorded contact receive a zero + /// wrench. This reads current dT force records without triggering contact detection or force evaluation. Contact + /// recording must remain enabled. The call is synchronous; CUDA selects the available inter-device transfer route + /// for cross-device output. + /// @param force_destination Writable CUDA memory for one float3 force per tracked owner. + /// @param torque_destination Writable CUDA memory for one float3 torque per tracked owner. + /// @param capacity Available elements in each destination buffer; must cover every tracked owner. + /// @param destination_device Logical CUDA device owning both destination buffers. + void ContactWrenchesToDevice(float3* force_destination, + float3* torque_destination, + size_t capacity, + int destination_device); /// @brief Get the mass of the tracked object. /// @param offset The offset to this entites. If first entites, input 0. @@ -208,6 +296,8 @@ class DEMTracker { /// @brief Get the masses of all the tracked objects. /// @return Masses as a vector. std::vector Masses(); + /// Fill CUDA-accessible memory with all tracked masses. + void MassesToDevice(float* destination, size_t capacity, int destination_device, bool validate = true); /// @brief Get the moment of inertia (in principal axis frame) of the tracked object. /// @param offset The offset to this entites. If first entites, input 0. /// @return The moment of inertia (in principal axis frame). @@ -217,6 +307,8 @@ class DEMTracker { /// @return The moment of inertia (in principal axis frame) of each element as a vector. std::vector MOIs(); std::vector> GetMOIs(); + /// Fill CUDA-accessible memory with all tracked principal moments of inertia. + void MOIsToDevice(float3* destination, size_t capacity, int destination_device, bool validate = true); /// Get the owner's wildcard value. float GetOwnerWildcardValue(const std::string& name, size_t offset = 0); @@ -224,15 +316,12 @@ class DEMTracker { /// @param name Name of the wildcard. /// @return All the values. std::vector GetOwnerWildcardValues(const std::string& name); - /// @brief Get the geometry wildcard values for all the geometry entities tracked by this tracker. - /// @param name Name of the wildcard. - /// @return All the values. - std::vector GetGeometryWildcardValues(const std::string& name); - /// @brief Get the geometry wildcard values for a geometry entity tracked by this tracker. - /// @param name Name of the wildcard. - /// @param offset The offset to this entity (where to start the modification). If first entity, input 0. - /// @return The value. - float GetGeometryWildcardValue(const std::string& name, size_t offset); + /// Fill CUDA-accessible memory with one wildcard value for every tracked owner. + void OwnerWildcardValuesToDevice(const std::string& name, + float* destination, + size_t capacity, + int destination_device, + bool validate = true); /// @brief Set the position of this tracked object. void SetPos(float3 pos, size_t offset = 0); @@ -260,6 +349,15 @@ class DEMTracker { /// Add an extra acc to n consecutive tracked objects, (only) for the next time step. Note if the user intends to /// add a persistent external force, then using family prescription is the better method. void AddAcc(const std::vector& acc); + /// @brief Queue one global-frame linear acceleration per tracked owner directly from CUDA memory. + /// @details This is the device-input counterpart of the vector AddAcc overload. Values preserve tracker owner + /// order and replace any previously queued next-step linear-acceleration contribution for those owners. Contact + /// acceleration is accumulated on top during the next force/integration step, gravity is applied separately, and + /// the queued contribution is consumed after one step. The call is synchronous. + /// @param source CUDA memory containing one float3 global-frame acceleration per tracked owner. + /// @param source_device Logical CUDA device owning `source`; remote input is copied to dT before unpacking. + /// @param validate Whether to validate the owner range and CUDA pointer metadata. + void AddAccFromDevice(const float3* source, int source_device, bool validate = true); /// Add an extra angular acceleration to the tracked body, (only) for the next time step. Note if the user intends /// to add a persistent external torque, then using family prescription is the better method. @@ -267,6 +365,15 @@ class DEMTracker { /// Add an extra angular acceleration to n consecutive tracked objects, (only) for the next time step. Note if the /// user intends to add a persistent external torque, then using family prescription is the better method. void AddAngAcc(const std::vector& angAcc); + /// @brief Queue one local-frame angular acceleration per tracked owner directly from CUDA memory. + /// @details This is the device-input counterpart of the vector AddAngAcc overload. Values preserve tracker owner + /// order, use each owner's local principal-axis frame, and replace any previously queued next-step angular- + /// acceleration contribution. Contact angular acceleration is accumulated on top during the next force/integration + /// step, and the queued contribution is consumed after one step. The call is synchronous. + /// @param source CUDA memory containing one float3 local-frame angular acceleration per tracked owner. + /// @param source_device Logical CUDA device owning `source`; remote input is copied to dT before unpacking. + /// @param validate Whether to validate the owner range and CUDA pointer metadata. + void AddAngAccFromDevice(const float3* source, int source_device, bool validate = true); /// @brief Change the family numbers of all the entities tracked by this tracker. /// @param fam_num Family number to change to. @@ -277,9 +384,6 @@ class DEMTracker { void SetFamily(unsigned int fam_num, size_t offset); //// TODO: Maybe give a vector-based tracker family number modification method, like what DEMInitializer allows for. - /// Change the size of clump entities - void ChangeClumpSizes(const std::vector& IDs, const std::vector& factors); - /// @brief Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes. /// @details This affects triangle facets' relative positions wrt the mesh center (CoM) only; mesh's overall /// position/rotation in simulation is not affected. So if provided input is the new mesh location with @@ -302,7 +406,7 @@ class DEMTracker { /// @brief Get a handle for the mesh this tracker is tracking. /// @return Pointer to the mesh. - std::shared_ptr& GetMesh(); + std::shared_ptr& GetMesh(); /// @brief Get the current locations of all the nodes in the mesh being tracked. /// @return A vector of float3 representing the global coordinates of the mesh nodes. std::vector GetMeshNodesGlobal(); @@ -317,16 +421,6 @@ class DEMTracker { /// @param wc Wildcard values as a vector (must have same length as the number of tracked owners). void SetOwnerWildcardValues(const std::string& name, const std::vector& wc); - /// @brief Set a wildcard value of the geometry entity this tracker is tracking. - /// @param name Name of the wildcard. - /// @param wc Wildcard value. - /// @param offset The offset to this entity (where to start the modification). If first entity, input 0. - void SetGeometryWildcardValue(const std::string& name, float wc, size_t offset = 0); - /// @brief Set a wildcard value of the geometry entities this tracker is tracking. - /// @param name Name of the wildcard. - /// @param wc Wildcard values as a vector (must have same length as the number of tracked geometry entities). - void SetGeometryWildcardValues(const std::string& name, const std::vector& wc); - /// @brief Get all contact forces that concern this track object, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object /// experiences. @@ -335,6 +429,12 @@ class DEMTracker { /// @param offset The offset to this owner (where to start querying). If first entity, input 0. /// @return Number of force pairs. size_t GetContactForces(std::vector& points, std::vector& forces, size_t offset = 0); + /// Fill CUDA memory with contact points/forces for one tracked owner. Capacity must cover the total contact count. + size_t GetContactForcesToDevice(float3* points, + float3* forces, + size_t capacity, + int destination_device, + size_t offset = 0); /// @brief Get all contact forces that concern all objects tracked by this tracker, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object @@ -344,10 +444,12 @@ class DEMTracker { /// @param forces The force in XYZ as float3 vector. The force in global frame. /// @return Number of force pairs. size_t GetContactForcesForAll(std::vector& points, std::vector& forces); + /// Fill CUDA memory with contact points/forces concerning all tracked owners. + size_t GetContactForcesForAllToDevice(float3* points, float3* forces, size_t capacity, int destination_device); /// @brief Get all contact forces and global torques that concern this track object, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object - /// experiences. Since we are getting all force pairs, the torque should be considered as `extra torque', since you + /// experiences. Since we are getting all force pairs, the torque should be considered as "extra torque", since you /// should be able to derive the normal and tangential force-induced torques based on all the force pairs. The extra /// torques emerge depending on your force model. For example, in the default force model, rolling friction could /// contribute to the torque. But if you do not have rolling friction, then you do not have torque here. The torques @@ -361,10 +463,17 @@ class DEMTracker { std::vector& forces, std::vector& torques, size_t offset = 0); + /// Fill CUDA memory with contact points, forces, and global extra torques for one tracked owner. + size_t GetContactForcesAndGlobalTorqueToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + size_t offset = 0); /// @brief Get all contact forces and global torques that concern all objects tracked by this tracker, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object - /// experiences. Since we are getting all force pairs, the torque should be considered as `extra torque', since you + /// experiences. Since we are getting all force pairs, the torque should be considered as "extra torque", since you /// should be able to derive the normal and tangential force-induced torques based on all the force pairs. The extra /// torques emerge depending on your force model. For example, in the default force model, rolling friction could /// contribute to the torque. But if you do not have rolling friction, then you do not have torque here. The torques @@ -378,10 +487,16 @@ class DEMTracker { size_t GetContactForcesAndGlobalTorqueForAll(std::vector& points, std::vector& forces, std::vector& torques); + /// Fill CUDA memory with contact points, forces, and global extra torques for all tracked owners. + size_t GetContactForcesAndGlobalTorqueForAllToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device); /// @brief Get all contact forces and local torques that concern this track object, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object - /// experiences. Since we are getting all force pairs, the torque should be considered as `extra torque', since you + /// experiences. Since we are getting all force pairs, the torque should be considered as "extra torque", since you /// should be able to derive the normal and tangential force-induced torques based on all the force pairs. The extra /// torques emerge depending on your force model. For example, in the default force model, rolling friction could /// contribute to the torque. But if you do not have rolling friction, then you do not have torque here. The torques @@ -395,10 +510,17 @@ class DEMTracker { std::vector& forces, std::vector& torques, size_t offset = 0); + /// Fill CUDA memory with contact points, forces, and local extra torques for one tracked owner. + size_t GetContactForcesAndLocalTorqueToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + size_t offset = 0); /// @brief Get all contact forces and local torques that concern all objects tracked by this tracker, as a vector. /// @details Every force pair will be queried using this function, instead of a reduced total force that this object - /// experiences. Since we are getting all force pairs, the torque should be considered as `extra torque', since you + /// experiences. Since we are getting all force pairs, the torque should be considered as "extra torque", since you /// should be able to derive the normal and tangential force-induced torques based on all the force pairs. The extra /// torques emerge depending on your force model. For example, in the default force model, rolling friction could /// contribute to the torque. But if you do not have rolling friction, then you do not have torque here. The torques @@ -412,6 +534,12 @@ class DEMTracker { size_t GetContactForcesAndLocalTorqueForAll(std::vector& points, std::vector& forces, std::vector& torques); + /// Fill CUDA memory with contact points, forces, and local extra torques for all tracked owners. + size_t GetContactForcesAndLocalTorqueForAllToDevice(float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device); }; class DEMForceModel { @@ -434,9 +562,6 @@ class DEMForceModel { // Quatity names that we want to associate each owner with. An array will be allocated for storing this, and it // lives and die with its associated owner. std::set m_owner_wildcards; - // Quatity names that we want to associate each owner with. An array will be allocated for storing this, and it - // lives and die with its associated geometry representation (most typically a sphere). - std::set m_geo_wildcards; public: friend class DEMSolver; @@ -474,12 +599,8 @@ class DEMForceModel { /// Set the names for the extra quantities that will be associated with each owner. For example, you can use this to /// associate a cohesion parameter to each particle. Only float is supported. void SetPerOwnerWildcards(const std::set& wildcards); - /// Set the names for the extra quantities that will be associated with each geometry. For example, you can use this - /// to associate certain electric charges to each particle's each component which represents a distribution of the - /// charges. Only float is supported. - void SetPerGeometryWildcards(const std::set& wildcards); }; } // END namespace deme -#endif \ No newline at end of file +#endif diff --git a/src/DEM/BdrsAndObjs.h b/src/DEM/BdrsAndObjs.h index 91cf1fe8..e9afce8c 100644 --- a/src/DEM/BdrsAndObjs.h +++ b/src/DEM/BdrsAndObjs.h @@ -13,17 +13,18 @@ #include #include #include +#include #include "../kernel/DEMHelperKernels.cuh" #include "Defines.h" #include "Structs.h" #include "../core/utils/CudaAllocator.hpp" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" namespace deme { /// External object type -/// Note all of them are `shell', not solid objects. If you need a solid cylinder for example, then use one CYLINDER as +/// Note all of them are "shell" objects, not solid objects. If you need a solid cylinder, use one CYLINDER as /// the side plus 2 CIRCLE as the ends to emulate it. Please be sure to set OUTWARD CYLINDER normal in this case. enum class OBJ_COMPONENT { PLANE, SPHERE, PLATE, CIRCLE, CYL, CYL_INF, CONE, CONE_INF, TRIANGLE }; @@ -33,8 +34,7 @@ struct DEMSphereParams_t { objNormal_t normal; }; -/// Cone side. `cone_tip' is the apex location, `dir' points from the apex toward the base, and `slope' is radius -/// growth per unit axial distance. +/// Cone side. `cone_tip` is the apex, `dir` points toward the base, and `slope` is radial growth per axial distance. struct DEMConeParams_t { float3 cone_tip; float3 dir; @@ -66,25 +66,20 @@ struct DEMCylinderParams_t { objNormal_t normal; }; +/// Validate the geometric parameters shared by infinite and bounded analytical cones. inline void assertConeInputs(const float3 axis, const float slope, const float hmin, const float hmax, - const std::string& func_name) { + const std::string& function_name) { if (!std::isfinite(axis.x) || !std::isfinite(axis.y) || !std::isfinite(axis.z) || length(axis) <= DEME_TINY_FLOAT) { - std::stringstream out; - out << func_name << "'s axis argument must be a finite, non-zero direction.\n"; - throw std::runtime_error(out.str()); + DEME_ERROR("%s's axis argument must be a finite, non-zero direction.", function_name.c_str()); } if (!std::isfinite(slope) || slope <= 0.0f) { - std::stringstream out; - out << func_name << "'s slope argument must be finite and positive.\n"; - throw std::runtime_error(out.str()); + DEME_ERROR("%s's slope argument must be finite and positive.", function_name.c_str()); } if (!std::isfinite(hmin) || !std::isfinite(hmax) || hmin < 0.0f || hmax <= hmin) { - std::stringstream out; - out << func_name << "'s axial bounds must be finite and satisfy 0 <= hmin < hmax.\n"; - throw std::runtime_error(out.str()); + DEME_ERROR("%s's axial bounds must be finite and satisfy 0 <= hmin < hmax.", function_name.c_str()); } } @@ -99,7 +94,7 @@ class DEMExternObj : public DEMInitializer { // Component object materials std::vector> materials; // Family code (used in prescribing its motions etc.) - unsigned int family_code = RESERVED_FAMILY_NUM; ///< Means it is default to the `fixed' family + unsigned int family_code = RESERVED_FAMILY_NUM; ///< Means it defaults to the "fixed" family // // The coordinate of the CoM of this external object, in the frame where all its components' properties are // // reported. This is usually all-0 (meaning you should define the object's components in its CoM frame to begin @@ -130,22 +125,26 @@ class DEMExternObj : public DEMInitializer { /// Define object contact family number void SetFamily(const unsigned int code) { if (code > std::numeric_limits::max()) { - std::stringstream ss; - ss << "An external object is instructed to have family number " << code - << ", which is larger than the max allowance " << std::numeric_limits::max() << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "An external object is instructed to have family number %u, which is larger than the max " + "allowance %u", + code, std::numeric_limits::max()); } family_code = code; } /// Set mass void SetMass(float mass) { this->mass = mass; } + /// Get mass + float GetMass() const { return mass; } /// Set MOI (in principal frame) void SetMOI(float3 MOI) { this->MOI = MOI; } void SetMOI(const std::vector& MOI) { assertThreeElements(MOI, "SetMOI", "MOI"); SetMOI(make_float3(MOI[0], MOI[1], MOI[2])); } + /// Get MOI (in principal frame) + float3 GetMOI() const { return MOI; } /// @brief Set the initial quaternion for this object (before simulation initializes). /// @param rotQ Initial quaternion. @@ -250,11 +249,7 @@ class DEMExternObj : public DEMInitializer { AddCylinder(make_float3(pos[0], pos[1], pos[2]), make_float3(axis[0], axis[1], axis[2]), rad, material, normal); } - /// Add a single-nappe cone side that starts at `tip' and extends indefinitely along `axis'. - /// - /// The cone is an analytical shell, like analytical cylinders. `slope' is radius per unit axial distance from the - /// tip. Use ENTITY_NORMAL_INWARD for particles inside the cone and ENTITY_NORMAL_OUTWARD for particles outside a - /// solid cone. + /// Add an analytical single-nappe cone side extending indefinitely from `tip` along `axis`. void AddCone(const float3 tip, const float3 axis, const float slope, @@ -282,10 +277,7 @@ class DEMExternObj : public DEMInitializer { AddCone(make_float3(tip[0], tip[1], tip[2]), make_float3(axis[0], axis[1], axis[2]), slope, material, normal); } - /// Add a height-bounded cone side. The surface is clipped to hmin <= dot(P - tip, axis) <= hmax. - /// - /// This creates the exact conical side/rim shell only; add analytical planes separately when cap contacts are - /// needed. Setting hmin > 0 gives a conical frustum side without introducing mesh facets. + /// Add a cone or frustum side clipped to hmin <= dot(point - tip, axis) <= hmax; caps are not included. void AddConeSegment(const float3 tip, const float3 axis, const float slope, @@ -320,21 +312,28 @@ class DEMExternObj : public DEMInitializer { }; // DEM mesh object -class DEMMeshConnected : public DEMInitializer { +class DEMMesh : public DEMInitializer { private: - void assertLength(size_t len, const std::string name) { + void assertPatchLength(size_t len, const std::string name) { + if (len != nPatches) { + DEME_ERROR( + "%s input argument must have length %u (not %zu), same as the number of convex patches in the " + "mesh.", + name.c_str(), nPatches, len); + } + } + + void assertTriLength(size_t len, const std::string name) { if (nTri == 0) { - std::cerr << "The settings at the " << name << " call were applied to 0 mesh facet.\nPlease consider using " - << name - << " only after loading the mesh file, because mesh utilities are supposed to provide per-facet " - "control of your mesh, so we need to know the mesh first." - << std::endl; + DEME_WARNING( + "The settings at the %s call were applied to 0 mesh facet.\nPlease consider using " + "%s only after loading the mesh file, because mesh utilities are supposed to provide per-facet " + "control of your mesh, so we need to know the mesh first.", + name.c_str(), name.c_str()); } if (len != nTri) { - std::stringstream ss; - ss << name << " input argument must have length " << nTri << " (not " << len - << "), same as the number of triangle facets in the mesh." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("%s input argument must have length %zu (not %zu), same as the number of triangles in the mesh.", + name.c_str(), nTri, len); } } @@ -384,7 +383,7 @@ class DEMMeshConnected : public DEMInitializer { std::vector> materials; bool isMaterialSet = false; // Family code (used in prescribing its motions etc.) - unsigned int family_code = RESERVED_FAMILY_NUM; ///< Means it is default to the `fixed' family + unsigned int family_code = RESERVED_FAMILY_NUM; ///< Means it defaults to the "fixed" family // // The coordinate of the CoM of this meshed object, in the frame where all the mesh's node coordinates are // // reported. This is usually all-0 (meaning you should define the object's components in its CoM frame to begin @@ -402,33 +401,53 @@ class DEMMeshConnected : public DEMInitializer { float mass = 1.f; // Mesh's MOI float3 MOI = make_float3(1.f); + // Whether mass/MOI were explicitly specified by the user. + bool mass_specified = false; + bool moi_specified = false; + // API-level mesh template identity. Mesh instances created from LoadMeshType/AddMeshFromTemplate keep this mark so + // mass/MOI jitification can treat repeated mesh particles like repeated clump templates. + size_t mesh_template_mark = NULL_MESH_TEMPLATE_MARK; + // If true, this mesh is treated as a shell surface with finite thickness in mesh utilities. + bool is_shell = false; + // Physical shell thickness (full thickness, not half-thickness), in simulation length unit. + float shell_thickness = 0.f; std::string filename; ///< file string if loading an obj file // If true, when the mesh is initialized into the system, it will re-order the nodes of each triangle so that the // normals derived from right-hand-rule are the same as the normals in the mesh file bool use_mesh_normals = false; + // If true, this mesh is treated as convex for contact island reduction. + bool is_convex = false; + // If true, this mesh is never selected as the winner side for island labeling. + bool never_winner = false; - DEMMeshConnected() { obj_type = OWNER_TYPE::MESH; } - DEMMeshConnected(std::string input_file) { + DEMMesh() { obj_type = OWNER_TYPE::MESH; } + DEMMesh(std::string input_file) { LoadWavefrontMesh(input_file); obj_type = OWNER_TYPE::MESH; } - DEMMeshConnected(std::string input_file, const std::shared_ptr& mat) { + DEMMesh(std::string input_file, const std::shared_ptr& mat) { LoadWavefrontMesh(input_file); SetMaterial(mat); obj_type = OWNER_TYPE::MESH; } - ~DEMMeshConnected() {} + ~DEMMesh() {} + + /// Load a triangle mesh saved as an STL file (ASCII or binary) + bool LoadSTLMesh(std::string input_file, bool load_normals = true); + + /// Load a triangle mesh saved as a PLY file (ASCII, triangulated or polygonal) + bool LoadPLYMesh(std::string input_file, bool load_normals = true); /// Load a triangle mesh saved as a Wavefront .obj file bool LoadWavefrontMesh(std::string input_file, bool load_normals = true, bool load_uv = false); /// Write the specified meshes in a Wavefront .obj file - static void WriteWavefront(const std::string& filename, std::vector& meshes); + static void WriteWavefront(const std::string& filename, std::vector& meshes); /// Utility function for merging multiple meshes. - static DEMMeshConnected Merge(std::vector& meshes); + static DEMMesh Merge(std::vector& meshes); /// Get the number of triangles already added to this mesh size_t GetNumTriangles() const { return nTri; } @@ -439,6 +458,33 @@ class DEMMeshConnected : public DEMInitializer { /// Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle so that /// the normals derived from right-hand-rule are the same as the normals in the mesh file void UseNormals(bool use = true) { use_mesh_normals = use; } + /// Mark this mesh as convex for contact reduction purposes. + void SetConvex(bool convex = true) { is_convex = convex; } + /// Query whether this mesh is marked convex. + bool IsConvex() const { return is_convex; } + /// Prevent this mesh from ever being chosen as the winner side in island labeling. + void SetNeverWinner(bool never = true) { never_winner = never; } + /// Query whether this mesh is marked as never-winner. + bool IsNeverWinner() const { return never_winner; } + /// Treat this mesh as a shell surface with finite thickness. Thickness must be finite and non-negative. + void SetShellThickness(float thickness) { + if (!std::isfinite(thickness) || thickness < 0.f) { + DEME_ERROR("Shell thickness must be finite and non-negative (got %.9g).", thickness); + } + shell_thickness = thickness; + is_shell = thickness > DEME_TINY_FLOAT; + } + /// Disable shell mode (fallback to zero-thickness triangle surface behavior). + void DisableShell() { + is_shell = false; + shell_thickness = 0.f; + } + /// Query whether this mesh is configured as a shell. + bool IsShell() const { return is_shell; } + /// Get full shell thickness. + float GetShellThickness() const { return shell_thickness; } + /// Get half shell thickness (used internally by kernels). + float GetShellHalfThickness() const { return (is_shell && shell_thickness > 0.f) ? 0.5f * shell_thickness : 0.f; } /// Access the n-th triangle in mesh DEMTriangle GetTriangle(size_t index) const { // No need to wrap (for Shlok) @@ -456,36 +502,61 @@ class DEMMeshConnected : public DEMInitializer { this->m_face_n_indices.clear(); this->m_face_uv_indices.clear(); this->m_face_col_indices.clear(); + this->m_patch_ids.clear(); + this->m_patch_locations.clear(); + this->nPatches = 1; + this->patches_explicitly_set = false; + this->patch_locations_explicitly_set = false; this->owner = NULL_BODYID; + this->mesh_template_mark = NULL_MESH_TEMPLATE_MARK; } /// Set mass. - void SetMass(float mass) { this->mass = mass; } + void SetMass(float mass) { + this->mass = mass; + this->mass_specified = true; + } + /// Get mass. + float GetMass() const { return mass; } /// Set MOI (in principal frame). - void SetMOI(float3 MOI) { this->MOI = MOI; } + void SetMOI(float3 MOI) { + this->MOI = MOI; + this->moi_specified = true; + } /// Set MOI (in principal frame). void SetMOI(const std::vector& MOI) { assertThreeElements(MOI, "SetMOI", "MOI"); SetMOI(make_float3(MOI[0], MOI[1], MOI[2])); } + /// Get MOI (in principal frame). + float3 GetMOI() const { return MOI; } /// Set mesh family number. void SetFamily(unsigned int num) { this->family_code = num; } - /// Set material types for the mesh. Technically, you can set that for each individual mesh facet. + /// @brief Set material types for the mesh. The input vector should have the same length as the number of patches in + /// the mesh, and each element is the material for that patch. + /// @details This allows you to set different materials for different patches of the mesh, which can be useful if + /// your mesh has multiple convex patches with different material properties. void SetMaterial(const std::vector>& input) { - assertLength(input.size(), "SetMaterial"); + assertPatchLength(input.size(), "SetMaterial"); materials = input; isMaterialSet = true; } - /// Set material types for the mesh. Technically, you can set that for each individual mesh facet. + /// @brief Set material types for the mesh. Using this method makes a uniform-materialed mesh. void SetMaterial(const std::shared_ptr& input) { - SetMaterial(std::vector>(nTri, input)); + SetMaterial(std::vector>(nPatches, input)); } - /* - /// Compute barycenter, mass and MOI in CoM frame - void ComputeMassProperties(double& mass, float3& center, float3& inertia); + /// Compute volume, centroid and MOI in CoM frame (unit density). + /// For shells (`SetShellThickness`), uses a centered-thickening shell model: volume = surface area * thickness. + void ComputeMassProperties(double& volume, float3& center, float3& inertia) const; + /// Compute volume, centroid and full inertia tensor in CoM frame (unit density). + /// `inertia_products` stores tensor terms (Ixy, Iyz, Izx). + void ComputeMassProperties(double& volume, float3& center, float3& inertia, float3& inertia_products) const; + /// Check if mesh is watertight (closed, manifold). Returns true if no boundary/non-manifold edges. + bool IsWatertight(size_t* boundary_edges = nullptr, size_t* nonmanifold_edges = nullptr) const; + /* /// Create a map of neighboring triangles, vector of: /// [Ti TieA TieB TieC] /// (the free sides have triangle id = -1). @@ -516,6 +587,13 @@ class DEMMeshConnected : public DEMInitializer { for (auto& node : m_vertices) { applyFrameTransformGlobalToLocal(node, center, prin_Q); } + for (auto& normal : m_normals) { + applyOriQToVector3(normal, make_float4(-prin_Q.x, -prin_Q.y, -prin_Q.z, prin_Q.w)); + const float n_len = length(normal); + if (n_len > DEME_TINY_FLOAT) { + normal /= n_len; + } + } } void InformCentroidPrincipal(const std::vector& center, const std::vector& prin_Q) { assertThreeElements(center, "InformCentroidPrincipal", "center"); @@ -531,6 +609,13 @@ class DEMMeshConnected : public DEMInitializer { for (auto& node : m_vertices) { applyFrameTransformLocalToGlobal(node, vec, rot_Q); } + for (auto& normal : m_normals) { + applyOriQToVector3(normal, rot_Q); + const float n_len = length(normal); + if (n_len > DEME_TINY_FLOAT) { + normal /= n_len; + } + } } void Move(const std::vector& vec, const std::vector& rot_Q) { assertThreeElements(vec, "Move", "vec"); @@ -591,6 +676,9 @@ class DEMMeshConnected : public DEMInitializer { for (auto& node : m_vertices) { node *= s; } + if (is_shell) { + shell_thickness *= s; + } double double_s = (double)std::abs(s); mass *= double_s * double_s * double_s; MOI *= double_s * double_s * double_s * double_s * double_s; @@ -616,30 +704,157 @@ class DEMMeshConnected : public DEMInitializer { Scale(make_float3(s[0], s[1], s[2])); } + //////////////////////////////////////////////////////// + // Mesh patch information for convex patch splitting + //////////////////////////////////////////////////////// + // Patch ID for each triangle facet (defaults to 0 for all triangles, assuming convex mesh) + std::vector m_patch_ids; + // Number of patches in this mesh + unsigned int nPatches = 1; + // Whether patch information has been explicitly set (either computed or manually supplied) + bool patches_explicitly_set = false; + // Relative location (to CoM) of each patch (vector of length nPatches) + std::vector m_patch_locations; + // Whether patch locations have been explicitly set + bool patch_locations_explicitly_set = false; + + /// @brief Manually set the patch IDs for each triangle. + /// @details Allows user to manually specify which patch each triangle belongs to. This is useful when + /// the user has pre-computed patch information or wants to define patches based on custom criteria. + /// @param patch_ids Vector of patch IDs, one for each triangle. Must have the same length as the number + /// of triangles in the mesh. Patch IDs should be non-negative integers starting from 0. + void SetPatchIDs(const std::vector& patch_ids); + + /// @brief Assign every triangle to its own patch. + /// @details Patch IDs are assigned in triangle order as 0, 1, ..., GetNumTriangles() - 1. This must be called after + /// loading or constructing the mesh, and the triangle count must fit in the patchID_t representation. + void SetEachTriangleAsPatch(); + + /// @brief Get the patch ID for each triangle. + /// @return Vector of patch IDs (one per triangle). By default, all triangles are in patch 0 (assuming convex mesh). + const std::vector& GetPatchIDs() const { return m_patch_ids; } + + /// @brief Get the number of patches in the mesh. + /// @return Number of patches. Default is 1 (assuming convex mesh). + unsigned int GetNumPatches() const { return nPatches; } + + /// @brief Check if patch information has been explicitly set. + /// @return True if patches have been computed via SplitIntoConvexPatches() or set via SetPatchIDs(), false if using + /// default (single patch). + bool ArePatchesExplicitlySet() const { return patches_explicitly_set; } + + /// @brief Set the relative location (to CoM) of each patch. + /// @details Allows user to manually specify the location of each patch relative to the mesh's center of mass. + /// @param patch_locations Vector of locations (float3), one for each patch. Must have the same length as the number + /// of patches in the mesh. + void SetPatchLocations(const std::vector& patch_locations) { + assertPatchLength(patch_locations.size(), "SetPatchLocations"); + m_patch_locations = patch_locations; + patch_locations_explicitly_set = true; + } + + /// @brief Get the relative location (to CoM) of each patch. + /// @return Vector of locations (one per patch). Will be automatically calculated at initialization if not + /// explicitly set. + const std::vector& GetPatchLocations() const { return m_patch_locations; } + + /// @brief Check if patch locations have been explicitly set. + /// @return True if locations have been set via SetPatchLocations(), false if they will be auto-calculated. + bool ArePatchLocationsExplicitlySet() const { return patch_locations_explicitly_set; } + + /// @brief Compute patch locations relative to the implicit CoM of the mesh. + /// @details For single patch: returns (0,0,0). For multiple patches: returns average of triangle centroids per + /// patch. + /// @return Vector of locations (one per patch). + std::vector ComputePatchLocations() const; + + enum class PatchQualityLevel : uint8_t { SAFE = 0, WARN = 1, CRITICAL = 2 }; + + enum class PatchConstraintStatus : uint8_t { SATISFIED = 0, TOO_MANY_UNMERGEABLE = 1, TOO_FEW_UNSPLITTABLE = 2 }; + + struct PatchQualityPatch { + PatchQualityLevel level = PatchQualityLevel::SAFE; + float worst_angle_deg = 0.0f; + float coherence_r = 1.0f; + unsigned int n_tris = 0; + unsigned int hard_crossings = 0; + unsigned int concave_crossings = 0; + unsigned int unoriented_edges = 0; + }; + + struct PatchQualityReport { + PatchQualityLevel overall = PatchQualityLevel::SAFE; + PatchConstraintStatus constraint_status = PatchConstraintStatus::SATISFIED; + unsigned int achieved_patches = 0; + unsigned int requested_min = 1; + unsigned int requested_max = std::numeric_limits::max(); + std::vector per_patch; + }; + + struct PatchQualityOptions { + float safe_r = 0.85f; + float warn_r = 0.65f; + float warn_worst_angle_margin_deg = 5.0f; + bool hard_crossings_are_critical = true; + bool concave_crossings_are_critical = false; + unsigned int unoriented_warn_threshold = 10; + }; + + struct PatchSplitOptions { + // `hard_angle_deg` passed to SplitIntoConvexPatches is always the maximum local neighbor angle. + // `soft_angle_deg` and `patch_normal_max_deg` add optional stricter checks while preserving the legacy default. + float soft_angle_deg = -1.0f; + float patch_normal_max_deg = -1.0f; + bool block_concave_edges = false; + float concave_allow_deg = 0.0f; + unsigned int patch_min = 1; + unsigned int patch_max = std::numeric_limits::max(); + bool seed_largest_first = true; + }; + + /// @brief Split the mesh into connected patches based on face-normal and optional quality constraints. + /// @details The default overload preserves the original angle-threshold region-growing behavior. The extended + /// overload can also fill a quality report and apply optional concavity/patch-normal checks. + unsigned int SplitIntoConvexPatches(float hard_angle_deg, + const PatchSplitOptions& opt, + PatchQualityReport* out_report, + const PatchQualityOptions& qopt); + unsigned int SplitIntoConvexPatches(float hard_angle_deg = 30.0f) { + return SplitIntoConvexPatches(hard_angle_deg, PatchSplitOptions(), nullptr, PatchQualityOptions()); + } + unsigned int SplitIntoConvexPatches(float hard_angle_deg, const PatchSplitOptions& opt) { + return SplitIntoConvexPatches(hard_angle_deg, opt, nullptr, PatchQualityOptions()); + } + unsigned int SplitIntoConvexPatches(float hard_angle_deg, + const PatchSplitOptions& opt, + PatchQualityReport* out_report) { + return SplitIntoConvexPatches(hard_angle_deg, opt, out_report, PatchQualityOptions()); + } + //////////////////////////////////////////////////////// // Some geo wildcard-related stuff //////////////////////////////////////////////////////// - // Initial geometry wildcard that all triangles should have + // Initial geometry wildcard that all triangles should have (one value per triangle facet) std::unordered_map> geo_wildcards; // Can be used to save mem after initialization void ClearWildcards() { deallocate_array(geo_wildcards); } void SetGeometryWildcards(const std::unordered_map>& wildcards) { if (wildcards.begin()->second.size() != nTri) { - std::stringstream ss; - ss << "Input gemometry wildcard arrays in a SetGeometryWildcards call must all have the same size as the " - "number of triangles in this mesh.\nHere, the input array has length " - << wildcards.begin()->second.size() << " but this mesh has " << nTri << " triangles." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input geometry wildcard arrays in a SetGeometryWildcards call must all have the same size as " + "the number of triangles in this mesh.\nHere, the input array has length %zu but this mesh has %zu " + "triangles.", + wildcards.begin()->second.size(), nTri); } geo_wildcards = wildcards; } void AddGeometryWildcard(const std::string& name, const std::vector& vals) { if (vals.size() != nTri) { - std::stringstream ss; - ss << "Input gemometry wildcard array in a AddGeometryWildcard call must have the same size as the number " - "of triangles in this mesh.\nHere, the input array has length " - << vals.size() << " but this mesh has " << nTri << " triangles." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input geometry wildcard array in a AddGeometryWildcard call must have the same size as the " + "number of triangles in this mesh.\nHere, the input array has length %zu but this mesh has %zu " + "triangles.", + vals.size(), nTri); } geo_wildcards[name] = vals; } @@ -648,30 +863,74 @@ class DEMMeshConnected : public DEMInitializer { } }; -/* -/// GPU-side struct that holds external object component info. Only component, not their parents, so this is the -/// equivalent of clump templates. External objects themselves (and their position, velocity etc.) are not stored with -/// this struct; instead, they are considered a general owner (or clump) -class DEMObjComponent { - public: - // float3* pSomething; - - // - // std::vector> something; +// Backward compatibility alias +using DEMMeshConnected = DEMMesh; - union { - ManagedAllocator plate; - ManagedAllocator plane; - }; +// Template-level rigid-group definition for combining owner templates with fixed relative poses. +class DEMCombinedTemplate { + public: + OWNER_TYPE member_type = OWNER_TYPE::CLUMP; + size_t master_member = 0; + std::vector> clump_templates; + std::vector> mesh_templates; + std::vector rel_pos; + std::vector rel_oriQ; + unsigned int load_order = 0; +}; - DEMObjComponent() { - // cudaMallocManaged(&pSomething, sizeof(float3)); +// Runtime metadata for a batch of combined template instantiations. +// Each element in the per-instance vectors corresponds to one instantiation request. +class DEMCombinedInstances { + public: + std::shared_ptr type; + // Flattened member initializer handles: n_instances blocks, each with template member count entries. + std::vector> member_objs; + std::vector member_owner_ids; + std::vector member_mass; + std::vector member_moi; + // Per-instantiation equivalent mass/MOI. + std::vector master_equiv_mass; + std::vector master_equiv_moi; + std::vector master_owner_ids; + bool owners_resolved = false; + size_t n_instances = 0; + + /// Get total number of member owners in this combined batch. + size_t GetNumOwners() const { return member_objs.size(); } + + /// Add an owner wildcard to all member owners in this combined batch with the same value. + void AddOwnerWildcard(const std::string& name, float val) { + for (auto& obj : member_objs) { + auto batch = std::dynamic_pointer_cast(obj); + if (!batch) { + DEME_ERROR( + "DEMCombinedInstances::AddOwnerWildcard encountered a member that is not a clump batch.\n" + "Owner wildcards are only supported for combined clump owners."); + } + batch->AddOwnerWildcard(name, val); + } } - ~DEMObjComponent() { - // cudaFree(pSomething); + + /// Add an owner wildcard to all member owners in this combined batch with per-owner values. + void AddOwnerWildcard(const std::string& name, const std::vector& vals) { + if (vals.size() != member_objs.size()) { + DEME_ERROR( + "Input owner wildcard array in a DEMCombinedInstances::AddOwnerWildcard call must have the same " + "size as the number of member owners.\nHere, the input array has length %zu but this combined " + "batch has %zu member owners.", + vals.size(), member_objs.size()); + } + for (size_t i = 0; i < member_objs.size(); i++) { + auto batch = std::dynamic_pointer_cast(member_objs[i]); + if (!batch) { + DEME_ERROR( + "DEMCombinedInstances::AddOwnerWildcard encountered a member that is not a clump batch.\n" + "Owner wildcards are only supported for combined clump owners."); + } + batch->AddOwnerWildcard(name, vals[i]); + } } }; -*/ } // namespace deme diff --git a/src/DEM/CMakeLists.txt b/src/DEM/CMakeLists.txt index 7cb96d09..4ca74fd8 100644 --- a/src/DEM/CMakeLists.txt +++ b/src/DEM/CMakeLists.txt @@ -13,15 +13,15 @@ target_include_directories( PUBLIC ${ProjectIncludeGenerated} ) -# DEM is pure CXX, no GPU library dependencies. -# CUB/hipcub is only needed by algorithms target. -if(USE_CHPF) - target_link_libraries( - DEM - PUBLIC ${ChPF_IMPORTED_NAME} - ) +if(DEME_ENABLE_NVTX) + target_compile_definitions(DEM PUBLIC DEME_ENABLE_NVTX) endif() +target_link_libraries( + DEM + PUBLIC CUB::CUB +) + # if(WIN32) # target_compile_options(DEM PRIVATE /GR) # endif() @@ -35,7 +35,11 @@ set(DEM_headers ${CMAKE_CURRENT_SOURCE_DIR}/Models.h ${CMAKE_CURRENT_SOURCE_DIR}/VariableTypes.h ${CMAKE_CURRENT_SOURCE_DIR}/BdrsAndObjs.h - ${CMAKE_CURRENT_SOURCE_DIR}/HostSideHelpers.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/ClumpMassProperties.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/AnalyticalOutput.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/CombinedOwnerUtils.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/HostSideHelpers.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/MeshUtils.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/Samplers.hpp ${CMAKE_CURRENT_SOURCE_DIR}/AuxClasses.h ) @@ -45,7 +49,10 @@ set(DEM_sources ${CMAKE_CURRENT_SOURCE_DIR}/dT.cpp ${CMAKE_CURRENT_SOURCE_DIR}/APIPublic.cpp ${CMAKE_CURRENT_SOURCE_DIR}/APIPrivate.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/MeshUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/ClumpMassProperties.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/AnalyticalOutput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/CombinedOwnerUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/MeshUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AuxClasses.cpp ) @@ -80,3 +87,66 @@ install( PATTERN "utils/*.hpp" ) +# Build Python module if requested +if(DEME_BUILD_PYTHON) + message(STATUS "==== Building Python Bindings ====") + + # Keep the compiled extension private so the public ``deme`` package can + # provide a stable Python namespace and compatibility aliases. + pybind11_add_module(_deme + ${DEM_headers} + ${DEM_sources} + ${CMAKE_CURRENT_SOURCE_DIR}/python/bindings.cpp + ) + if(DEME_BUILD_VISUALIZER) + target_sources(_deme PRIVATE $ $) + endif() + + set_target_properties(_deme PROPERTIES + LINKER_LANGUAGE CXX + CXX_STANDARD ${CXXSTD_SUPPORTED} + POSITION_INDEPENDENT_CODE True + ) + + target_include_directories(_deme + PUBLIC ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES} + PUBLIC ${ProjectIncludeSource} + PUBLIC ${ProjectIncludeGenerated} + ) + + target_link_libraries(_deme + PUBLIC CUDA::cudart + PUBLIC CUDA::nvrtc + PUBLIC CUDA::cuda_driver + PUBLIC core_python + PUBLIC DEMERuntimeDataHelper_python + PUBLIC algorithms + PUBLIC pybind11::headers + ) + if(DEME_BUILD_VISUALIZER) + target_link_libraries(_deme PUBLIC raylib) + target_compile_definitions(_deme PRIVATE DEME_HAS_VISUALIZER=1) + endif() + + # Install the Python module + install(TARGETS _deme + LIBRARY DESTINATION deme + COMPONENT python + ) + + # ``deme`` is the canonical package. DEME.py preserves the historical + # capitalized import without duplicating the compiled extension. + install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/python/deme/__init__.py + ${CMAKE_CURRENT_SOURCE_DIR}/python/deme/_cuda.py + DESTINATION deme + COMPONENT python + ) + install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/python/DEME.py + DESTINATION . + COMPONENT python + ) + + message(STATUS "==== Completed Building Python Bindings ====") +endif() diff --git a/src/DEM/Defines.h b/src/DEM/Defines.h index 6957ef53..97548222 100644 --- a/src/DEM/Defines.h +++ b/src/DEM/Defines.h @@ -12,7 +12,13 @@ #include #include "VariableTypes.h" -#include +#include "cuda_runtime.h" + +#ifdef __CUDACC__ + #define DEME_KERNEL extern "C" __global__ +#else + #define DEME_KERNEL +#endif #define DEME_MIN(a, b) ((a < b) ? a : b) #define DEME_MAX(a, b) ((a > b) ? a : b) @@ -29,17 +35,7 @@ namespace deme { #define DEME_TINY_FLOAT 1e-12 ///< Appears to be very sensitive to even smaller values... #define DEME_HUGE_FLOAT 1e15 #define DEME_BITS_PER_BYTE 8 -// Wavefront/warp size: 64 only on AMD wave64 device code (gfx8/gfx9 GCN/CDNA); 32 everywhere -// else (AMD RDNA, NVIDIA, and host code). ROCm 7.2.x does not provide __AMDGCN_WAVEFRONT_SIZE__, -// so device code keys off the __GFX*__ macros. For JIT kernels the actual device warp size is -// queried at runtime and substituted. -#if defined(__HIP_DEVICE_COMPILE__) && (defined(__GFX8__) || defined(__GFX9__)) - #define DEME_WARP_SIZE 64 -#else - #define DEME_WARP_SIZE 32 -#endif -// Legacy macro for compatibility -#define DEME_CUDA_WARP_SIZE DEME_WARP_SIZE +#define DEME_CUDA_WARP_SIZE 32 #define DEME_MAX_WILDCARD_NUM 16 // In bin--triangle intersection scan, all bins are enlarged by a factor of this following constant, so that no triangle // lies in between bins and not picked up by any bins. @@ -62,7 +58,6 @@ constexpr int64_t MAX_SUBVOXEL = (int64_t)1 << VOXEL_RES_POWER2; #define DEME_NUM_TRIANGLE_PER_BLOCK 512 #define DEME_MAX_THREADS_PER_BLOCK 1024 -#define DEME_INIT_CNT_MULTIPLIER 1 // If there are more than this number of analytical geometry, we may have difficulty jitify them all #define DEME_THRESHOLD_TOO_MANY_ANAL_GEO 64 // If a clump has more than this number of sphere components, it is automatically considered a non-jitifiable big clump @@ -70,11 +65,91 @@ constexpr int64_t MAX_SUBVOXEL = (int64_t)1 << VOXEL_RES_POWER2; // If there are more than this number of sphere components across all clumps (excluding the clumps that are considered // big clumps), then some of them may have to stay in global memory, rather than being jitified #define DEME_THRESHOLD_TOO_MANY_SPHERE_COMP 512 +// Mesh mass/MOI jitification should stay template-like. If a run needs more distinct mesh template mass/MOI entries +// than this, the solver falls back to flattened mass/MOI instead of emitting a very large constant-memory table. +#define DEME_THRESHOLD_TOO_MANY_MESH_TEMPLATES 512 // It should generally just be the warp size. When a block is launched, at least min(these_numbers) threads will be // launched so the template loading is always safe. constexpr clumpComponentOffset_t NUM_ACTIVE_TEMPLATE_LOADING_THREADS = DEME_MIN(DEME_MIN(DEME_CUDA_WARP_SIZE, DEME_KT_CD_NTHREADS_PER_BLOCK), DEME_NUM_BODIES_PER_BLOCK); +// Codes for owner types. We just have a handful of types... +const ownerType_t NOT_A_OWNER = 0; +const ownerType_t OWNER_T_CLUMP = 1; +const ownerType_t OWNER_T_MESH = 2; +const ownerType_t OWNER_T_ANALYTICAL = 4; ///< Must be 4, not 3, as used in bitwise operations + +const geoType_t NOT_A_GEO = 0; +const geoType_t GEO_T_SPHERE = 1; +const geoType_t GEO_T_TRIANGLE = 2; +const geoType_t GEO_T_ANALYTICAL = 4; ///< Analytical components + +// Templated encode function that packs two values into one. The order matters. +// For contact types: uses 4 bits per value (8 bits total, stored in contact_t/uint8_t) +// For patch IDs: uses 32 bits per value (64 bits total, stored in patchIDPair_t/uint64_t) +template +inline __device__ __host__ constexpr ReturnType encodeType(InputType typeA, InputType typeB) { + constexpr size_t bits_per_value = sizeof(ReturnType) * DEME_BITS_PER_BYTE / 2; + return static_cast( + (static_cast(typeA) << bits_per_value) | + (static_cast(typeB) & ((static_cast(1) << bits_per_value) - 1))); +} + +// Templated decode function for typeA (in high bits) +template +inline __device__ __host__ constexpr ReturnType decodeTypeA(InputType id) { + constexpr size_t bits_per_value = sizeof(InputType) * 8 / 2; + return static_cast(id >> bits_per_value); +} + +// Templated decode function for typeB (in low bits) +template +inline __device__ __host__ constexpr ReturnType decodeTypeB(InputType id) { + constexpr size_t bits_per_value = sizeof(InputType) * 8 / 2; + return static_cast(id & ((static_cast(1) << bits_per_value) - 1)); +} + +// If you modify this, you are responsible for updating NUM_SUPPORTED_CONTACT_TYPES accordingly, and mind the order of +// the two types, as encodeType(GEO_T_SPHERE, GEO_T_TRIANGLE) != encodeType(GEO_T_TRIANGLE, GEO_T_SPHERE). +constexpr contact_t NOT_A_CONTACT = 0; +constexpr contact_t SPHERE_SPHERE_CONTACT = encodeType(GEO_T_SPHERE, GEO_T_SPHERE); +constexpr contact_t SPHERE_TRIANGLE_CONTACT = encodeType(GEO_T_SPHERE, GEO_T_TRIANGLE); +// Preserve the legacy public name. A DEM mesh is represented by triangles, so this is intentionally the exact same +// contact identifier rather than an additional supported contact type. +constexpr contact_t SPHERE_MESH_CONTACT = SPHERE_TRIANGLE_CONTACT; +constexpr contact_t SPHERE_ANALYTICAL_CONTACT = encodeType(GEO_T_SPHERE, GEO_T_ANALYTICAL); +constexpr contact_t TRIANGLE_TRIANGLE_CONTACT = encodeType(GEO_T_TRIANGLE, GEO_T_TRIANGLE); +constexpr contact_t TRIANGLE_ANALYTICAL_CONTACT = encodeType(GEO_T_TRIANGLE, GEO_T_ANALYTICAL); +constexpr contact_t NUM_SUPPORTED_CONTACT_TYPES = 5; +inline __host__ __device__ constexpr bool isSupportedContactType(contact_t type) { + return type == SPHERE_SPHERE_CONTACT || type == SPHERE_TRIANGLE_CONTACT || type == SPHERE_ANALYTICAL_CONTACT || + type == TRIANGLE_TRIANGLE_CONTACT || type == TRIANGLE_ANALYTICAL_CONTACT; +} +// Stable flooded-island history currently applies only to mesh-related contacts. SS and SA patch IDs are already +// determined unambiguously by their primitive IDs, so rewriting their island labels adds work without improving +// identity. If sphere/clump-side patching is introduced later, extend this predicate to opt those types back in. +inline __host__ __device__ constexpr bool usesStablePatchIslandHistory(contact_t type) { + return type == SPHERE_TRIANGLE_CONTACT || type == TRIANGLE_TRIANGLE_CONTACT || type == TRIANGLE_ANALYTICAL_CONTACT; +} +constexpr contact_t ALL_CONTACT_TYPES[NUM_SUPPORTED_CONTACT_TYPES] = { + SPHERE_SPHERE_CONTACT, SPHERE_TRIANGLE_CONTACT, SPHERE_ANALYTICAL_CONTACT, TRIANGLE_TRIANGLE_CONTACT, + TRIANGLE_ANALYTICAL_CONTACT}; + +// Device version of getting geo owner ID +#define DEME_GET_GEO_OWNER_ID(geo, type) \ + ((type) == deme::GEO_T_SPHERE ? granData->ownerClumpBody[(geo)] \ + : (type) == deme::GEO_T_TRIANGLE ? granData->ownerTriMesh[(geo)] \ + : (type) == deme::GEO_T_ANALYTICAL ? granData->ownerAnalBody[(geo)] \ + : deme::NULL_BODYID) + +// Device version of getting patch owner ID +#define DEME_GET_PATCH_OWNER_ID(patchID, type) \ + ((type) == deme::GEO_T_SPHERE ? granData->ownerClumpBody[(patchID)] \ + : (type) == deme::GEO_T_TRIANGLE ? granData->ownerPatchMesh[(patchID)] \ + : (type) == deme::GEO_T_ANALYTICAL ? granData->ownerAnalBody[(patchID)] \ + : deme::NULL_BODYID) + +// Can be seen as even finer grain type identifiers of the analytical component type const objType_t ANAL_OBJ_TYPE_PLANE = 0; const objType_t ANAL_OBJ_TYPE_PLATE = 1; const objType_t ANAL_OBJ_TYPE_CYL_INF = 2; @@ -83,24 +158,9 @@ const objType_t ANAL_OBJ_TYPE_CONE = 4; const objNormal_t ENTITY_NORMAL_INWARD = 0; const objNormal_t ENTITY_NORMAL_OUTWARD = 1; -const contact_t NOT_A_CONTACT = 0; -const contact_t SPHERE_SPHERE_CONTACT = 1; -const contact_t SPHERE_MESH_CONTACT = 2; -// Aux contact types (contact with analytical objects) must be larger than SPHERE_ANALYTICAL_CONTACT! -const contact_t SPHERE_ANALYTICAL_CONTACT = 10; -const contact_t SPHERE_PLANE_CONTACT = 11; -const contact_t SPHERE_PLATE_CONTACT = 12; -const contact_t SPHERE_CYL_CONTACT = 13; -const contact_t SPHERE_CONE_CONTACT = 14; - const notStupidBool_t DONT_PREVENT_CONTACT = 0; const notStupidBool_t PREVENT_CONTACT = 1; -// Codes for owner types. We just have a handful of types... -const ownerType_t OWNER_T_CLUMP = 1; -const ownerType_t OWNER_T_ANALYTICAL = 2; -const ownerType_t OWNER_T_MESH = 4; - // Contact persistency marker consts... const notStupidBool_t CONTACT_NOT_PERSISTENT = 0; const notStupidBool_t CONTACT_IS_PERSISTENT = 1; @@ -129,31 +189,20 @@ constexpr inertiaOffset_t RESERVED_INERTIA_OFFSET = ((size_t)1 << (sizeof(inerti // kernel, instead have to be brought from the global memory constexpr clumpComponentOffset_t RESERVED_CLUMP_COMPONENT_OFFSET = ((size_t)1 << (sizeof(clumpComponentOffset_t) * DEME_BITS_PER_BYTE)) - 1; +constexpr size_t NULL_MESH_TEMPLATE_MARK = ((size_t)1 << (sizeof(size_t) * DEME_BITS_PER_BYTE - 1)) + + (((size_t)1 << (sizeof(size_t) * DEME_BITS_PER_BYTE - 1)) - 1); // Used to be compared against, so we know if some of the sphere components need to stay in global memory -constexpr unsigned int THRESHOLD_CANT_JITIFY_ALL_COMP = - DEME_MIN(DEME_MIN(RESERVED_CLUMP_COMPONENT_OFFSET, DEME_THRESHOLD_BIG_CLUMP), DEME_THRESHOLD_TOO_MANY_SPHERE_COMP); +constexpr unsigned int THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES = + DEME_MIN(RESERVED_INERTIA_OFFSET, DEME_THRESHOLD_TOO_MANY_MESH_TEMPLATES); +constexpr unsigned int THRESHOLD_CANT_JITIFY_ALL_COMP = DEME_MIN( + DEME_MIN(DEME_MIN(RESERVED_CLUMP_COMPONENT_OFFSET, DEME_THRESHOLD_BIG_CLUMP), DEME_THRESHOLD_TOO_MANY_SPHERE_COMP), + THRESHOLD_CANT_JITIFY_ALL_MESH_TEMPLATES); // Max size change the bin auto-adjust algorithm can apply to the bin size per step constexpr float BIN_SIZE_MAX_CHANGE_RATE = 0.2; - -// Device version of getting geo owner ID -#define DEME_GET_GEO_OWNER_ID(geoB, type) \ - ((type) == NOT_A_CONTACT ? NULL_BODYID \ - : (type) == SPHERE_SPHERE_CONTACT ? granData->ownerClumpBody[(geoB)] \ - : (type) == SPHERE_MESH_CONTACT ? granData->ownerMesh[(geoB)] \ - : granData->ownerAnalBody[(geoB)]) +// Safety factor for hertz_const adaptive time step estimates. +constexpr double N_DT = 16.0; // Some enums... -// Verbosity -enum VERBOSITY { - QUIET = 0, - ERR = 10, - WARNING = 20, - INFO = 30, - STEP_ANOMALY = 32, - STEP_METRIC = 35, - DEBUG = 40, - STEP_DEBUG = 50 -}; // Stepping method enum class TIME_INTEGRATOR { FORWARD_EULER, CENTERED_DIFFERENCE, EXTENDED_TAYLOR, CHUNG }; // Owner types @@ -162,6 +211,22 @@ enum class OWNER_TYPE { CLUMP, ANALYTICAL, MESH }; enum class FORCE_MODEL { HERTZIAN, HERTZIAN_FRICTIONLESS, CUSTOM }; // The info that should be present in the output files enum OUTPUT_CONTENT { + XYZ = 0, + QUAT = 1, + ABSV = 2, + VEL = 4, + ANG_VEL = 8, + ABS_ACC = 16, + ACC = 32, + ANG_ACC = 64, + FAMILY = 128, + MAT = 256, + OWNER_WILDCARD = 512, + GEO_WILDCARD = 1024 +}; +// Per-triangle fields that can accompany mesh geometry in VTK output. XYZ is always written and intentionally has no +// bit set, preserving the historical geometry-only default. +enum class MESH_OUTPUT_CONTENT : unsigned int { XYZ = 0, QUAT = 1, ABSV = 2, @@ -174,10 +239,20 @@ enum OUTPUT_CONTENT { MAT = 256, OWNER_WILDCARD = 512, GEO_WILDCARD = 1024, - // How much this clump expanded in size via ChangeClumpSizes, compared to its `vanilla' template. Can be useful if - // the user imposed some fine-grain clump size control. - EXP_FACTOR = 2048 + OWNER = 2048, + MESH_ID = 4096, + TRI_ID = 8192, + PATCH_ID = 16384 }; +constexpr unsigned int operator|(MESH_OUTPUT_CONTENT lhs, MESH_OUTPUT_CONTENT rhs) { + return static_cast(lhs) | static_cast(rhs); +} +constexpr unsigned int operator|(unsigned int lhs, MESH_OUTPUT_CONTENT rhs) { + return lhs | static_cast(rhs); +} +constexpr unsigned int operator|(MESH_OUTPUT_CONTENT lhs, unsigned int rhs) { + return static_cast(lhs) | rhs; +} // Output particles as individual (component) spheres, or as owner clumps (clump CoMs for location, as an example)? enum class SPATIAL_DIR { X, Y, Z, NONE }; // The info that should be present in the contact pair output files @@ -202,7 +277,26 @@ enum CNT_OUTPUT_CONTENT { // NOTE: All data structs here need to be simple enough to jitify. In general, if you need to include something much // more complex than DEMDefines for example, then do it in Structs.h. -// A structure for storing simulation parameters. +// A structure for storing frequently updated simulation parameters. +struct DEMSimParamsDynamic { + // The edge length and inverse edge length of a bin (for contact detection) + double binSize; + double inv_binSize; + // Time step size + float h; + // Time elappsed since start of simulation + double timeElapsed = 0; + // Sphere radii/geometry thickness inflation amount (for safer contact detection) + float beta; + // Max velocity, user approximated, we verify during simulation + float approxMaxVel; + // Expand safety parameter (multiplier for the max vel) + float expSafetyMulti; + // Expand safety parameter (adder for the max vel) + float expSafetyAdder; +}; + +// A structure for storing mostly static simulation parameters. struct DEMSimParams { // Number of voxels in the X direction, expressed as a power of 2 unsigned char nvXp2; @@ -220,11 +314,12 @@ struct DEMSimParams { double l; // Double-precision single voxel size double voxelSize; - // The edge length of a bin (for contact detection) - double binSize; + // Frequently updated parameters (kept inline for device access) + DEMSimParamsDynamic dyn; // Number of clumps, spheres, triangles, mesh-represented objects, analytical components, external objs... bodyID_t nSpheresGM; bodyID_t nTriGM; + bodyID_t nMeshPatches; objID_t nAnalGM; bodyID_t nOwnerBodies; bodyID_t nOwnerClumps; @@ -248,32 +343,44 @@ struct DEMSimParams { // User's box size float3 userBoxMin; float3 userBoxMax; - // Time step size - float h; - // Time elappsed since start of simulation - double timeElapsed = 0; - // Sphere radii/geometry thickness inflation amount (for safer contact detection) - float beta; - // Max velocity, user approximated, we verify during simulation - float approxMaxVel; - // Expand safety parameter (multiplier for the max vel) - float expSafetyMulti; - // Expand safety parameter (adder for the max vel) - float expSafetyAdder; // Stepping method TIME_INTEGRATOR stepping = TIME_INTEGRATOR::FORWARD_EULER; + // Whether needs to store the contact normal + bool storeNormal = false; // Number of wildcards (extra property) arrays associated with contacts and owners and geometries unsigned int nContactWildcards; unsigned int nOwnerWildcards; unsigned int nGeoWildcards; + // Max tri-tri penetration margin (to prevent super large margins from being added) + double capTriTriPenetration = DEME_HUGE_FLOAT; + + // Ratio threshold for rejecting suspicious tri-tri contacts: a contact is rejected when the penetration depth + // exceeds this fraction of the distance from the contact point to a mesh's geometric center. + float triTriContactRejectionRatio = 0.8f; + // The max vel at which the solver errors out float errOutVel = DEME_HUGE_FLOAT; + // The max ang vel at which the solver errors out + float errOutAngVel = DEME_HUGE_FLOAT; // The max num of spheres per bin before solver errors out unsigned int errOutBinSphNum = 32768; // The max num of triangles per bin before solver errors out unsigned int errOutBinTriNum = 32768; + // Whether angular velocity contributes to contact margin sizing (and sphere--sphere rot. velocity use). + notStupidBool_t useAngVelMargin = 1; + + // When true, the per-triangle maxTriTriPenetration array is neither computed (atomic max skipped in force kernel), + // nor transferred to kT, nor used to guard finalMargin in computeMarginFromAbsv. This saves compute time when the + // user knows that meshed particles have a low polygon count (e.g. box with 12 triangles, tetrahedron with 4) and + // mesh-mesh contacts are always SAT-traceable, i.e. no triangle can be completely submerged inside another mesh. + bool meshParticlesLowPoly = false; + + // Number of owners currently participating in combined-owner rigid groups. + bodyID_t nCombinedOwners = 0; + // If 1, contact detection keeps contacts among owners that share the same combined master. + notStupidBool_t allowIntraCombinedOwnerContacts = 0; }; // A struct that holds pointers to data arrays that dT uses @@ -315,18 +422,34 @@ struct DEMDataDT { notStupidBool_t* accSpecified; notStupidBool_t* angAccSpecified; - bodyID_t* idGeometryA; - bodyID_t* idGeometryB; - contact_t* contactType; + bodyID_t* idPrimitiveA; + bodyID_t* idPrimitiveB; + contact_t* contactTypePrimitive; + contactPairs_t* geomToPatchMap; + + // NEW: Separate patch IDs and mapping array + bodyID_t* idPatchA; + bodyID_t* idPatchB; + contact_t* contactTypePatch; + bodyID_t* contactPatchIsland; contactPairs_t* contactMapping; // Family mask notStupidBool_t* familyMasks; // Extra margin size float* familyExtraMarginSize; + // Combined-owner mapping (owner -> master owner, NULL_BODYID when not in a combined group) + bodyID_t* ownerCombinedMaster = nullptr; + // Member-fixed transforms in master frame (indexed by owner; ignored for non-members and masters) + float3* ownerCombinedRelPos = nullptr; + float4* ownerCombinedRelOriQ = nullptr; + // Per-master equivalent mass/MOI for combined-group integration. + float* ownerCombinedMasterMass = nullptr; + float3* ownerCombinedMasterMOI = nullptr; // Some dT's own work array pointers float3* contactForces; + float3* contactNormals; float3* contactTorque_convToForce; float3* contactPointGeometryA; float3* contactPointGeometryB; @@ -338,16 +461,31 @@ struct DEMDataDT { clumpComponentOffset_t* clumpComponentOffset; clumpComponentOffsetExt_t* clumpComponentOffsetExt; materialsOffset_t* sphereMaterialOffset; - bodyID_t* ownerMesh; + bodyID_t* ownerTriMesh; + bodyID_t* ownerPatchMesh; bodyID_t* ownerAnalBody; + // Mesh-owner flags used by primitive mesh contact handling. + notStupidBool_t* ownerMeshConvex = nullptr; + notStupidBool_t* ownerMeshNeverWinner = nullptr; + notStupidBool_t* ownerMeshWatertight = nullptr; + float* ownerMeshShellHalfThickness = nullptr; + bodyID_t* triPatchID; + // Map global triangle ID -> compact neighbor index (NULL_BODYID if neighbors are not stored). + bodyID_t* triNeighborIndex = nullptr; + bodyID_t* triNeighbor1 = nullptr; + bodyID_t* triNeighbor2 = nullptr; + bodyID_t* triNeighbor3 = nullptr; float3* relPosNode1; float3* relPosNode2; float3* relPosNode3; - materialsOffset_t* triMaterialOffset; + float3* relPosPatch; + materialsOffset_t* patchMaterialOffset; + float* maxTriTriPenetration = nullptr; // pointer to remote buffer where kinematic thread stores work-order data provided by the dynamic thread unsigned int* pKTOwnedBuffer_maxDrift = nullptr; float* pKTOwnedBuffer_absVel = nullptr; + float* pKTOwnedBuffer_absAngVel = nullptr; float* pKTOwnedBuffer_ts = nullptr; voxelID_t* pKTOwnedBuffer_voxelID = nullptr; subVoxelPos_t* pKTOwnedBuffer_locX = nullptr; @@ -361,6 +499,7 @@ struct DEMDataDT { float3* pKTOwnedBuffer_relPosNode1 = nullptr; float3* pKTOwnedBuffer_relPosNode2 = nullptr; float3* pKTOwnedBuffer_relPosNode3 = nullptr; + float* pKTOwnedBuffer_maxTriTriPenetration = nullptr; // The collection of pointers to DEM template arrays such as radiiSphere, still useful when there are template info // not directly jitified into the kernels @@ -396,41 +535,74 @@ struct DEMDataKT { oriQ_t* oriQx; oriQ_t* oriQy; oriQ_t* oriQz; - // Derived from absv which is for determining contact margin size. - float* marginSize; + // Derived from absv which is for determining contact margin size. Each type of primitive geometry has its own size. + float* marginSizeSphere; + float* marginSizeAnalytical; + float* marginSizeTriangle; // Family mask notStupidBool_t* familyMasks; // Extra margin size float* familyExtraMarginSize; + // Combined-owner mapping (owner -> master owner, NULL_BODYID when not in a combined group) + bodyID_t* ownerCombinedMaster = nullptr; // The offset info that indexes into the template arrays bodyID_t* ownerClumpBody; clumpComponentOffset_t* clumpComponentOffset; clumpComponentOffsetExt_t* clumpComponentOffsetExt; - bodyID_t* ownerMesh; + bodyID_t* ownerTriMesh; bodyID_t* ownerAnalBody; + notStupidBool_t* ownerMeshConvex = nullptr; + notStupidBool_t* ownerMeshNeverWinner = nullptr; + float* ownerMeshShellHalfThickness = nullptr; + bodyID_t* triPatchID; + // Map global triangle ID -> compact neighbor index (NULL_BODYID if neighbors are not stored). + bodyID_t* triNeighborIndex = nullptr; + bodyID_t* triNeighbor1 = nullptr; + bodyID_t* triNeighbor2 = nullptr; + bodyID_t* triNeighbor3 = nullptr; float3* relPosNode1; float3* relPosNode2; float3* relPosNode3; + float* maxTriTriPenetration = nullptr; // kT produces contact info, and stores it, temporarily - bodyID_t* idGeometryA; - bodyID_t* idGeometryB; - contact_t* contactType; + bodyID_t* idPrimitiveA; + bodyID_t* idPrimitiveB; + contact_t* contactTypePrimitive; notStupidBool_t* contactPersistency; - bodyID_t* previous_idGeometryA; - bodyID_t* previous_idGeometryB; - contact_t* previous_contactType; + bodyID_t* previous_idPrimitiveA; + bodyID_t* previous_idPrimitiveB; + contact_t* previous_contactTypePrimitive; contactPairs_t* contactMapping; + // NEW: Separate patch IDs and mapping array (kT work arrays) + bodyID_t* idPatchA; + bodyID_t* idPatchB; + bodyID_t* previous_idPatchA; + bodyID_t* previous_idPatchB; + contact_t* contactTypePatch; + contact_t* previous_contactTypePatch; + bodyID_t* contactPatchIsland; + bodyID_t* previous_contactPatchIsland; + contactPairs_t* geomToPatchMap; + // data pointers that is kT's transfer destination - size_t* pDTOwnedBuffer_nContactPairs = nullptr; - bodyID_t* pDTOwnedBuffer_idGeometryA = nullptr; - bodyID_t* pDTOwnedBuffer_idGeometryB = nullptr; + size_t* pDTOwnedBuffer_nPrimitiveContacts = nullptr; + size_t* pDTOwnedBuffer_nPatchContacts = nullptr; + bodyID_t* pDTOwnedBuffer_idPrimitiveA = nullptr; + bodyID_t* pDTOwnedBuffer_idPrimitiveB = nullptr; contact_t* pDTOwnedBuffer_contactType = nullptr; contactPairs_t* pDTOwnedBuffer_contactMapping = nullptr; + // NEW: Buffer pointers for separate patch arrays + bodyID_t* pDTOwnedBuffer_idPatchA = nullptr; + bodyID_t* pDTOwnedBuffer_idPatchB = nullptr; + contact_t* pDTOwnedBuffer_contactTypePatch = nullptr; + bodyID_t* pDTOwnedBuffer_contactPatchIsland = nullptr; + contactPairs_t* pDTOwnedBuffer_geomToPatchMap = nullptr; + // The collection of pointers to DEM template arrays such as radiiSphere, still useful when there are template info // not directly jitified into the kernels float* radiiSphere; @@ -459,6 +631,13 @@ const unsigned int NUM_STEPS_RESERVED_AFTER_RENEWING_FREQ_TUNER = 10; const float DEFAULT_BOX_DOMAIN_SIZE = 20.; // The enlargement ratio we apply to the target sim world size when we construct it. const float DEFAULT_BOX_DOMAIN_ENLARGE_RATIO = 0.2; +// Initial contact array size; does not matter that much as they can be resized anytime in simulation +const contactPairs_t INITIAL_CONTACT_ARRAY_SIZE = 1024; +// Inclusive bin range on one axis. An empty range is represented by imax < imin. +struct AxisBounds { + int imin; + int imax; +}; // #ifndef CUB_IGNORE_DEPRECATED_API // #define CUB_IGNORE_DEPRECATED_API diff --git a/src/DEM/MeshUtils.cpp b/src/DEM/MeshUtils.cpp deleted file mode 100644 index f0efa3cd..00000000 --- a/src/DEM/MeshUtils.cpp +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// -// SPDX-License-Identifier: BSD-3-Clause - -// This file contains modifications of the code by Alessandro Tasora and Radu Serban -// Below is the BSD license - -// Copyright (c) 2016, Project Chrono Development Team -// All Rghts Reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: - -// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// - Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. - -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#include -#include -#include -#include -#include - -#include "../kernel/DEMHelperKernels.cuh" -#include "BdrsAndObjs.h" -#include "../core/utils/WavefrontMeshLoader.hpp" - -namespace deme { - -using namespace WAVEFRONT; - -std::vector> DEMMeshConnected::GetCoordsVerticesAsVectorOfVectors() { - auto vec = GetCoordsVertices(); - std::vector> res(vec.size()); - for (size_t i = 0; i < vec.size(); i++) { - res[i] = {vec[i].x, vec[i].y, vec[i].z}; - } - return res; -} - -std::vector> DEMMeshConnected::GetIndicesVertexesAsVectorOfVectors() { - auto vec = GetIndicesVertexes(); - std::vector> res(vec.size()); - for (size_t i = 0; i < vec.size(); i++) { - res[i] = {vec[i].x, vec[i].y, vec[i].z}; - } - return res; -} - -bool DEMMeshConnected::LoadWavefrontMesh(std::string input_file, bool load_normals, bool load_uv) { - this->m_vertices.clear(); - this->m_normals.clear(); - this->m_UV.clear(); - this->m_face_v_indices.clear(); - this->m_face_n_indices.clear(); - this->m_face_uv_indices.clear(); - - GeometryInterface emptybm; // BuildMesh bm; - - filename = input_file; - - OBJ obj; - - int ret = obj.LoadMesh(filename.c_str(), &emptybm, true); - if (ret == -1) { - std::cerr << "Error loading OBJ file " << filename << std::endl; - return false; - } - - float3 tmp_f3; - int3 tmp_i3; - for (unsigned int iv = 0; iv < obj.mVerts.size(); iv += 3) { - tmp_f3.x = obj.mVerts[iv]; - tmp_f3.y = obj.mVerts[iv + 1]; - tmp_f3.z = obj.mVerts[iv + 2]; - this->m_vertices.push_back(tmp_f3); - } - for (unsigned int in = 0; in < obj.mNormals.size(); in += 3) { - tmp_f3.x = obj.mNormals[in]; - tmp_f3.y = obj.mNormals[in + 1]; - tmp_f3.z = obj.mNormals[in + 2]; - this->m_normals.push_back(tmp_f3); - } - for (unsigned int it = 0; it < obj.mTexels.size(); it += 2) // +2 because only u,v each texel - { - tmp_f3.x = obj.mTexels[it]; - tmp_f3.y = obj.mTexels[it + 1]; - tmp_f3.z = 0; - this->m_UV.push_back(tmp_f3); - } - for (unsigned int iiv = 0; iiv < obj.mIndexesVerts.size(); iiv += 3) { - tmp_i3.x = obj.mIndexesVerts[iiv]; - tmp_i3.y = obj.mIndexesVerts[iiv + 1]; - tmp_i3.z = obj.mIndexesVerts[iiv + 2]; - this->m_face_v_indices.push_back(tmp_i3); - } - for (unsigned int iin = 0; iin < obj.mIndexesNormals.size(); iin += 3) { - tmp_i3.x = obj.mIndexesNormals[iin]; - tmp_i3.y = obj.mIndexesNormals[iin + 1]; - tmp_i3.z = obj.mIndexesNormals[iin + 2]; - this->m_face_n_indices.push_back(tmp_i3); - } - for (unsigned int iit = 0; iit < obj.mIndexesTexels.size(); iit += 3) { - tmp_i3.x = obj.mIndexesTexels[iit]; - tmp_i3.y = obj.mIndexesTexels[iit + 1]; - tmp_i3.z = obj.mIndexesTexels[iit + 2]; - this->m_face_uv_indices.push_back(tmp_i3); - } - - if (!load_normals) { - this->m_normals.clear(); - this->m_face_n_indices.clear(); - } - if (!load_uv) { - this->m_UV.clear(); - this->m_face_uv_indices.clear(); - } - - this->nTri = m_face_v_indices.size(); - - return true; -} - -// Write the specified meshes in a Wavefront .obj file -void DEMMeshConnected::WriteWavefront(const std::string& filename, std::vector& meshes) { - std::ofstream mf(filename); - - //// TODO: include normal information if available - - // Create a single object mesh - std::vector v_offsets; - int v_off = 1; - for (auto& m : meshes) { - for (auto& v : m.m_vertices) { - mf << "v " << v.x << " " << v.y << " " << v.z << std::endl; - } - v_offsets.push_back(v_off); - v_off += static_cast(m.m_vertices.size()); - } - - std::vector has_normals; - std::vector vn_offsets; - int vn_off = 1; - for (auto& m : meshes) { - has_normals.push_back(m.m_normals.size() > 0); - for (auto& v : m.m_normals) { - mf << "vn " << v.x << " " << v.y << " " << v.z << std::endl; - } - vn_offsets.push_back(vn_off); - vn_off += static_cast(m.m_normals.size()); - } - - for (size_t i = 0; i < meshes.size(); i++) { - v_off = v_offsets[i]; - if (has_normals[i]) { - auto& idxV = meshes[i].m_face_v_indices; - auto& idxN = meshes[i].m_face_n_indices; - assert(idxV.size() == idxN.size()); - vn_off = vn_offsets[i]; - for (int j = 0; j < idxV.size(); j++) { - mf << "f " << idxV[j].x + v_off << "//" << idxN[j].x + vn_off << " " << idxV[j].y + v_off << "//" - << idxN[j].y + vn_off << " " << idxV[j].z + v_off << "//" << idxN[j].z + vn_off << std::endl; - } - } else { - for (auto& f : meshes[i].m_face_v_indices) { - mf << "f " << f.x + v_off << " " << f.y + v_off << " " << f.z + v_off << std::endl; - } - } - } - - mf.close(); -} - -} // end namespace deme diff --git a/src/DEM/Models.h b/src/DEM/Models.h index 42f7fd5c..af266409 100644 --- a/src/DEM/Models.h +++ b/src/DEM/Models.h @@ -13,7 +13,7 @@ #include "Defines.h" #include "Structs.h" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" #include "../core/utils/RuntimeData.h" #include "../core/utils/DEMEPaths.h" @@ -256,10 +256,10 @@ inline void equip_force_model_ingr_acq(std::string& definition, std::string& acquisition_B, std::unordered_map& added_ingredients) { if (added_ingredients["ts"]) { - definition += "float ts = simParams->h;\n"; + definition += "float ts = simParams->dyn.h;\n"; } if (added_ingredients["time"]) { - definition += "float time = simParams->timeElapsed;\n"; + definition += "float time = simParams->dyn.timeElapsed;\n"; } if (added_ingredients["AOwnerFamily"]) { definition += "deme::family_t AOwnerFamily;\n"; @@ -298,9 +298,8 @@ inline void equip_force_model_ingr_acq(std::string& definition, } if (added_ingredients["AGeo"] || added_ingredients["BGeo"]) { definition += "deme::bodyID_t AGeo, BGeo;\n"; - acquisition_A += "AGeo = sphereID;"; - // BGeo can be sphere, tri or analytical, but they are all named sphereID in the force kernel. - acquisition_B += "BGeo = sphereID;"; + acquisition_A += "AGeo = myPatchID;"; + acquisition_B += "BGeo = myPatchID;"; } if (added_ingredients["AOwnerMOI"] || added_ingredients["BOwnerMOI"]) { definition += "float3 AOwnerMOI, BOwnerMOI;\n"; @@ -341,24 +340,6 @@ inline void equip_owner_wildcards(std::string& definition, } } -// Sweep through all ingredients... -inline void equip_geo_wildcards(std::string& definition, - std::string& acquisition_A, - std::string& acquisition_B_sph, - std::string& acquisition_B_tri, - std::string& acquisition_B_anal, - const std::set& added_ingredients) { - unsigned int i = 0; - for (const auto& name : added_ingredients) { - definition += "float* " + name + "_A, *" + name + "_B;\n"; - acquisition_A += name + "_A = granData->sphereWildcards[" + std::to_string(i) + "];\n"; - acquisition_B_sph += name + "_B = granData->sphereWildcards[" + std::to_string(i) + "];\n"; - acquisition_B_tri += name + "_B = granData->triWildcards[" + std::to_string(i) + "];\n"; - acquisition_B_anal += name + "_B = granData->analWildcards[" + std::to_string(i) + "];\n"; - i++; - } -} - // Massage contact wildcards (by that I mean those contact history arrays) inline void equip_contact_wildcards(std::string& acquisition, std::string& write_back, @@ -368,9 +349,10 @@ inline void equip_contact_wildcards(std::string& acquisition, for (const auto& name : names) { // Rigth now, supports float arrays only... // Getting it from global mem - acquisition += "float " + name + " = granData->contactWildcards[" + std::to_string(i) + "][myContactID];\n"; + acquisition += + "float " + name + " = granData->contactWildcards[" + std::to_string(i) + "][myPatchContactID];\n"; // Write it back to global mem - write_back += "granData->contactWildcards[" + std::to_string(i) + "][myContactID] = " + name + ";\n"; + write_back += "granData->contactWildcards[" + std::to_string(i) + "][myPatchContactID] = " + name + ";\n"; // Destroy it (set to 0) if it is a fake contact destroy_record += name + " = 0;\n"; i++; diff --git a/src/DEM/PyDEME.cpp b/src/DEM/PyDEME.cpp deleted file mode 100644 index e084e22b..00000000 --- a/src/DEM/PyDEME.cpp +++ /dev/null @@ -1,1476 +0,0 @@ -#include -#include -#include -#include -#include "API.h" -#include "AuxClasses.h" -#include -#include "VariableTypes.h" -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace py = pybind11; - -// Custom type caster for float3 and float4 -namespace pybind11 { -namespace detail { -template <> -struct type_caster { - public: - PYBIND11_TYPE_CASTER(float3, _("float3")); - - // Python -> C++ - bool load(handle src, bool) { - if (!isinstance(src)) - return false; - auto seq = reinterpret_borrow(src); - if (seq.size() != 3) - return false; - value.x = seq[0].cast(); - value.y = seq[1].cast(); - value.z = seq[2].cast(); - return true; - } - - // C++ -> Python - static handle cast(const float3& src, return_value_policy, handle) { - return py::make_tuple(src.x, src.y, src.z).release(); - } -}; - -template <> -struct type_caster { - public: - PYBIND11_TYPE_CASTER(float4, _("float4")); - - // Python -> C++ - bool load(handle src, bool) { - if (!isinstance(src)) - return false; - auto seq = reinterpret_borrow(src); - if (seq.size() != 4) - return false; - value.x = seq[0].cast(); - value.y = seq[1].cast(); - value.z = seq[2].cast(); - value.w = seq[3].cast(); - return true; - } - - // C++ -> Python - static handle cast(const float4& src, return_value_policy, handle) { - return py::make_tuple(src.x, src.y, src.z, src.w).release(); - } -}; -} // namespace detail -} // namespace pybind11 - -// Allow pybind11 to convert std::vector using the custom type caster -// PYBIND11_DECLARE_VECTOR_SPECIALIZATION(std::vector, true); -// PYBIND11_DECLARE_VECTOR_SPECIALIZATION(std::vector, true); - -PYBIND11_MODULE(_deme, obj) { - obj.attr("__version__") = std::to_string(DEME_VERSION_MAJOR) + "." + std::to_string(DEME_VERSION_MINOR) + "." + - std::to_string(DEME_VERSION_PATCH); - // Obtaining the location of python's site-packages dynamically and setting it - // as path prefix - - std::filesystem::path path; - if (py::hasattr(obj, "__file__")) { - path = std::filesystem::path(py::str(obj.attr("__file__"))).parent_path().parent_path(); - } else { - py::module_ site = py::module_::import("site"); - path = site.attr("getsitepackages")().cast()[0].cast(); - } - - // Set runtime paths relative to the installed extension rather than the process working directory. - DEMERuntimeDataHelper::SetPathPrefix(path); - deme::SetDEMEDataPath((DEMERuntimeDataHelper::data_path / "data").string()); - - // Setting JitHelper variables - JitHelper::KERNEL_DIR = DEMERuntimeDataHelper::data_path / "kernel"; - JitHelper::KERNEL_INCLUDE_DIR = DEMERuntimeDataHelper::include_path; - - // To define methods independent of a class, use obj.def() syntax to wrap them! - obj.def("FrameTransformGlobalToLocal", &deme::FrameTransformGlobalToLocal, - "Translating the inverse of the provided vec then applying a local inverse rotation of the provided rot_Q, " - "then return the result."); - obj.def("FrameTransformLocalToGlobal", &deme::FrameTransformLocalToGlobal, - "Apply a local rotation then a translation, then return the result."); - obj.def("GetDEMEDataFile", &deme::GetDEMEDataFile); - obj.def( - "DEMBoxGridSampler", - static_cast (*)(const std::vector&, const std::vector&, float, float, float)>( - &deme::DEMBoxGridSampler), - py::arg("BoxCenter"), py::arg("HalfDims"), py::arg("GridSizeX"), py::arg("GridSizeY") = -1.0, - py::arg("GridSizeZ") = -1.0); - obj.def("DEMBoxHCPSampler", - static_cast (*)(const std::vector&, const std::vector&, float)>( - &deme::DEMBoxHCPSampler)); - obj.def("DEMCylSurfSampler", - static_cast> (*)(const std::vector&, const std::vector&, float, - float, float, float)>(&deme::DEMCylSurfSampler), - py::arg("CylCenter"), py::arg("CylAxis"), py::arg("CylRad"), py::arg("CylHeight"), py::arg("ParticleRad"), - py::arg("spacing") = 1.2); - - obj.attr("PI") = py::float_(3.141592653589793238462643383279502884197); - - py::class_(obj, "DEMERuntimeDataHelper") - .def(py::init<>()) - .def_static("SetPathPrefix", &DEMERuntimeDataHelper::SetPathPrefix); - - py::class_(obj, "PDSampler") - .def(py::init()) - .def("SetSeparation", &deme::PDSampler::SetSeparation) - .def("SetRandomEngineSeed", &deme::PDSampler::SetRandomEngineSeed) - .def("SampleBox", - static_cast> (deme::PDSampler::*)( - const std::vector& center, const std::vector& halfDim)>(&deme::PDSampler::SampleBox), - "Generates a sample box") - - .def("SampleSphere", - static_cast> (deme::PDSampler::*)(const std::vector&, float)>( - &deme::PDSampler::SampleSphere)) - - .def("SampleCylinderX", - static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( - &deme::PDSampler::SampleCylinderX)) - - .def("SampleCylinderY", - static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( - &deme::PDSampler::SampleCylinderY)) - - .def("SampleCylinderZ", - static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( - &deme::PDSampler::SampleCylinderZ)) - - .def("GetSeparation", &deme::Sampler::GetSeparation); - - py::class_(obj, "GridSampler") - .def(py::init()) - .def("SetSeparation", &deme::GridSampler::SetSeparation) - .def("SampleBox", - static_cast> (deme::GridSampler::*)( - const std::vector& center, const std::vector& halfDim)>(&deme::GridSampler::SampleBox), - "Generates a sample box") - - .def("SampleSphere", - static_cast> (deme::GridSampler::*)(const std::vector&, float)>( - &deme::GridSampler::SampleSphere)) - - .def("SampleCylinderX", static_cast> (deme::GridSampler::*)( - const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderX)) - - .def("SampleCylinderY", static_cast> (deme::GridSampler::*)( - const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderY)) - - .def("SampleCylinderZ", static_cast> (deme::GridSampler::*)( - const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderZ)) - - .def("GetSeparation", &deme::Sampler::GetSeparation); - - py::class_(obj, "HCPSampler") - .def(py::init()) - .def("SampleBox", - static_cast> (deme::HCPSampler::*)( - const std::vector& center, const std::vector& halfDim)>(&deme::HCPSampler::SampleBox), - "Generates a sample box") - - .def("SetSeparation", &deme::HCPSampler::SetSeparation) - .def("SampleSphere", - static_cast> (deme::HCPSampler::*)(const std::vector&, float)>( - &deme::HCPSampler::SampleSphere)) - - .def( - "SampleCylinderX", - static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( - &deme::HCPSampler::SampleCylinderX)) - - .def( - "SampleCylinderY", - static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( - &deme::HCPSampler::SampleCylinderY)) - - .def( - "SampleCylinderZ", - static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( - &deme::HCPSampler::SampleCylinderZ)) - - .def("GetSeparation", &deme::HCPSampler::GetSeparation); - - // Have to forward declare this templated method so pybind can see it - std::vector& (deme::DataContainer::*get_float_dc)(const std::string&) = &deme::DataContainer::Get; - py::class_>(obj, "DataContainer") - .def(py::init<>()) - .def("Get", get_float_dc, py::return_value_policy::reference_internal, - "Get a float value from the container by name. Should be used for getting wildcard by name."); - - py::class_>( - obj, "ContactInfoContainer") - .def(py::init>&>()) - .def("GetContactType", &deme::ContactInfoContainer::GetContactType, - "Get contact type as strings from the container.") - .def("GetPoint", &deme::ContactInfoContainer::GetPoint, "Get contact points as vectors from the container.") - .def("GetAOwner", &deme::ContactInfoContainer::GetAOwner, "Get AOwner number from the container.") - .def("GetBOwner", &deme::ContactInfoContainer::GetBOwner, "Get BOwner number from the container.") - .def("GetAGeo", &deme::ContactInfoContainer::GetAGeo, "Get AGeo number from the container.") - .def("GetBGeo", &deme::ContactInfoContainer::GetBGeo, "Get BGeo number from the container.") - .def("GetAOwnerFamily", &deme::ContactInfoContainer::GetAOwnerFamily, - "Get AOwnerFamily number from the container.") - .def("GetBOwnerFamily", &deme::ContactInfoContainer::GetBOwnerFamily, - "Get BOwnerFamily number from the container.") - .def("GetForce", &deme::ContactInfoContainer::GetForce, "Get force as vectors from the container.") - .def("GetTorque", &deme::ContactInfoContainer::GetTorque, "Get torque as vectors from the container.") - .def("GetNormal", &deme::ContactInfoContainer::GetNormal, "Get contact normal as vectors from the container."); - - py::class_>(obj, "DEMInspector") - .def(py::init()) - .def("GetValue", &deme::DEMInspector::GetValue); - - py::class_>(obj, "DEMInitializer").def(py::init<>()); - - py::class_>(obj, "DEMTrackedObj") - .def(py::init()); - - py::class_>(obj, "Tracker") - .def(py::init()) - .def("GetOwnerID", &deme::DEMTracker::GetOwnerID, "Get the owner ID of the tracked obj.", py::arg("offset") = 0) - .def("GetOwnerIDs", &deme::DEMTracker::GetOwnerIDs, "Get the owner IDs of all the tracked objects.") - - .def("Pos", &deme::DEMTracker::GetPos, "Get the position of this tracked object.", py::arg("offset") = 0) - .def("Positions", &deme::DEMTracker::GetPositions, "Get the positions of all tracked objects.") - - .def("AngVelLocal", &deme::DEMTracker::GetAngVelLocal, - "Get the angular velocity of this tracked object in its own local coordinate system. Applying OriQ to it " - "would give you the ang vel in global frame.", - py::arg("offset") = 0) - .def("AngularVelocitiesLocal", &deme::DEMTracker::GetAngularVelocitiesLocal, - "Get the angular velocity of all tracked objects in their own local coordinate system. Applying OriQ to " - "it would give you the ang vel in global frame.") - - .def("AngVelGlobal", &deme::DEMTracker::GetAngVelGlobal, - "Get the angular velocity of this tracked object in global coordinate system.", py::arg("offset") = 0) - .def("AngularVelocitiesGlobal", &deme::DEMTracker::GetAngularVelocitiesGlobal, - "Get the angular velocity of all objects tracked by this tracker, in global coordinate system.") - - .def("Vel", &deme::DEMTracker::GetVel, "Get the velocity of this tracked object in global frame.", - py::arg("offset") = 0) - .def("Velocities", &deme::DEMTracker::GetVelocities, - "Get the velocities of all objects tracked by this tracker, in global frame.") - - .def("OriQ", &deme::DEMTracker::GetOriQ, - "Get the quaternion that represents the orientation of this tracked object's own coordinate system. " - "Returns a vector of 4 floats. The order is (x, y, z, w). If compared against Chrono naming convention, " - "then it is saying our ordering here is (e1, e2, e3, e0).", - py::arg("offset") = 0) - .def("OrientationQuaternions", &deme::DEMTracker::GetOrientationQuaternions, - "Get all quaternions that represent the orientation of all the tracked objects' own coordinate systems. " - "Returns a vector of 4-float vectors. The order is (x, y, z, w). If compared against Chrono naming " - "convention, then it is saying our ordering here is (e1, e2, e3, e0).") - - .def("GetFamily", &deme::DEMTracker::GetFamily, "Get the family number of the tracked object.", - py::arg("offset") = 0) - .def("GetFamilies", &deme::DEMTracker::GetFamilies, "Get the family numbers of all the tracked object.") - - .def("MOI", &deme::DEMTracker::GetMOI, - "Get the moment of inertia (in principal axis frame) of the tracked object.", py::arg("offset") = 0) - .def("MOIs", &deme::DEMTracker::GetMOIs, - "Get the moment of inertia (in principal axis frame) of all the tracked objects.") - - .def("Mass", &deme::DEMTracker::Mass, "Get the mass of the tracked object.", py::arg("offset") = 0) - .def("Masses", &deme::DEMTracker::Masses, "Get the masses of all the tracked objects.") - - .def("GetContactClumps", &deme::DEMTracker::GetContactClumps, - "Get the clumps that are in contact with this tracked owner as a vector.", py::arg("offset") = 0) - - .def("ContactAcc", &deme::DEMTracker::GetContactAcc, - "Get the a portion of the acceleration of this tracked object, that is the result of its contact with " - "other simulation entities. In most cases, this means excluding the gravitational acceleration. The " - "acceleration is in global frame.", - py::arg("offset") = 0) - .def("ContactAccelerations", &deme::DEMTracker::GetContactAccelerations, - "Get the acceleration experienced by all objects tracked by this tracker, that is the result of their " - "contact with other simulation entities. The acceleration is in global frame. In most cases, this means " - "excluding the gravitational acceleration. The acceleration is in global frame.") - - .def("ContactAngAccLocal", &deme::DEMTracker::GetContactAngAccLocal, - "Get the a portion of the angular acceleration of this tracked object, that is the result of its contact " - "with other simulation entities. The acceleration is in this object's local frame.", - py::arg("offset") = 0) - .def("ContactAngularAccelerationsLocal", &deme::DEMTracker::GetContactAngularAccelerationsLocal, - "Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of " - "their contact with other simulation entities. The acceleration is in this object's local frame.") - - .def("ContactAngAccGlobal", &deme::DEMTracker::GetContactAngAccGlobal, - "Get the a portion of the angular acceleration of this tracked object, that is the result of its contact " - "with other simulation entities. The acceleration is in this object's global frame.", - py::arg("offset") = 0) - .def("ContactAngularAccelerationsGlobal", &deme::DEMTracker::GetContactAngularAccelerationsGlobal, - "Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of " - "their contact with other simulation entities. The acceleration is in this object's global frame.") - - .def("GetOwnerWildcardValue", - static_cast( - &deme::DEMTracker::GetOwnerWildcardValue), - "Get the owner's wildcard value.", py::arg("name"), py::arg("offset") = 0) - .def("GetOwnerWildcardValues", - static_cast (deme::DEMTracker::*)(const std::string&)>( - &deme::DEMTracker::GetOwnerWildcardValues), - "Get the owner wildcard values for all the owners entities tracked by this tracker.", py::arg("name")) - .def("GetGeometryWildcardValues", - static_cast (deme::DEMTracker::*)(const std::string&)>( - &deme::DEMTracker::GetGeometryWildcardValues), - "Get the geometry wildcard values for all the geometry entities tracked by this tracker.", py::arg("name")) - .def("GetGeometryWildcardValue", - static_cast( - &deme::DEMTracker::GetGeometryWildcardValue), - "Get the geometry wildcard values for a geometry entity tracked by this tracker.", py::arg("name"), - py::arg("offset")) - - .def("SetPos", static_cast(&deme::DEMTracker::SetPos), - "Set the position of this tracked object.", py::arg("pos"), py::arg("offset") = 0) - .def("SetPos", static_cast&)>(&deme::DEMTracker::SetPos), - "Set the positions of consecutive tracked objects.", py::arg("pos")) - - .def("SetAngVel", static_cast(&deme::DEMTracker::SetAngVel), - "Set the angular velocity of this tracked object in its own local coordinate system.", py::arg("angVel"), - py::arg("offset") = 0) - .def("SetAngVel", - static_cast&)>(&deme::DEMTracker::SetAngVel), - "Set the angular velocity of consecutive tracked objects in their own local coordinate systems.", - py::arg("angVel")) - - .def("SetVel", static_cast(&deme::DEMTracker::SetVel), - "Set the velocity of this tracked object in global frame.", py::arg("vel"), py::arg("offset") = 0) - .def("SetVel", static_cast&)>(&deme::DEMTracker::SetVel), - "Set the velocity of consecutive tracked objects in global frame.", py::arg("vel")) - - .def("SetOriQ", static_cast(&deme::DEMTracker::SetOriQ), - "Set the quaternion which represents the orientation of this tracked object's coordinate system.", - py::arg("oriQ"), py::arg("offset") = 0) - .def("SetOriQ", static_cast&)>(&deme::DEMTracker::SetOriQ), - "Set the quaternion which represents the orientation of consecutive tracked objects' coordinate systems.", - py::arg("oriQ")) - - .def("AddAcc", static_cast(&deme::DEMTracker::AddAcc), - "Add an extra acc to the tracked body, for the next time step. Note if the user intends to add a " - "persistent external force, then using family prescription is the better method.", - py::arg("acc"), py::arg("offset") = 0) - .def("AddAcc", static_cast&)>(&deme::DEMTracker::AddAcc), - "Add an extra acc to consecutive tracked objects, (only) for the next time step. Note if the user intends " - "to add a persistent external force, then using family prescription is the better method.", - py::arg("acc")) - - .def("AddAngAcc", static_cast(&deme::DEMTracker::AddAngAcc), - "Add an extra angular acceleration to the tracked body, for the next time step. Note if the user intends " - "to add a persistent external torque, then using family prescription is the better method.", - py::arg("angAcc"), py::arg("offset") = 0) - .def("AddAngAcc", - static_cast&)>(&deme::DEMTracker::AddAngAcc), - "Add an extra angular acceleration to consecutive tracked objects, (only) for the next time step. Note if " - "the user intends to add a persistent external torque, then using family prescription is the better " - "method.", - py::arg("angAcc")) - - .def("SetFamily", static_cast(&deme::DEMTracker::SetFamily), - "Change the family numbers of all the entities tracked by this tracker.", py::arg("fam_num")) - .def("SetFamily", static_cast(&deme::DEMTracker::SetFamily), - "Change the family number of one entities tracked by this tracker.", py::arg("fam_num"), py::arg("offset")) - - .def("UpdateMesh", - static_cast&)>(&deme::DEMTracker::UpdateMesh), - "Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes.", - py::arg("new_nodes")) - .def("UpdateMeshByIncrement", - static_cast&)>( - &deme::DEMTracker::UpdateMeshByIncrement), - "Change the coordinates of each mesh node by the given amount.", py::arg("deformation")) - - .def("GetMeshNodesGlobal", - static_cast (deme::DEMTracker::*)()>(&deme::DEMTracker::GetMeshNodesGlobal), - "Get the current locations of all the nodes in the mesh being tracked.") - - .def("GetMesh", &deme::DEMTracker::GetMesh, "Get a handle for the mesh this tracker is tracking.") - - .def("SetOwnerWildcardValue", &deme::DEMTracker::SetOwnerWildcardValue, - "Set a wildcard value of the owner this tracker is tracking.", py::arg("name"), py::arg("wc"), - py::arg("offset") = 0) - .def("SetOwnerWildcardValues", &deme::DEMTracker::SetOwnerWildcardValues, - "Set a wildcard value of the owner this tracker is tracking.", py::arg("name"), py::arg("wc")) - .def("SetGeometryWildcardValue", &deme::DEMTracker::SetGeometryWildcardValue, - "Set a wildcard value of the geometry entity this tracker is tracking.", py::arg("name"), py::arg("wc"), - py::arg("offset") = 0) - .def("SetGeometryWildcardValues", &deme::DEMTracker::SetGeometryWildcardValues, - "Set a wildcard value of the geometry entities this tracker is tracking.", py::arg("name"), py::arg("wc")) - - .def( - "GetContactForcesAndLocalTorque", - [](deme::DEMTracker& tracker, size_t offset) { - std::vector points, forces, torques; - tracker.GetContactForcesAndLocalTorque(points, forces, torques, offset); - return py::make_tuple(points, forces, torques); - }, - "Get all contact forces and local torques that concern this tracked object, as a list.", - py::arg("offset") = 0) - .def( - "GetContactForcesAndLocalTorqueForAll", - [](deme::DEMTracker& tracker) { - std::vector points, forces, torques; - tracker.GetContactForcesAndLocalTorqueForAll(points, forces, torques); - return py::make_tuple(points, forces, torques); - }, - "Get all contact forces and local torques that concern all objects tracked by this tracker, as a list.") - - .def( - "GetContactForcesAndGlobalTorque", - [](deme::DEMTracker& tracker, size_t offset) { - std::vector points, forces, torques; - tracker.GetContactForcesAndGlobalTorque(points, forces, torques, offset); - return py::make_tuple(points, forces, torques); - }, - "Get all contact forces and global torques that concern this tracked object, as a list.", - py::arg("offset") = 0) - .def( - "GetContactForcesAndGlobalTorqueForAll", - [](deme::DEMTracker& tracker) { - std::vector points, forces, torques; - tracker.GetContactForcesAndGlobalTorqueForAll(points, forces, torques); - return py::make_tuple(points, forces, torques); - }, - "Get all contact forces and global torques that concern all objects tracked by this tracker, as a list.") - - .def( - "GetContactForces", - [](deme::DEMTracker& tracker, size_t offset) { - std::vector points, forces; - tracker.GetContactForces(points, forces, offset); - return py::make_tuple(points, forces); - }, - "Get all contact forces that concern this tracked object, as a list.", py::arg("offset") = 0) - .def( - "GetContactForcesForAll", - [](deme::DEMTracker& tracker) { - std::vector points, forces; - tracker.GetContactForcesForAll(points, forces); - return py::make_tuple(points, forces); - }, - "Get all contact forces that concern all objects tracked by this tracker, as a list."); - - py::class_>(obj, "DEMForceModel") - .def(py::init()) - .def("SetForceModelType", &deme::DEMForceModel::SetForceModelType, "Set the contact force model type") - - .def("DefineCustomModel", &deme::DEMForceModel::DefineCustomModel, - "Define user-custom force model with a string which is your force calculation code.") - .def("ReadCustomModelFile", &deme::DEMForceModel::ReadCustomModelFile, - "Read user-custom force model from a file (which by default should reside in kernel/DEMUserScripts), " - "which contains your force calculation code. Returns 0 if read successfully, otherwise 1.") - .def("DefineCustomModelPrerequisites", &deme::DEMForceModel::DefineCustomModelPrerequisites, - "Define user-custom force model's utility __device__ functions with a string.") - .def("ReadCustomModelPrerequisitesFile", &deme::DEMForceModel::ReadCustomModelPrerequisitesFile, - "Read user-custom force model's utility __device__ functions from a file (which by default should reside " - "in kernel/DEMUserScripts). Returns 0 if read successfully, otherwise 1.") - - .def("SetMustHaveMatProp", &deme::DEMForceModel::SetMustHaveMatProp, - "Specifiy the material properties that this force model will use") - .def("SetMustPairwiseMatProp", &deme::DEMForceModel::SetMustPairwiseMatProp, - "Specifiy the material properties that are pair-wise (instead of being associated with each individual " - "material).") - .def("SetPerContactWildcards", &deme::DEMForceModel::SetPerContactWildcards, - "Set the names for the extra quantities that will be associated with each contact pair. For example, " - "history-based models should have 3 float arrays to store contact history. Only float is supported. Note " - "the initial value of all contact wildcard arrays is automatically 0") - .def("SetPerOwnerWildcards", &deme::DEMForceModel::SetPerOwnerWildcards, - " Set the names for the extra quantities that will be associated with each owner. For example, you can " - "use this to associate a cohesion parameter to each particle. Only float is supported.") - .def("SetPerGeometryWildcards", &deme::DEMForceModel::SetPerGeometryWildcards, - "Set the names for the extra quantities that will be associated with each geometry. For example, you can " - "use this to associate certain electric charges to each particle's each component which represents a " - "distribution of the charges. Only float is supported."); - - py::class_(obj, "DEMSolver") - .def(py::init(), py::arg("nGPUs") = 2) - .def("UpdateStepSize", &deme::DEMSolver::UpdateStepSize, - "Update the time step size. Used after system initialization.", py::arg("ts") = -1.0) - .def("SetNoForceRecord", &deme::DEMSolver::SetNoForceRecord, - "Instruct the solver that there is no need to record the contact force (and contact point location etc.) " - "in an array.", - py::arg("flag") = true) - .def("LoadSphereType", &deme::DEMSolver::LoadSphereType) - .def("EnsureKernelErrMsgLineNum", &deme::DEMSolver::EnsureKernelErrMsgLineNum, - "If true, each jitification string substitution will do a one-liner to one-liner replacement, so that if " - "the kernel compilation fails, the error meessage line number will reflex the actual spot where that " - "happens (instead of some random number).", - py::arg("flag") = true) - .def("UseCubForceCollection", &deme::DEMSolver::UseCubForceCollection, - "Whether the force collection (acceleration calc and reduction) process should be using CUB. If true, the " - "acceleration array is flattened and reduced using CUB; if false, the acceleration is computed and " - "directly applied to each body through atomic operations.", - py::arg("flag") = true) - .def("SetCollectAccRightAfterForceCalc", &deme::DEMSolver::SetCollectAccRightAfterForceCalc, - "Reduce contact forces to accelerations right after calculating them, in the same kernel. This may give " - "some performance boost if you have only polydisperse spheres, no clumps.", - py::arg("flag") = true) - - .def("SetInitBinSize", &deme::DEMSolver::SetInitBinSize, - " Explicitly instruct the bin size (for contact detection) that the solver should use.") - .def("SetOutputFormat", - static_cast(&deme::DEMSolver::SetOutputFormat), - "Choose sphere and clump output file format.") - .def("GetNumContacts", &deme::DEMSolver::GetNumContacts, - "Get the number of kT-reported potential contact pairs.") - .def("GetTimeStepSize", &deme::DEMSolver::GetTimeStepSize, "Get the current time step size in simulation.") - .def("SetCDUpdateFreq", &deme::DEMSolver::SetCDUpdateFreq, - "Set the number of dT steps before it waits for a contact-pair info update from kT.") - .def("GetSimTime", &deme::DEMSolver::GetSimTime, - "Get the simulation time passed since the start of simulation.") - .def("SetSimTime", &deme::DEMSolver::SetSimTime, - "Get the simulation time passed since the start of simulation.") - .def("UpdateClumps", &deme::DEMSolver::UpdateClumps, - "Transfer newly loaded clumps to the GPU-side in mid-simulation.") - .def("SetAdaptiveTimeStepType", &deme::DEMSolver::SetAdaptiveTimeStepType, - "Set the strategy for auto-adapting time step size (NOT implemented, no effect yet).") - .def("SetIntegrator", - static_cast(&deme::DEMSolver::SetIntegrator), - "Set the time integrator for this simulator.") - .def("SetIntegrator", - static_cast(&deme::DEMSolver::SetIntegrator), - "Set the time integrator for this simulator.") - .def("GetInitStatus", &deme::DEMSolver::GetInitStatus, "Return whether this simulation system is initialized.") - .def("GetJitStringSubs", &deme::DEMSolver::GetJitStringSubs, - "Get the jitification string substitution laundary list. It is needed by some of this simulation system's " - "friend classes.") - .def("GetJitifyOptions", &deme::DEMSolver::GetJitifyOptions, - "Get current jitification options. It is needed by some of this simulation system's friend classes.") - .def("SetJitifyOptions", &deme::DEMSolver::SetJitifyOptions, - "Set the jitification options. It is only needed by advanced users.") - - .def("SetInitBinSizeAsMultipleOfSmallestSphere", &deme::DEMSolver::SetInitBinSizeAsMultipleOfSmallestSphere, - "Explicitly instruct the bin size (for contact detection) that the solver should use, as a multiple of " - "the radius of the smallest sphere in simulation.") - .def( - "SetInitBinNumTarget", &deme::DEMSolver::SetInitBinNumTarget, - "Set the target number of bins (for contact detection) at the start of the simulation upon initialization.") - .def("InstructNumOwners", &deme::DEMSolver::InstructNumOwners, - "Explicitly instruct the sizes for the arrays at initialization time. This is useful when the number of " - "owners tends to change (especially gradually increase) frequently in the simulation.") - .def("UseFrictionalHertzianModel", &deme::DEMSolver::UseFrictionalHertzianModel, - py::return_value_policy::reference_internal, - "Instruct the solver to use frictonal (history-based) Hertzian contact force model.") - .def("UseFrictionlessHertzianModel", &deme::DEMSolver::UseFrictionlessHertzianModel, - py::return_value_policy::reference_internal, - "Instruct the solver to use frictonless Hertzian contact force model") - .def("DefineContactForceModel", &deme::DEMSolver::DefineContactForceModel, - py::return_value_policy::reference_internal, - "Define a custom contact force model by a string. Returns a pointer to the force model in use.") - .def("ReadContactForceModel", &deme::DEMSolver::ReadContactForceModel, - py::return_value_policy::reference_internal, - "Read user custom contact force model from a file (which by default should reside in " - "kernel/DEMUserScripts). Returns a pointer to the force model in use.") - - .def("GetContactForceModel", &deme::DEMSolver::GetContactForceModel, "Get the current force model", - py::return_value_policy::reference_internal) - .def("SetSortContactPairs", &deme::DEMSolver::SetSortContactPairs, - "Instruct the solver if contact pair arrays should be sorted (based on the types of contacts) before " - "usage.") - .def( - "SetJitifyClumpTemplates", &deme::DEMSolver::SetJitifyClumpTemplates, - "Instruct the solver to rearrange and consolidate clump templates information, then jitify it into GPU " - "kernels (if set to true), rather than using flattened sphere component configuration arrays whose entries " - "are associated with individual spheres.", - py::arg("use") = true) - .def("SetJitifyMassProperties", &deme::DEMSolver::SetJitifyMassProperties, - "Instruct the solver to rearrange and consolidate mass property information (for all owner types), then " - "jitify it into GPU kernels (if set to true), rather than using flattened mass property arrays whose " - "entries are associated with individual owners.", - py::arg("use") = true) - .def("SetExpandFactor", &deme::DEMSolver::SetExpandFactor, - "(Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are " - "expanded for the purpose of contact detection (safe, and creates false positives). If fix is set to " - "true, then this expand factor does not change even if the user uses variable time step size.", - py::arg("beta"), py::arg("fix") = true) - - .def("SetExpandSafetyType", &deme::DEMSolver::SetExpandSafetyType, - "A string. If 'auto': the solver automatically derives.") - .def("SetExpandSafetyAdder", &deme::DEMSolver::SetExpandSafetyAdder, - "Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving " - "the thinckness of the contact `safety' margin") - .def("SetMaxSphereInBin", &deme::DEMSolver::SetMaxSphereInBin, - "Used to force the solver to error out when there are too many spheres in a bin. A huge number can be " - "used to discourage this error type") - .def("SetMaxTriangleInBin", &deme::DEMSolver::SetMaxTriangleInBin, - "Used to force the solver to error out when there are too many spheres in a bin. A huge number can be " - "used to discourage this error type") - - .def("SetErrorOutAvgContacts", &deme::DEMSolver::SetErrorOutAvgContacts, - "Set the average number of contacts a sphere has, before the solver errors out. A huge number can be used " - "to discourage this error type. Defaulted to 100") - .def("GetAvgSphContacts", &deme::DEMSolver::GetAvgSphContacts, - "Get the current number of contacts each sphere has") - .def("UseAdaptiveBinSize", &deme::DEMSolver::UseAdaptiveBinSize, - "Enable or disable the use of adaptive bin size (by default it is on)", py::arg("use") = true) - .def("DisableAdaptiveBinSize", &deme::DEMSolver::DisableAdaptiveBinSize, - "Disable the use of adaptive bin size (always use initial size)") - .def("UseAdaptiveUpdateFreq", &deme::DEMSolver::UseAdaptiveUpdateFreq, - "Enable or disable the use of adaptive max update step count (by default it is on)", py::arg("use") = true) - .def("DisableAdaptiveUpdateFreq", &deme::DEMSolver::DisableAdaptiveUpdateFreq, - "Disable the use of adaptive max update step count (always use initial update frequency)") - .def("SetAdaptiveBinSizeDelaySteps", &deme::DEMSolver::SetAdaptiveBinSizeDelaySteps, - "Adjust how frequent kT updates the bin size") - .def("SetAdaptiveBinSizeMaxRate", &deme::DEMSolver::SetAdaptiveBinSizeMaxRate, - "Set the max rate that the bin size can change in one adjustment") - .def("SetAdaptiveBinSizeAcc", &deme::DEMSolver::SetAdaptiveBinSizeAcc, - "Set how fast kT changes the direction of bin size adjustmemt when there's a more beneficial direction") - .def("SetAdaptiveBinSizeUpperProactivity", &deme::DEMSolver::SetAdaptiveBinSizeUpperProactivity, - "Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a " - "bin)") - .def( - "SetAdaptiveBinSizeLowerProactivity", &deme::DEMSolver::SetAdaptiveBinSizeLowerProactivity, - "Set how proactive the solver is in avoiding the bin being too small (leading to too many bins in domain).") - .def("GetBinSize", &deme::DEMSolver::GetBinSize, - "Get the current bin (for contact detection) size. Must be called from synchronized stance.") - .def("GetBinNum", &deme::DEMSolver::GetBinNum, - "Get the current number of bins (for contact detection). Must be called from synchronized stance.") - .def("SetCDMaxUpdateFreq", &deme::DEMSolver::SetCDMaxUpdateFreq, - "Set the upper bound of kT update frequency (when it is adjusted automatically).") - .def("SetCDNumStepsMaxDriftAheadOfAvg", &deme::DEMSolver::SetCDNumStepsMaxDriftAheadOfAvg, - "Set the number of steps dT configures its max drift more than average drift steps.") - .def("SetCDNumStepsMaxDriftMultipleOfAvg", &deme::DEMSolver::SetCDNumStepsMaxDriftMultipleOfAvg, - "Set the multiplier which dT configures its max drift to be w.r.t. the average drift steps.") - .def("SetCDNumStepsMaxDriftHistorySize", &deme::DEMSolver::SetCDNumStepsMaxDriftHistorySize) - .def("GetUpdateFreq", &deme::DEMSolver::GetUpdateFreq, "Get the current update frequency used by the solver.") - .def("SetForceCalcThreadsPerBlock", &deme::DEMSolver::SetForceCalcThreadsPerBlock, - "Set the number of threads per block in force calculation (default 256).") - .def("Duplicate", - static_cast (deme::DEMSolver::*)( - const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), - "Duplicate a material that is loaded into the system.") - .def("Duplicate", - static_cast (deme::DEMSolver::*)( - const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), - "Duplicate a clump template that is loaded into the system.") - .def("Duplicate", - static_cast (deme::DEMSolver::*)( - const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), - "Duplicate a batch of clumps that is loaded into the system.") - .def("AddExternalObject", &deme::DEMSolver::AddExternalObject, - "Add an analytical object to the simulation system.") - .def( - "SetOutputContent", - static_cast&)>(&deme::DEMSolver::SetOutputContent), - "Specify the information that needs to go into the clump or sphere output files.") - .def("SetMeshOutputFormat", - static_cast(&deme::DEMSolver::SetMeshOutputFormat), - "Specify the output file format of meshes.") - .def("SetContactOutputContent", - static_cast&)>( - &deme::DEMSolver::SetContactOutputContent), - "Specify the information that needs to go into the contact pair output files.") - .def("SetContactOutputFormat", - static_cast(&deme::DEMSolver::SetContactOutputFormat), - "Specify the file format of contact pairs.") - .def("SetVerbosity", static_cast(&deme::DEMSolver::SetVerbosity), - "Set the verbosity level of the solver.") - - .def("AddKernelInclude", &deme::DEMSolver::AddKernelInclude, - "Add a library that the kernels will be compiled with (so that the user can use the provided methods in " - "their customized code, like force model).") - .def("SetKernelInclude", &deme::DEMSolver::SetKernelInclude, - "Set the kernels' headers' extra include lines. Useful for customization.") - .def("RemoveKernelInclude", &deme::DEMSolver::RemoveKernelInclude, - "Remove all extra libraries that the kernels `include' in their headers.") - - .def("LoadMaterial", - static_cast (deme::DEMSolver::*)( - const std::unordered_map&)>(&deme::DEMSolver::LoadMaterial), - "Load materials properties (Young's modulus, Poisson's ratio...) into the system.") - .def("LoadMaterial", - static_cast (deme::DEMSolver::*)(deme::DEMMaterial&)>( - &deme::DEMSolver::LoadMaterial), - "Load materials properties into the system.") - .def("InstructBoxDomainDimension", - static_cast( - &deme::DEMSolver::InstructBoxDomainDimension), - "Set the Box Domain Dimension", py::arg("x"), py::arg("y"), py::arg("z"), py::arg("dir_exact") = "none") - .def("InstructBoxDomainDimension", - static_cast&, const std::pair&, - const std::pair&, const std::string& dir_exact)>( - &deme::DEMSolver::InstructBoxDomainDimension), - "Set the span of the Box Domain", py::arg("x"), py::arg("y"), py::arg("z"), py::arg("dir_exact") = "none") - .def("InstructBoxDomainBoundingBC", &deme::DEMSolver::InstructBoxDomainBoundingBC, - "Instruct if and how we should add boundaries to the simulation world upon initialization. Choose between " - "`none', `all' (add 6 boundary planes) and `top_open' (add 5 boundary planes and leave the z-directon top " - "open). Also specifies the material that should be assigned to those bounding boundaries.") - .def("SetMaterialPropertyPair", &deme::DEMSolver::SetMaterialPropertyPair, - "Set the value for a material property that by nature involves a pair of a materials (e.g. friction " - "coefficient).") - .def("AddBCPlane", - static_cast (deme::DEMSolver::*)( - const std::vector&, const std::vector&, const std::shared_ptr&)>( - &deme::DEMSolver::AddBCPlane), - "Add an analytical plane to the simulation.") - .def("Track", (&deme::DEMSolver::PythonTrack), - "Create a DEMTracker to allow direct control/modification/query to this external object/batch of " - "clumps/triangle mesh object.") - .def("AddWavefrontMeshObject", - static_cast (deme::DEMSolver::*)( - const std::string&, const std::shared_ptr&, bool, bool)>( - &deme::DEMSolver::AddWavefrontMeshObject), - "Load a mesh-represented object.", py::arg("filename"), py::arg("mat"), py::arg("load_normals") = true, - py::arg("load_uv") = false) - .def("AddWavefrontMeshObject", - static_cast (deme::DEMSolver::*)(deme::DEMMeshConnected&)>( - &deme::DEMSolver::AddWavefrontMeshObject), - "Load a mesh-represented object.") - .def("AddWavefrontMeshObject", - static_cast (deme::DEMSolver::*)( - const std::string& filename, bool, bool)>(&deme::DEMSolver::AddWavefrontMeshObject), - "Load a mesh-represented object.", py::arg("filename"), py::arg("load_normals") = true, - py::arg("load_uv") = false) - .def("LoadClumpType", - static_cast (deme::DEMSolver::*)( - float, const std::vector&, const std::string, const std::shared_ptr&)>( - &deme::DEMSolver::LoadClumpType), - "Load a clump type into the API-level cache") - .def("LoadClumpType", - static_cast (deme::DEMSolver::*)( - float mass, const std::vector&, const std::vector&, - const std::vector>&, const std::shared_ptr&)>( - &deme::DEMSolver::LoadClumpType), - "Load a clump type into the API-level cache") - .def("LoadClumpType", - static_cast (deme::DEMSolver::*)(deme::DEMClumpTemplate&)>( - &deme::DEMSolver::LoadClumpType), - "Load a clump type into the API-level cache") - .def("LoadClumpType", - static_cast (deme::DEMSolver::*)( - float, const std::vector&, const std::string, - const std::vector>&)>(&deme::DEMSolver::LoadClumpType), - "Load a clump type into the API-level cache") - .def("LoadClumpType", - static_cast (deme::DEMSolver::*)( - float, const std::vector&, const std::string, const std::shared_ptr&)>( - &deme::DEMSolver::LoadClumpType), - "Load a clump type into the API-level cache") - - .def("GetOwnerContactClumps", &deme::DEMSolver::GetOwnerContactClumps, - "Get the clumps that are in contact with this owner as a vector.") - .def("GetOwnerPosition", &deme::DEMSolver::GetOwnerPosition, "Get position of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerAngVel", &deme::DEMSolver::GetOwnerAngVel, "Get angular velocity of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerOriQ", &deme::DEMSolver::GetOwnerOriQ, "Get quaternion of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerVelocity", &deme::DEMSolver::GetOwnerVelocity, "Get velocity of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerAcc", &deme::DEMSolver::GetOwnerAcc, "Get the acceleration of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerAngAcc", &deme::DEMSolver::GetOwnerAngAcc, - "Get the angular acceleration of n consecutive owners.", py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerFamily", &deme::DEMSolver::GetOwnerFamily, "Get the family number of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerMass", &deme::DEMSolver::GetOwnerMass, "Get the mass of n consecutive owners.", - py::arg("ownerID"), py::arg("n") = 1) - .def("GetOwnerMOI", &deme::DEMSolver::GetOwnerMOI, - "Get the moment of inertia (in principal axis frame) of n consecutive owners.", py::arg("ownerID"), - py::arg("n") = 1) - - .def("SetOwnerPosition", &deme::DEMSolver::SetOwnerPosition, - "Set position of consecutive owners starting from ownerID, based on input position vector. N (the size of " - "the input vector) elements will be modified.") - .def("SetOwnerAngVel", &deme::DEMSolver::SetOwnerAngVel, - "Set angular velocity of consecutive owners starting from ownerID, based on input angular velocity " - "vector. N (the size of the input vector) elements will be modified.") - .def("SetOwnerVelocity", &deme::DEMSolver::SetOwnerVelocity, - "Set velocity of consecutive owners starting from ownerID, based on input velocity vector. N (the size of " - "the input vector) elements will be modified.") - .def("SetOwnerOriQ", &deme::DEMSolver::SetOwnerOriQ, - "Set quaternion of consecutive owners starting from ownerID, based on input quaternion vector. N (the " - "size of the input vector) elements will be modified.") - .def("SetOwnerFamily", &deme::DEMSolver::SetOwnerFamily, "Set the family number of consecutive owners.", - py::arg("ownerID"), py::arg("fam"), py::arg("n") = 1) - - .def("SetTriNodeRelPos", &deme::DEMSolver::SetTriNodeRelPos, - "Rewrite the relative positions of the flattened triangle soup.") - .def("UpdateTriNodeRelPos", &deme::DEMSolver::UpdateTriNodeRelPos, - "Update the relative positions of the flattened triangle soup.") - .def("GetCachedMesh", &deme::DEMSolver::GetCachedMesh, "Get a handle for the mesh this tracker is tracking.") - .def("GetMeshNodesGlobal", &deme::DEMSolver::GetMeshNodesGlobal, - "Get the current locations of all the nodes in the mesh being tracked.") - - .def("GetClumpContacts", - static_cast> (deme::DEMSolver::*)() const>( - &deme::DEMSolver::GetClumpContacts), - "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " - "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") - .def("GetClumpContacts", - static_cast> (deme::DEMSolver::*)( - const std::set&) const>(&deme::DEMSolver::GetClumpContacts), - "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " - "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to " - "include only these families in the output.") - .def("GetClumpContacts", - static_cast> (deme::DEMSolver::*)( - std::vector>&) const>(&deme::DEMSolver::GetClumpContacts), - "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " - "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") - - .def("GetContacts", - static_cast> (deme::DEMSolver::*)() const>( - &deme::DEMSolver::GetContacts), - "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " - "contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") - .def("GetContacts", - static_cast> (deme::DEMSolver::*)( - const std::set&) const>(&deme::DEMSolver::GetContacts), - "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " - "contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to " - "include only these families in the output.") - .def("GetContacts", - static_cast> (deme::DEMSolver::*)( - std::vector>&) const>(&deme::DEMSolver::GetContacts), - "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " - "contacts (not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") - - .def("GetContactDetailedInfo", &deme::DEMSolver::GetContactDetailedInfo, - "Get all contact pairs' detailed information (actual content based on the setting with " - "SetContactOutputContent; default are owner IDs, contact point location, contact force, and associated " - "wildcard values) in the simulation system. Note all GetContact-like methods reports potential contacts " - "(not necessarily confirmed contacts), meaning they are similar to what " - "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.", - py::arg("force_thres") = -1.0) - - .def("GetHostMemUsageDynamic", &deme::DEMSolver::GetHostMemUsageDynamic, - "Get the host memory usage (in bytes) on dT.") - .def("GetDeviceMemUsageDynamic", &deme::DEMSolver::GetDeviceMemUsageDynamic, - "Get the device memory usage (in bytes) on dT.") - .def("GetHostMemUsageKinematic", &deme::DEMSolver::GetHostMemUsageKinematic, - "Get the host memory usage (in bytes) on kT.") - .def("GetDeviceMemUsageKinematic", &deme::DEMSolver::GetDeviceMemUsageKinematic, - "Get the device memory usage (in bytes) on kT.") - .def("ShowMemStats", &deme::DEMSolver::ShowMemStats, "Print the current memory usage in pretty format.") - - .def("AddClumps", - static_cast (deme::DEMSolver::*)(deme::DEMClumpBatch&)>( - &deme::DEMSolver::AddClumps), - "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " - "location means the location of the clumps' CoM coordinates in the global frame.") - .def("AddClumps", - static_cast (deme::DEMSolver::*)( - std::shared_ptr&, const std::vector>&)>( - &deme::DEMSolver::AddClumps), - "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " - "location means the location of the clumps' CoM coordinates in the global frame.") - .def("AddClumps", - static_cast (deme::DEMSolver::*)( - const std::vector>&, const std::vector>&)>( - &deme::DEMSolver::AddClumps), - "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " - "location means the location of the clumps' CoM coordinates in the global frame.") - - .def("SetFamilyFixed", &deme::DEMSolver::SetFamilyFixed, "Mark all entities in this family to be fixed.") - - .def("SetFamilyPrescribedAngVel", - static_cast( - &deme::DEMSolver::SetFamilyPrescribedAngVel), - "Set the prescribed angular velocity to all entities in a family. If dictate is set to true, then this " - "family will not be influenced by the force exerted from other simulation entites (both linear and " - "rotational motions). If false, only specified components (that is, not specified with 'none') will not " - "be influenced by the force exerted from other simulation entites.", - py::arg("ID"), py::arg("velX"), py::arg("velY"), py::arg("velZ"), py::arg("dictate") = true, - py::arg("pre") = "none") - .def("SetFamilyPrescribedAngVel", - static_cast(&deme::DEMSolver::SetFamilyPrescribedAngVel), - "Let the angular velocities of all entites in this family always keep `as is', and not influenced by the " - "force exerted from other simulation entites.") - .def("SetFamilyPrescribedAngVelX", &deme::DEMSolver::SetFamilyPrescribedAngVelX, - "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - .def("SetFamilyPrescribedAngVelY", &deme::DEMSolver::SetFamilyPrescribedAngVelY, - "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - .def("SetFamilyPrescribedAngVelZ", &deme::DEMSolver::SetFamilyPrescribedAngVelZ, - "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - - .def("SetFamilyPrescribedLinVel", - static_cast( - &deme::DEMSolver::SetFamilyPrescribedLinVel), - "Set the prescribed linear velocity to all entities in a family. If dictate is set to true, then this " - "family will not be influenced by the force exerted from other simulation entites (both linear and " - "rotational motions). If false, only specified components (that is, not specified with 'none') will not " - "be influenced by the force exerted from other simulation entites.", - py::arg("ID"), py::arg("velX"), py::arg("velY"), py::arg("velZ"), py::arg("dictate") = true, - py::arg("pre") = "none") - .def("SetFamilyPrescribedLinVel", - static_cast(&deme::DEMSolver::SetFamilyPrescribedLinVel), - "Let the linear velocities of all entites in this family always keep `as is', and not influenced by the " - "force exerted from other simulation entites.") - .def("SetFamilyPrescribedLinVelX", &deme::DEMSolver::SetFamilyPrescribedLinVelX, - "Let the X component of the linear velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - .def("SetFamilyPrescribedLinVelY", &deme::DEMSolver::SetFamilyPrescribedLinVelY, - "Let the Y component of the linear velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - .def("SetFamilyPrescribedLinVelZ", &deme::DEMSolver::SetFamilyPrescribedLinVelZ, - "Let the Z component of the linear velocities of all entites in this family always keep `as is', and not " - "influenced by the force exerted from other simulation entites.") - - .def("SetFamilyPrescribedPosition", - static_cast( - &deme::DEMSolver::SetFamilyPrescribedPosition), - "Keep the positions of all entites in this family to remain exactly the user-specified values.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("dictate") = true, - py::arg("pre") = "none") - .def("SetFamilyPrescribedPosition", - static_cast(&deme::DEMSolver::SetFamilyPrescribedPosition), - "Keep the positions of all entites in this family to remain as is.") - .def("SetFamilyPrescribedPositionX", &deme::DEMSolver::SetFamilyPrescribedPositionX, - "Let the X component of the linear positions of all entites in this family always keep `as is'.") - .def("SetFamilyPrescribedPositionY", &deme::DEMSolver::SetFamilyPrescribedPositionY, - "Let the Y component of the linear positions of all entites in this family always keep `as is'.") - .def("SetFamilyPrescribedPositionZ", &deme::DEMSolver::SetFamilyPrescribedPositionZ, - "Let the Z component of the linear positions of all entites in this family always keep `as is'.") - - .def("SetFamilyPrescribedQuaternion", - static_cast( - &deme::DEMSolver::SetFamilyPrescribedQuaternion), - "Keep the orientation quaternions of all entites in this family to remain exactly the user-specified " - "values.", - py::arg("ID"), py::arg("q_formula"), py::arg("dictate") = true) - .def("SetFamilyPrescribedQuaternion", - static_cast(&deme::DEMSolver::SetFamilyPrescribedQuaternion), - "Let the orientation quaternions of all entites in this family always keep `as is'.") - - .def("AddFamilyPrescribedAcc", &deme::DEMSolver::AddFamilyPrescribedAcc, - "The entities in this family will always experienced an extra acceleration defined using this method.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") - .def("AddFamilyPrescribedAngAcc", &deme::DEMSolver::AddFamilyPrescribedAngAcc, - "The entities in this family will always experienced an extra angular acceleration defined using this " - "method.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") - - .def("CorrectFamilyLinVel", &deme::DEMSolver::CorrectFamilyLinVel, - "The entities in this family will always experience an added linear-velocity correction defined using " - "this method. At the same time, they are still subject to the `simulation physics'.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") - .def("CorrectFamilyAngVel", &deme::DEMSolver::CorrectFamilyAngVel, - "The entities in this family will always experience an added angular-velocity correction defined using " - "this method. At the same time, they are still subject to the `simulation physics'.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") - - .def("CorrectFamilyPosition", &deme::DEMSolver::CorrectFamilyPosition, - "The entities in this family will always experience an added positional correction defined using this " - "method. At the same time, they are still subject to the `simulation physics'.", - py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") - .def("CorrectFamilyQuaternion", &deme::DEMSolver::CorrectFamilyQuaternion, - "The entities in this family will always experience an added quaternion correction defined using this " - "method. At the same time, they are still subject to the `simulation physics'.", - py::arg("ID"), py::arg("q_formula")) - - .def("SetContactWildcards", &deme::DEMSolver::SetContactWildcards, - "Set the names for the extra quantities that will be associated with each contact pair.") - .def("SetOwnerWildcards", &deme::DEMSolver::SetOwnerWildcards, - "Set the names for the extra quantities that will be associated with each owner.") - .def("SetGeometryWildcards", &deme::DEMSolver::SetGeometryWildcards, - "Set the names for the extra quantities that will be associated with each geometry entity (such as " - "sphere, triangle).") - - .def("SetFamilyContactWildcardValueEither", &deme::DEMSolver::SetFamilyContactWildcardValueEither, - "Change the value of contact wildcards to val if either of the contact geometries is in family N.") - .def("SetFamilyContactWildcardValueBoth", &deme::DEMSolver::SetFamilyContactWildcardValueBoth, - "Change the value of contact wildcards to val if both of the contact geometries are in family N.") - .def("SetFamilyContactWildcardValue", &deme::DEMSolver::SetFamilyContactWildcardValue, - "Change the value of contact wildcards to val if one of the contact geometry is in family N1, and the " - "other is in N2.") - .def("SetContactWildcardValue", &deme::DEMSolver::SetContactWildcardValue, - "Change the value of contact wildcards to val. Apply to all simulation bodies that are present.") - - .def("MarkFamilyPersistentContactEither", &deme::DEMSolver::MarkFamilyPersistentContactEither, - "Make it so that for any currently-existing contact, if one of its contact geometries is in family N, " - "then this contact will never be removed.") - .def("MarkFamilyPersistentContactBoth", &deme::DEMSolver::MarkFamilyPersistentContactBoth, - "Make it so that for any currently-existing contact, if both of its contact geometries are in family N, " - "then this contact will never be removed.") - .def("MarkFamilyPersistentContact", &deme::DEMSolver::MarkFamilyPersistentContact, - "Make it so that if for any currently-existing contact, if its two contact geometries are in family N1 " - "and N2 respectively, this contact will never be removed.") - .def("MarkPersistentContact", &deme::DEMSolver::MarkPersistentContact, - "Make it so that all currently-existing contacts in this simulation will never be removed.") - - .def("RemoveFamilyPersistentContactEither", &deme::DEMSolver::RemoveFamilyPersistentContactEither, - "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactEither.") - .def("RemoveFamilyPersistentContactBoth", &deme::DEMSolver::RemoveFamilyPersistentContactBoth, - "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactBoth.") - .def("RemoveFamilyPersistentContact", &deme::DEMSolver::RemoveFamilyPersistentContact, - "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContact.") - .def("RemovePersistentContact", &deme::DEMSolver::RemovePersistentContact, - "Cancel contact persistence qualification. Work like the inverse of MarkPersistentContact.") - - .def("GetOwnerContactForces", - static_cast& ownerIDs, - std::vector& points, std::vector& forces)>( - &deme::DEMSolver::GetOwnerContactForces), - "Get all contact forces that concern a list of owners.") - .def("GetOwnerContactForces", - static_cast& ownerIDs, std::vector& points, std::vector& forces, - std::vector& torques, bool torque_in_local)>(&deme::DEMSolver::GetOwnerContactForces), - "Get all contact forces and torque that concern a list of owners.", py::arg("ownerIDs"), py::arg("points"), - py::arg("forces"), py::arg("poitorquesnts"), py::arg("torque_in_local") = false) - - .def("SetTriWildcardValue", &deme::DEMSolver::SetTriWildcardValue, "Set the wildcard values of some triangles.") - .def("SetSphereWildcardValue", &deme::DEMSolver::SetSphereWildcardValue, - "Set the wildcard values of some spheres.") - .def("SetAnalWildcardValue", &deme::DEMSolver::SetAnalWildcardValue, - "Set the wildcard values of some analytical components.") - - .def("SetOwnerWildcardValue", - static_cast& vals)>( - &deme::DEMSolver::SetOwnerWildcardValue), - "Set the wildcard values of some owners using a list.", py::arg("ownerIDs"), py::arg("name"), - py::arg("vals")) - .def("SetOwnerWildcardValue", - static_cast(&deme::DEMSolver::SetOwnerWildcardValue), - "Set the wildcard values of some owners using a list.", py::arg("ownerIDs"), py::arg("name"), - py::arg("val"), py::arg("n") = 1) - - .def("GetOwnerWildcardValue", - static_cast (deme::DEMSolver::*)(deme::bodyID_t ownerID, const std::string& name, - deme::bodyID_t n)>( - &deme::DEMSolver::GetOwnerWildcardValue), - "Get the owner wildcard's values of some owners.", py::arg("ownerID"), py::arg("name"), py::arg("n") = 1) - .def("GetAllOwnerWildcardValue", &deme::DEMSolver::GetAllOwnerWildcardValue, - "Get the owner wildcard's values of all entities.") - .def("GetFamilyOwnerWildcardValue", &deme::DEMSolver::GetFamilyOwnerWildcardValue, - "Get the owner wildcard's values of all entities in family N.") - - .def("GetTriWildcardValue", &deme::DEMSolver::GetTriWildcardValue, - "Get the geometry wildcard's values of a series of triangles.") - .def("GetSphereWildcardValue", &deme::DEMSolver::GetSphereWildcardValue, - "Get the geometry wildcard's values of a series of spheres.") - .def("GetAnalWildcardValue", &deme::DEMSolver::GetAnalWildcardValue, - "Get the geometry wildcard's values of a series of analytical entities.") - - .def("SyncMemoryTransfer", &deme::DEMSolver::SyncMemoryTransfer, - "If the user used async-ed version of a tracker's get/set methods (to get a speed boost in many piecemeal " - "accesses of a long array), this method should be called to mark the end of to-host transactions.") - - .def("SetFamilyOwnerWildcardValue", - static_cast& vals)>( - &deme::DEMSolver::SetFamilyOwnerWildcardValue), - "Modify the owner wildcard's values of all entities in family N.") - .def("SetFamilyOwnerWildcardValue", - static_cast( - &deme::DEMSolver::SetFamilyOwnerWildcardValue), - "Modify the owner wildcard's values of all entities in family N.") - - .def("SetFamilyClumpMaterial", &deme::DEMSolver::SetFamilyClumpMaterial, - "Set all clumps in this family to have this material.") - .def("SetFamilyMeshMaterial", &deme::DEMSolver::SetFamilyMeshMaterial, - "Set all meshes in this family to have this material.") - .def("SetFamilyExtraMargin", &deme::DEMSolver::SetFamilyExtraMargin, - "Add an extra contact margin to entities in a family so they are registered as potential contact pairs " - "earlier.") - - .def("ClearCache", &deme::DEMSolver::ClearCache, - "Remove host-side cached vectors (so you can re-define them, and then re-initialize system).") - - .def("CreateInspector", - static_cast (deme::DEMSolver::*)(const std::string&)>( - &deme::DEMSolver::CreateInspector), - "Create a inspector object that can help query some statistical info of the clumps in the simulation.", - py::arg("quantity") = "clump_max_z") - .def("GetNumClumps", &deme::DEMSolver::GetNumClumps, - "Return the number of clumps that are currently in the simulation. Must be used after initialization.") - .def("GetNumOwners", &deme::DEMSolver::GetNumOwners, - "Return the total number of owners (clumps + meshes + analytical objects) that are currently in the " - "simulation. Must be used after initialization.") - .def("CreateInspector", - static_cast (deme::DEMSolver::*)( - const std::string&, const std::string&)>(&deme::DEMSolver::CreateInspector), - "Create a inspector object that can help query some statistical info of the clumps in the simulation.") - .def("SetInitTimeStep", &deme::DEMSolver::SetInitTimeStep, - "Set the initial time step size. If using constant step size, then this will be used throughout; " - "otherwise, the actual step size depends on the variable step strategy.") - .def("SetGravitationalAcceleration", - static_cast&)>( - &deme::DEMSolver::SetGravitationalAcceleration), - "Set gravitational pull") - .def("SetMaxVelocity", &deme::DEMSolver::SetMaxVelocity, - "Set the maximum expected particle velocity. The solver will not use a velocity larger than this for " - "determining the margin thickness, and velocity larger than this will be considered a system anomaly.") - .def("SetErrorOutVelocity", &deme::DEMSolver::SetErrorOutVelocity, - "Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage " - "this error type. Defaulted to 5e4.") - .def("SetExpandSafetyMultiplier", &deme::DEMSolver::SetExpandSafetyMultiplier, - "Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the " - "contact `safety' margin.") - .def("Initialize", &deme::DEMSolver::Initialize, "Initializes the system.", py::arg("dry_run") = false) - .def("WriteSphereFile", - static_cast(&deme::DEMSolver::WriteSphereFile), - "Writes the current status of clumps (but decomposed as spheres) file.") - .def("WriteMeshFile", - static_cast(&deme::DEMSolver::WriteMeshFile), - "Write the current status of all meshes to a file.") - .def("WriteClumpFile", - static_cast( - &deme::DEMSolver::WriteClumpFile), - "Write the current status of clumps to a file.", py::arg("outfilename"), py::arg("accuracy") = 10) - .def( - "WriteContactFile", - static_cast(&deme::DEMSolver::WriteContactFile), - "Write all contact pairs to a file. Forces smaller than threshold will not be outputted.", - py::arg("outfilename"), py::arg("force_thres") = 1e-30) - .def("WriteContactFileIncludingPotentialPairs", - static_cast( - &deme::DEMSolver::WriteContactFileIncludingPotentialPairs), - "Write all contact pairs kT-supplied to a file, thus including the potential ones (those are not yet in " - "contact, or recently used to be in contact).", - py::arg("outfilename")) - - // Maybe add checkpoint-reading methods here... - .def("DoDynamics", &deme::DEMSolver::DoDynamics, - "Advance simulation by this amount of time (but does not attempt to sync kT and dT). This can work with " - "both long and short call durations and allows interplay with co-simulation APIs.") - .def("DoStepDynamics", &deme::DEMSolver::DoStepDynamics, - "Equivalent to calling DoDynamics with the time step size as the argument.") - .def("DoDynamicsThenSync", &deme::DEMSolver::DoDynamicsThenSync, - "Advance simulation by this amount of time, and at the end of this call, synchronize kT and dT. This is " - "suitable for a longer call duration and without co-simulation.") - .def("UpdateSimParams", &deme::DEMSolver::UpdateSimParams, - "Transfer the cached sim params to the workers. Used for sim environment modification after system " - "initialization.") - .def("ChangeFamily", &deme::DEMSolver::ChangeFamily, - "Change all entities with family number ID_from to have a new number ID_to, when the condition defined by " - "the string is satisfied by the entities in question. This should be called before initialization, and " - "will be baked into the solver, so the conditions will be checked and changes applied every time step.") - .def("ChangeFamilyWhen", &deme::DEMSolver::ChangeFamilyWhen, - "Change all entities with family number ID_from to have a new number ID_to, when the condition defined by " - "the string is satisfied by the entities in question. This should be called before initialization, and " - "will be baked into the solver, so the conditions will be checked and changes applied every time step.") - .def("ChangeClumpFamily", &deme::DEMSolver::ChangeClumpFamily, - "Change the family number for the clumps in a box region to the specified value.", py::arg("fam_num"), - py::arg("X") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), - py::arg("Y") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), - py::arg("Z") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), - py::arg("orig_fam") = std::set()) - - .def("ShowThreadCollaborationStats", &deme::DEMSolver::ShowThreadCollaborationStats, - "Show the collaboration stats between dT and kT. This is more useful for tweaking the number of time " - "steps that dT should be allowed to be in advance of kT.") - .def("ShowTimingStats", &deme::DEMSolver::ShowTimingStats, - "Show the wall time and percentages of wall time spend on various solver tasks.") - .def("PrintKinematicScratchSpaceUsage", &deme::DEMSolver::PrintKinematicScratchSpaceUsage, - "Print kT's scratch space usage. This is a debug method.") - .def("ShowAnomalies", &deme::DEMSolver::ShowAnomalies, - "Show potential anomalies that may have been there in the simulation, then clear the anomaly log.") - .def("ClearThreadCollaborationStats", &deme::DEMSolver::ClearThreadCollaborationStats, - "Reset the collaboration stats between dT and kT back to the initial value (0). You should call this if " - "you want to start over and re-inspect the stats of the new run; otherwise, it is generally not needed, " - "you can go ahead and destroy DEMSolver.") - .def("ClearTimingStats", &deme::DEMSolver::ClearTimingStats, - "Reset the recordings of the wall time and percentages of wall time spend on various solver tasks.") - .def("PurgeFamily", &deme::DEMSolver::PurgeFamily) - .def("ReleaseFlattenedArrays", &deme::DEMSolver::ReleaseFlattenedArrays) - .def("GetWhetherForceCollectInKernel", &deme::DEMSolver::GetWhetherForceCollectInKernel, - "Return whether the solver is currently reducing force in the force calculation kernel.") - .def("AddOwnerNextStepAcc", &deme::DEMSolver::AddOwnerNextStepAcc, - "Add an extra acceleration to a owner for the next time step.") - .def("AddOwnerNextStepAngAcc", &deme::DEMSolver::AddOwnerNextStepAngAcc, - " Add an extra angular acceleration to a owner for the next time step.") - .def("DisableContactBetweenFamilies", &deme::DEMSolver::DisableContactBetweenFamilies, - "Instruct the solver that the 2 input families should not have contacts (a.k.a. ignored, if such a pair " - "is encountered in contact detection). These 2 families can be the same (which means no contact within " - "members of that family).") - .def("EnableContactBetweenFamilies", &deme::DEMSolver::EnableContactBetweenFamilies, - "Re-enable contact between 2 families after the system is initialized.") - .def("DisableFamilyOutput", &deme::DEMSolver::DisableFamilyOutput, - "Prevent entites associated with this family to be outputted to files."); - - py::class_>(obj, "DEMMaterial") - .def(py::init&>()) - .def_readwrite("mat_prop", &deme::DEMMaterial::mat_prop) - .def_readwrite("load_order", &deme::DEMMaterial::load_order); - - py::class_>(obj, "DEMClumpTemplate") - .def(py::init<>()) - .def("Mass", [](const deme::DEMClumpTemplate& clump) { return clump.mass; }) - .def("MOI", [](const deme::DEMClumpTemplate& clump) { return clump.MOI; }) - .def("SetMass", &deme::DEMClumpTemplate::SetMass) - .def("SetMOI", - static_cast&)>(&deme::DEMClumpTemplate::SetMOI)) - .def("SetMaterial", - static_cast>&)>( - &deme::DEMClumpTemplate::SetMaterial)) - .def("SetMaterial", - static_cast& input)>( - &deme::DEMClumpTemplate::SetMaterial)) - .def("SetVolume", &deme::DEMClumpTemplate::SetVolume) - .def("ReadComponentFromFile", &deme::DEMClumpTemplate::ReadComponentFromFile, - "Retrieve clump's sphere component information from a file", py::arg("filename"), py::arg("x_id") = "x", - py::arg("y_id") = "y", py::arg("z_id") = "z", py::arg("r_id") = "r") - .def("InformCentroidPrincipal", - static_cast&, const std::vector&)>( - &deme::DEMClumpTemplate::InformCentroidPrincipal)) - .def("Move", - static_cast&, const std::vector&)>( - &deme::DEMClumpTemplate::Move)) - .def("Scale", &deme::DEMClumpTemplate::Scale) - .def("AssignName", &deme::DEMClumpTemplate::AssignName); - - py::class_>(obj, "DEMClumpBatch") - .def(py::init()) - .def("GetNumClumps", &deme::DEMClumpBatch::GetNumClumps) - .def("GetNumSpheres", &deme::DEMClumpBatch::GetNumSpheres) - .def("SetTypes", - static_cast>&)>( - &deme::DEMClumpBatch::SetTypes)) - .def("SetTypes", static_cast&)>( - &deme::DEMClumpBatch::SetTypes)) - .def("SetType", &deme::DEMClumpBatch::SetType) - .def("SetVel", static_cast>&)>( - &deme::DEMClumpBatch::SetVel)) - .def("SetVel", - static_cast&)>(&deme::DEMClumpBatch::SetVel)) - // .def("SetAngVel", - // static_cast&)>(&deme::DEMClumpBatch::SetAngVel)) - .def("SetFamilies", static_cast&)>( - &deme::DEMClumpBatch::SetFamilies)) - .def("SetFamilies", static_cast(&deme::DEMClumpBatch::SetFamilies)) - .def("SetFamily", &deme::DEMClumpBatch::SetFamily) - .def("SetExistingContacts", &deme::DEMClumpBatch::SetExistingContacts) - .def("SetExistingContactWildcards", &deme::DEMClumpBatch::SetExistingContactWildcards) - .def("AddExistingContactWildcard", &deme::DEMClumpBatch::AddExistingContactWildcard) - .def("SetOwnerWildcards", &deme::DEMClumpBatch::SetOwnerWildcards) - .def("AddOwnerWildcard", - static_cast&)>( - &deme::DEMClumpBatch::AddOwnerWildcard)) - .def("AddOwnerWildcard", static_cast( - &deme::DEMClumpBatch::AddOwnerWildcard)) - .def("SetGeometryWildcards", &deme::DEMClumpBatch::SetGeometryWildcards) - .def("AddGeometryWildcard", - static_cast&)>( - &deme::DEMClumpBatch::AddGeometryWildcard)) - .def("AddGeometryWildcard", static_cast( - &deme::DEMClumpBatch::AddGeometryWildcard)) - .def("SetOriQ", - static_cast&)>(&deme::DEMClumpBatch::SetOriQ)) - .def("SetOriQ", static_cast(&deme::DEMClumpBatch::SetOriQ)) - .def("SetOriQ", - static_cast&)>(&deme::DEMClumpBatch::SetOriQ)) - .def("SetOriQ", static_cast>&)>( - &deme::DEMClumpBatch::SetOriQ)) - .def("GetNumContacts", &deme::DEMClumpBatch::GetNumContacts); - - py::class_>(obj, "DEMExternObj") - .def(py::init<>()) - .def("Mass", [](const deme::DEMExternObj& object) { return object.mass; }) - .def("MOI", [](const deme::DEMExternObj& object) { return object.MOI; }) - .def("SetFamily", &deme::DEMExternObj::SetFamily, "Defines an object contact family number") - .def("SetMass", &deme::DEMExternObj::SetMass, "Sets the mass of this object") - .def("SetMOI", - static_cast&)>(&deme::DEMExternObj::SetMOI), - "Sets the MOI (in the principal frame)") - .def("SetInitQuat", - static_cast&)>(&deme::DEMExternObj::SetInitQuat), - "Set the initial quaternion for this object (before simulation initializes).") - .def("SetInitPos", - static_cast&)>(&deme::DEMExternObj::SetInitPos), - "Set the initial position for this object (before simulation initializes).") - .def("AddPlane", - static_cast&, const std::vector&, - const std::shared_ptr&)>( - &deme::DEMExternObj::AddPlane), - "Add a plane with infinite size.") - //.def("AddPlate", static_cast&, const float, - const std::shared_ptr&, - const deme::objNormal_t)>(&deme::DEMExternObj::AddZCylinder), - "Add a z-axis-aligned cylinder of infinite length", py::arg("pos"), py::arg("rad"), py::arg("material"), - py::arg("normal") = deme::ENTITY_NORMAL_INWARD) - .def("AddCylinder", - static_cast&, const std::vector&, const float, - const std::shared_ptr&, - const deme::objNormal_t)>(&deme::DEMExternObj::AddCylinder), - "Add a cylinder of infinite length, which is along a user-specific axis", py::arg("pos"), py::arg("axis"), - py::arg("rad"), py::arg("material"), py::arg("normal") = deme::ENTITY_NORMAL_INWARD) - .def_readwrite("types", &deme::DEMExternObj::types) - .def_readwrite("materials", &deme::DEMExternObj::materials) - .def_readwrite("family_code", &deme::DEMExternObj::family_code) - .def_readwrite("init_pos", &deme::DEMExternObj::init_pos) - .def_readwrite("init_oriQ", &deme::DEMExternObj::init_oriQ) - .def_readwrite("mass", &deme::DEMExternObj::mass) - .def_readwrite("MOI", &deme::DEMExternObj::MOI) - .def_readwrite("load_order", &deme::DEMExternObj::load_order) - .def_readwrite("entity_params", &deme::DEMExternObj::entity_params); - - py::class_>( - obj, "DEMMeshConnected") - .def(py::init<>()) - .def(py::init()) - .def(py::init&>()) - .def("Mass", [](const deme::DEMMeshConnected& mesh) { return mesh.mass; }) - .def("MOI", [](const deme::DEMMeshConnected& mesh) { return mesh.MOI; }) - .def("Clear", &deme::DEMMeshConnected::Clear, "Clears everything from memory") - .def("LoadWavefrontMesh", &deme::DEMMeshConnected::LoadWavefrontMesh, - "Load a triangle mesh saved as a Wavefront .obj file", py::arg("input_file"), - py::arg("load_normals") = true, py::arg("load_uv") = false) - .def("WriteWavefront", &deme::DEMMeshConnected::WriteWavefront, - "Write the specified meshes in a Wavefront .obj file") - .def("GetNumTriangles", &deme::DEMMeshConnected::GetNumTriangles, - "Get the number of triangles already added to this mesh") - .def("GetNumNodes", &deme::DEMMeshConnected::GetNumNodes, "Get the number of nodes in the mesh") - .def("UseNormals", &deme::DEMMeshConnected::UseNormals, - "Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle " - "so that the normals derived from right-hand-rule are the same as the normals in the mesh file", - py::arg("use") = true) - .def("GetTriangle", &deme::DEMMeshConnected::GetTriangle, "Access the n-th triangle in mesh") - .def("SetMass", &deme::DEMMeshConnected::SetMass) - .def("SetMOI", - static_cast&)>(&deme::DEMMeshConnected::SetMOI)) - .def("SetFamily", &deme::DEMMeshConnected::SetFamily) - .def("SetMaterial", static_cast&)>( - &deme::DEMMeshConnected::SetMaterial)) - .def("SetMaterial", - static_cast>&)>( - &deme::DEMMeshConnected::SetMaterial)) - .def("SetInitQuat", static_cast&)>( - &deme::DEMMeshConnected::SetInitQuat)) - .def("SetInitPos", static_cast&)>( - &deme::DEMMeshConnected::SetInitPos)) - .def("InformCentroidPrincipal", - static_cast&, const std::vector&)>( - &deme::DEMMeshConnected::InformCentroidPrincipal)) - .def("Move", - static_cast&, const std::vector&)>( - &deme::DEMMeshConnected::Move)) - .def("Mirror", - static_cast&, const std::vector&)>( - &deme::DEMMeshConnected::Mirror)) - .def("Scale", static_cast(&deme::DEMMeshConnected::Scale)) - .def("Scale", - static_cast&)>(&deme::DEMMeshConnected::Scale)) - .def("ClearWildcards", &deme::DEMMeshConnected::ClearWildcards) - .def("SetGeometryWildcards", &deme::DEMMeshConnected::SetGeometryWildcards) - .def("AddGeometryWildcard", - static_cast&)>( - &deme::DEMMeshConnected::AddGeometryWildcard)) - .def("AddGeometryWildcard", static_cast( - &deme::DEMMeshConnected::AddGeometryWildcard)) - .def("GetCoordsVertices", &deme::DEMMeshConnected::GetCoordsVerticesAsVectorOfVectors) - //.def("GetCoordsUV", &deme::DEMMeshConnected::GetCoordsUVPython) - //.def("GetCoordsColors", &deme::DEMMeshConnected::GetCoordsColorsPython) - .def("GetIndicesVertexes", &deme::DEMMeshConnected::GetIndicesVertexesAsVectorOfVectors); - // .def("GetIndicesNormals", &deme::DEMMeshConnected::GetIndicesNormalsPython) - // .def("GetIndicesUV", &deme::DEMMeshConnected::GetIndicesUVPython) - // .def("GetIndicesColors", &deme::DEMMeshConnected::GetIndicesColorsPython); - - //// TODO: Insert readwrite functions to access all public class objects! - - py::enum_(obj, "VERBOSITY") - .value("QUIET", deme::VERBOSITY::QUIET) - .value("ERROR", deme::VERBOSITY::ERR) - .value("WARNING", deme::VERBOSITY::WARNING) - .value("INFO", deme::VERBOSITY::INFO) - .value("STEP_ANOMALY", deme::VERBOSITY::STEP_ANOMALY) - .value("STEP_METRIC", deme::VERBOSITY::STEP_METRIC) - .value("DEBUG", deme::VERBOSITY::DEBUG) - .value("STEP_DEBUG", deme::VERBOSITY::STEP_DEBUG) - .export_values(); - - py::enum_(obj, "TIME_INTEGRATOR") - .value("FORWARD_EULER", deme::TIME_INTEGRATOR::FORWARD_EULER) - .value("CENTERED_DIFFERENCE", deme::TIME_INTEGRATOR::CENTERED_DIFFERENCE) - .value("EXTENDED_TAYLOR", deme::TIME_INTEGRATOR::EXTENDED_TAYLOR) - .value("CHUNG", deme::TIME_INTEGRATOR::CHUNG) - .export_values(); - - py::enum_(obj, "OWNER_TYPE") - .value("CLUMP", deme::OWNER_TYPE::CLUMP) - .value("ANALYTICAL", deme::OWNER_TYPE::ANALYTICAL) - .value("MESH", deme::OWNER_TYPE::MESH) - .export_values(); - - py::enum_(obj, "FORCE_MODEL") - .value("HERTZIAN", deme::FORCE_MODEL::HERTZIAN) - .value("HERTZIAN_FRICTIONLES", deme::FORCE_MODEL::HERTZIAN_FRICTIONLESS) - .value("CUSTOM", deme::FORCE_MODEL::CUSTOM) - .export_values(); - - py::enum_(obj, "OUTPUT_CONTENT") - .value("XYZ", deme::OUTPUT_CONTENT::XYZ) - .value("QUAT", deme::OUTPUT_CONTENT::QUAT) - .value("ABSV", deme::OUTPUT_CONTENT::ABSV) - .value("VEL", deme::OUTPUT_CONTENT::VEL) - .value("ANG_VEL", deme::OUTPUT_CONTENT::ANG_VEL) - .value("ABS_ACC", deme::OUTPUT_CONTENT::ABS_ACC) - .value("ACC", deme::OUTPUT_CONTENT::ACC) - .value("ANG_ACC", deme::OUTPUT_CONTENT::ANG_ACC) - .value("FAMILY", deme::OUTPUT_CONTENT::FAMILY) - .value("MAT", deme::OUTPUT_CONTENT::MAT) - .value("OWNER_WILDCARD", deme::OUTPUT_CONTENT::OWNER_WILDCARD) - .value("GEO_WILDCARD", deme::OUTPUT_CONTENT::GEO_WILDCARD) - .value("EXP_FACTOR", deme::OUTPUT_CONTENT::EXP_FACTOR) - .export_values(); - - py::enum_(obj, "OUTPUT_FORMAT") - .value("CSV", deme::OUTPUT_FORMAT::CSV) - .value("BINARY", deme::OUTPUT_FORMAT::BINARY) - .value("CHPF", deme::OUTPUT_FORMAT::CHPF) - .export_values(); - - py::enum_(obj, "MESH_FORMAT") - .value("VTK", deme::MESH_FORMAT::VTK) - .value("OBJ", deme::MESH_FORMAT::OBJ) - .export_values(); - - py::enum_(obj, "SPATIAL_DIR") - .value("X", deme::SPATIAL_DIR::X) - .value("Y", deme::SPATIAL_DIR::Y) - .value("Z", deme::SPATIAL_DIR::Z) - .value("NONE", deme::SPATIAL_DIR::NONE) - .export_values(); - - py::enum_(obj, "CNT_OUTPUT_CONTENT") - .value("CNT_TYPE", deme::CNT_OUTPUT_CONTENT::CNT_TYPE) - .value("FORCE", deme::CNT_OUTPUT_CONTENT::FORCE) - .value("POINT", deme::CNT_OUTPUT_CONTENT::CNT_POINT) - .value("COMPONENT", deme::CNT_OUTPUT_CONTENT::COMPONENT) - .value("NORMAL", deme::CNT_OUTPUT_CONTENT::NORMAL) - .value("TORQUE", deme::CNT_OUTPUT_CONTENT::TORQUE) - .value("CNT_WILDCARD", deme::CNT_OUTPUT_CONTENT::CNT_WILDCARD) - .value("OWNER", deme::CNT_OUTPUT_CONTENT::OWNER) - .value("GEO_ID", deme::CNT_OUTPUT_CONTENT::GEO_ID) - .value("NICKNAME", deme::CNT_OUTPUT_CONTENT::NICKNAME) - .export_values(); -} diff --git a/src/DEM/Structs.h b/src/DEM/Structs.h index 22d540dc..739b24fe 100644 --- a/src/DEM/Structs.h +++ b/src/DEM/Structs.h @@ -8,18 +8,18 @@ #include "Defines.h" #include "../core/utils/CudaAllocator.hpp" -#include "../core/utils/ManagedMemory.hpp" #include "../core/utils/csv.hpp" -#include "../core/utils/GpuError.h" +#include "../core/utils/Logger.hpp" #include "../core/utils/DataMigrationHelper.hpp" #include "../core/utils/Timer.hpp" #include "../core/utils/RuntimeData.h" #include "../kernel/DEMHelperKernels.cuh" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" #include #include #include +#include #include #include #include @@ -76,24 +76,30 @@ const std::string OUTPUT_FILE_NORMAL_Z_NAME = std::string("n_z"); const std::string OUTPUT_FILE_SPH_SPH_CONTACT_NAME = std::string("SS"); const std::string OUTPUT_FILE_SPH_ANAL_CONTACT_NAME = std::string("SA"); const std::string OUTPUT_FILE_SPH_MESH_CONTACT_NAME = std::string("SM"); +const std::string OUTPUT_FILE_TRIANGLE_TRIANGLE_CONTACT_NAME = std::string("MM"); +const std::string OUTPUT_FILE_MESH_ANAL_CONTACT_NAME = std::string("MA"); const std::set CNT_FILE_KNOWN_COL_NAMES = { - OUTPUT_FILE_OWNER_1_NAME, OUTPUT_FILE_OWNER_2_NAME, OUTPUT_FILE_COMP_1_NAME, - OUTPUT_FILE_COMP_2_NAME, OUTPUT_FILE_GEO_ID_1_NAME, OUTPUT_FILE_GEO_ID_2_NAME, - OUTPUT_FILE_OWNER_NICKNAME_1_NAME, OUTPUT_FILE_OWNER_NICKNAME_2_NAME, OUTPUT_FILE_CNT_TYPE_NAME, - OUTPUT_FILE_FORCE_X_NAME, OUTPUT_FILE_FORCE_Y_NAME, OUTPUT_FILE_FORCE_Z_NAME, - OUTPUT_FILE_TORQUE_X_NAME, OUTPUT_FILE_TORQUE_Y_NAME, OUTPUT_FILE_TORQUE_Z_NAME, - OUTPUT_FILE_NORMAL_X_NAME, OUTPUT_FILE_NORMAL_Y_NAME, OUTPUT_FILE_NORMAL_Z_NAME, - OUTPUT_FILE_SPH_SPH_CONTACT_NAME, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME, OUTPUT_FILE_SPH_MESH_CONTACT_NAME}; + OUTPUT_FILE_OWNER_1_NAME, OUTPUT_FILE_OWNER_2_NAME, + OUTPUT_FILE_COMP_1_NAME, OUTPUT_FILE_COMP_2_NAME, + OUTPUT_FILE_GEO_ID_1_NAME, OUTPUT_FILE_GEO_ID_2_NAME, + OUTPUT_FILE_OWNER_NICKNAME_1_NAME, OUTPUT_FILE_OWNER_NICKNAME_2_NAME, + OUTPUT_FILE_CNT_TYPE_NAME, OUTPUT_FILE_FORCE_X_NAME, + OUTPUT_FILE_FORCE_Y_NAME, OUTPUT_FILE_FORCE_Z_NAME, + OUTPUT_FILE_TORQUE_X_NAME, OUTPUT_FILE_TORQUE_Y_NAME, + OUTPUT_FILE_TORQUE_Z_NAME, OUTPUT_FILE_NORMAL_X_NAME, + OUTPUT_FILE_NORMAL_Y_NAME, OUTPUT_FILE_NORMAL_Z_NAME, + OUTPUT_FILE_SPH_SPH_CONTACT_NAME, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME, + OUTPUT_FILE_SPH_MESH_CONTACT_NAME, OUTPUT_FILE_TRIANGLE_TRIANGLE_CONTACT_NAME, + OUTPUT_FILE_MESH_ANAL_CONTACT_NAME}; // Map contact type identifier to their names const std::unordered_map contact_type_out_name_map = { {NOT_A_CONTACT, "fake"}, {SPHERE_SPHERE_CONTACT, OUTPUT_FILE_SPH_SPH_CONTACT_NAME}, - {SPHERE_MESH_CONTACT, OUTPUT_FILE_SPH_MESH_CONTACT_NAME}, - {SPHERE_PLANE_CONTACT, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME}, - {SPHERE_PLATE_CONTACT, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME}, - {SPHERE_CYL_CONTACT, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME}, - {SPHERE_CONE_CONTACT, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME}}; + {SPHERE_TRIANGLE_CONTACT, OUTPUT_FILE_SPH_MESH_CONTACT_NAME}, + {SPHERE_ANALYTICAL_CONTACT, OUTPUT_FILE_SPH_ANAL_CONTACT_NAME}, + {TRIANGLE_TRIANGLE_CONTACT, OUTPUT_FILE_TRIANGLE_TRIANGLE_CONTACT_NAME}, + {TRIANGLE_ANALYTICAL_CONTACT, OUTPUT_FILE_MESH_ANAL_CONTACT_NAME}}; // Possible force model ingredients. This map is used to ensure we don't double-add them. const std::unordered_map force_kernel_ingredient_stats = {{"ts", false}, @@ -114,148 +120,6 @@ const std::unordered_map force_kernel_ingredient_stats = {{"t {"force", true}, {"torque_only_force", true}}; -// Structs defined here will be used by some host classes in DEM. -// NOTE: Data structs here need to be those complex ones (such as needing to include CudaAllocator.hpp), which may -// not be jitifiable. - -// DEMSolverScratchData mainly contains space allocated as system scratch pad and as thread temporary arrays -class DEMSolverScratchData { - private: - // NOTE! The type MUST be scratch_t, since all DEMSolverScratchData's allocation methods use num of bytes as - // arguments, but DeviceVectorPool's resize considers number of elements - DeviceVectorPool m_deviceVecPool; - DualArrayPool m_dualArrPool; - DualStructPool m_dualStructPool; - - public: - // Number of contacts in this CD step - DualStruct numContacts = DualStruct(0); - // Number of contacts in the previous CD step - DualStruct numPrevContacts = DualStruct(0); - // Number of spheres in the previous CD step (in case user added/removed clumps from the system) - DualStruct numPrevSpheres = DualStruct(0); - - DEMSolverScratchData(size_t* external_host_counter = nullptr, size_t* external_device_counter = nullptr) - : m_deviceVecPool(external_device_counter), m_dualArrPool(external_host_counter, external_device_counter) { - m_deviceVecPool.claim("ScratchSpace", 42); - } - ~DEMSolverScratchData() { releaseMemory(); } - - // Return raw pointer to swath of device memory that is at least "sizeNeeded" large - scratch_t* allocateScratchSpace(size_t sizeNeeded) { - m_deviceVecPool.resize("ScratchSpace", sizeNeeded); - return m_deviceVecPool.get("ScratchSpace"); - } - - // This flavor does not prevent you from forgeting to recycle before this time step ends - scratch_t* allocateVector(const std::string& name, size_t sizeNeeded) { - return m_deviceVecPool.claim(name, sizeNeeded, /*allow_duplicate=*/true); - } - - // This flavor prevents you from forgeting to recycle before this time step ends - scratch_t* allocateTempVector(const std::string& name, size_t sizeNeeded) { - return m_deviceVecPool.claim(name, sizeNeeded); - } - - // Dual arrays allocated here will always be temporary. If you need permanent dual array, create it as a member of - // your worker. - DualArray* allocateDualArray(const std::string& name, size_t sizeNeeded) { - return m_dualArrPool.claim(name, sizeNeeded); - } - scratch_t* getDualArrayHost(const std::string& name) { return m_dualArrPool.getHost(name); } - scratch_t* getDualArrayDevice(const std::string& name) { return m_dualArrPool.getDevice(name); } - void syncDualArrayDeviceToHost(const std::string& name) { m_dualArrPool.get(name)->toHost(); } - void syncDualArrayHostToDevice(const std::string& name) { m_dualArrPool.get(name)->toDevice(); } - // When using these methods, remember the type is scratch_t - void syncDualArrayDeviceToHost(const std::string& name, size_t start, size_t n) { - m_dualArrPool.get(name)->toHost(start, n); - } - void syncDualArrayHostToDevice(const std::string& name, size_t start, size_t n) { - m_dualArrPool.get(name)->toDevice(start, n); - } - // Likewise, all DualStruct allocated using this class will be temporary - DualStruct* allocateDualStruct(const std::string& name) { return m_dualStructPool.claim(name); } - size_t* getDualStructHost(const std::string& name) { return m_dualStructPool.getHost(name); } - size_t* getDualStructDevice(const std::string& name) { return m_dualStructPool.getDevice(name); } - void syncDualStructDeviceToHost(const std::string& name) { m_dualStructPool.get(name)->toHost(); } - void syncDualStructHostToDevice(const std::string& name) { m_dualStructPool.get(name)->toDevice(); } - - void finishUsingTempVector(const std::string& name) { m_deviceVecPool.unclaim(name); } - void finishUsingVector(const std::string& name) { finishUsingTempVector(name); } - void finishUsingDualArray(const std::string& name) { m_dualArrPool.unclaim(name); } - void finishUsingDualStruct(const std::string& name) { m_dualStructPool.unclaim(name); } - - // Debug util - void printVectorUsage() const { - m_deviceVecPool.printStatus(); - m_dualArrPool.printStatus(); - m_dualStructPool.printStatus(); - } - - void releaseMemory() { - m_deviceVecPool.releaseAll(); - m_dualArrPool.releaseAll(); - m_dualStructPool.releaseAll(); - } -}; - -struct kTStateParams { - // The `top speed' of the change of bin size - float binTopChangeRate = 0.05; - // The `current speed' fo the change of bin size - float binCurrentChangeRate = 0.0; - // The `acceleration' of bin size change rate, (0, 1]: 1 means each time a change is applied, it's at top speed - float binChangeRateAcc = 0.1; - // Number of CD steps before the solver makes a decision on how to change the bin size - unsigned int binChangeObserveSteps = 25; - // Past the point that (this number * error out bin geometry count)-many geometries found in a bin, the solver will - // force the bin to shrink - float binChangeUpperSafety = 0.25; - // Past the point that (this number * max num of bin)-many bins in the domain, the solver will force the bin to - // expand - float binChangeLowerSafety = 0.3; - - // The max num of geometries in a bin that appeared in the CD process - size_t maxSphFoundInBin; - size_t maxTriFoundInBin; - - // Num of bins, currently - size_t numBins = 0; - - // Current average num of contacts per sphere has. - float avgCntsPerSphere = 0.; - - // float maxVel_buffer; // buffer for the current max vel sent by dT - DualStruct maxVel = DualStruct(0.f); // kT's own storage of max vel - DualStruct ts_buffer; // buffer for the current ts size sent by dT - DualStruct ts; // kT's own storage of ts size - DualStruct maxDrift_buffer; // buffer for max dT future drift steps - DualStruct maxDrift; // kT's own storage for max future drift -}; - -struct dTStateParams {}; - -inline std::string pretty_format_bytes(size_t bytes) { - // set up byte prefixes - constexpr size_t KIBI = 1024; - constexpr size_t MEBI = KIBI * KIBI; - constexpr size_t GIBI = KIBI * KIBI * KIBI; - float gibival = float(bytes) / GIBI; - float mebival = float(bytes) / MEBI; - float kibival = float(bytes) / KIBI; - std::stringstream ret; - if (gibival > 1) { - ret << gibival << " GiB"; - } else if (mebival > 1) { - ret << mebival << " MiB"; - } else if (kibival > 1) { - ret << kibival << " KiB"; - } else { - ret << bytes << " B"; - } - return ret.str(); -} - // ============================================================================= // SOME HOST-SIDE ENUMS // ============================================================================= @@ -265,117 +129,169 @@ enum class INSPECT_ENTITY_TYPE { SPHERE, CLUMP, MESH, MESH_FACET, EVERYTHING }; // Which reduce operation is needed in an inspection enum class CUB_REDUCE_FLAVOR { NONE, MAX, MIN, SUM }; // Format of the output files -enum class OUTPUT_FORMAT { CSV, BINARY, CHPF }; +enum class OUTPUT_FORMAT { CSV, BINARY, VTK }; // Mesh output format -enum class MESH_FORMAT { VTK, OBJ }; +enum class MESH_FORMAT { VTK, OBJ, STL, PLY }; // Adaptive time step size methods -enum class ADAPT_TS_TYPE { NONE, MAX_VEL, INT_DIFF }; +enum class ADAPT_TS_TYPE { NONE, HERTZ_CONST, MAX_VEL, INT_DIFF }; // ============================================================================= // NOW DEFINING MACRO COMMANDS USED BY THE DEM MODULE // ============================================================================= -#define DEME_PRINTF(...) \ - { \ - if (verbosity > VERBOSITY::QUIET) { \ - printf(__VA_ARGS__); \ - } \ - } - -#define DEME_ERROR(...) \ - { \ - char error_message[1024]; \ - sprintf(error_message, __VA_ARGS__); \ - std::string out = error_message; \ - out += "\n"; \ - out += "This happened in "; \ - out += __func__; \ - out += ".\n"; \ - throw std::runtime_error(out); \ +#define DEME_PRINTF(...) \ + { \ + if (verbosity > VERBOSITY_QUIET) { \ + printf(__VA_ARGS__); \ + } \ } -#define DEME_WARNING(...) \ - { \ - if (verbosity >= VERBOSITY::WARNING) { \ - char warn_message[1024]; \ - sprintf(warn_message, __VA_ARGS__); \ - std::string out = "\nWARNING! "; \ - out += warn_message; \ - out += "\n\n"; \ - std::cerr << out; \ - } \ - } +// DEME_ERROR, DEME_WARNING, and DEME_INFO are now defined in Logger.hpp -#define DEME_INFO(...) \ +#define DEME_DEBUG_PRINTF(...) \ { \ - if (verbosity >= VERBOSITY::INFO) { \ + if (verbosity >= VERBOSITY_DEBUG) { \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } -#define DEME_STEP_ANOMALY(...) \ - { \ - if (verbosity >= VERBOSITY::STEP_ANOMALY) { \ - char warn_message[1024]; \ - sprintf(warn_message, __VA_ARGS__); \ - std::string out = "\n-------- SIM ANOMALY!!! --------\n"; \ - out += warn_message; \ - out += "\n\n"; \ - std::cerr << out; \ - } \ +#define DEME_DEBUG_EXEC(...) \ + { \ + if (verbosity >= VERBOSITY_DEBUG) { \ + __VA_ARGS__; \ + } \ } -#define DEME_STEP_METRIC(...) \ - { \ - if (verbosity >= VERBOSITY::STEP_METRIC) { \ - printf(__VA_ARGS__); \ - printf("\n"); \ - } \ - } +// ============================================================================= +// NOW SOME HOST-SIDE SIMPLE STRUCTS USED BY THE DEM MODULE +// ============================================================================= -#define DEME_DEBUG_PRINTF(...) \ - { \ - if (verbosity >= VERBOSITY::DEBUG) { \ - printf(__VA_ARGS__); \ - printf("\n"); \ - } \ - } +// Simple CUDA event-backed span timer, used for GPU section timing without forcing host synchronizations. +struct StreamEventTimerSpan { + static constexpr int kQueueDepth = 8; -#define DEME_DEBUG_EXEC(...) \ - { \ - if (verbosity >= VERBOSITY::DEBUG) { \ - __VA_ARGS__; \ - } \ - } + struct Slot { + cudaEvent_t start = nullptr; + cudaEvent_t stop = nullptr; + bool started = false; + bool stopped = false; + }; -#define DEME_STEP_DEBUG_PRINTF(...) \ - { \ - if (verbosity >= VERBOSITY::STEP_DEBUG) { \ - printf(__VA_ARGS__); \ - printf("\n"); \ - } \ + std::array slots{}; + int head = 0; + int count = 0; + int active = -1; + + void create(unsigned int flags = cudaEventDefault) { + for (auto& slot : slots) { + DEME_GPU_CALL(cudaEventCreateWithFlags(&slot.start, flags)); + DEME_GPU_CALL(cudaEventCreateWithFlags(&slot.stop, flags)); + slot.started = false; + slot.stopped = false; + } + head = 0; + count = 0; + active = -1; } -#define DEME_STEP_DEBUG_EXEC(...) \ - { \ - if (verbosity >= VERBOSITY::STEP_DEBUG) { \ - __VA_ARGS__; \ - } \ + void destroy() { + for (auto& slot : slots) { + if (slot.start) { + cudaEventDestroy(slot.start); + slot.start = nullptr; + } + if (slot.stop) { + cudaEventDestroy(slot.stop); + slot.stop = nullptr; + } + slot.started = false; + slot.stopped = false; + } + head = 0; + count = 0; + active = -1; } -// ============================================================================= -// NOW SOME HOST-SIDE SIMPLE STRUCTS USED BY THE DEM MODULE -// ============================================================================= + void reset() { + for (auto& slot : slots) { + slot.started = false; + slot.stopped = false; + } + head = 0; + count = 0; + active = -1; + } -// Anomalies log -class WorkerAnomalies { - public: - WorkerAnomalies() {} + bool hasFreeSlot() const { return count < kQueueDepth; } - bool over_max_vel = false; + bool begin(cudaStream_t stream, Timer& timer, bool allow_sync) { + if (active != -1) { + return false; + } + if (!hasFreeSlot()) { + accumulateAll(timer, allow_sync); + } + if (!hasFreeSlot()) { + return false; + } + const int idx = (head + count) % kQueueDepth; + Slot& slot = slots[idx]; + DEME_GPU_CALL(cudaEventRecord(slot.start, stream)); + slot.started = true; + slot.stopped = false; + active = idx; + count++; + return true; + } + + void end(cudaStream_t stream) { + if (active < 0) { + return; + } + Slot& slot = slots[active]; + if (!slot.started) { + return; + } + DEME_GPU_CALL(cudaEventRecord(slot.stop, stream)); + slot.stopped = true; + active = -1; + } - void Clear() { over_max_vel = false; } + bool accumulate(Timer& timer, bool allow_sync) { + if (count == 0) { + return false; + } + Slot& slot = slots[head]; + if (!slot.started || !slot.stopped) { + return false; + } + // Non-blocking elapsed-time queries can return cudaErrorNotReady if the stop event has not completed yet. + cudaError_t q = cudaEventQuery(slot.stop); + if (q == cudaErrorNotReady) { + if (!allow_sync) { + (void)cudaGetLastError(); + return false; + } + (void)cudaGetLastError(); + DEME_GPU_CALL(cudaEventSynchronize(slot.stop)); + } else { + DEME_GPU_CALL(q); + } + float milliseconds = 0.f; + DEME_GPU_CALL(cudaEventElapsedTime(&milliseconds, slot.start, slot.stop)); + timer.addDuration(milliseconds / 1000.0); + slot.started = false; + slot.stopped = false; + head = (head + 1) % kQueueDepth; + count--; + return true; + } + + void accumulateAll(Timer& timer, bool allow_sync) { + while (accumulate(timer, allow_sync)) { + } + } }; // Timers used by kT and dT @@ -383,14 +299,107 @@ class SolverTimers { private: const unsigned int num_timers; std::unordered_map> m_timers; + std::unordered_map m_gpu_timers; + bool gpu_timers_initialized = false; + bool defer_gpu_timer_accumulation = true; public: SolverTimers(const std::vector& names) : num_timers(names.size()) { for (unsigned int i = 0; i < num_timers; i++) { m_timers[names.at(i)] = Timer(); + m_gpu_timers[names.at(i)] = StreamEventTimerSpan(); + } + } + + void InitGpuEvents(unsigned int flags = cudaEventDefault) { + if (gpu_timers_initialized) { + return; } + for (auto& [name, span] : m_gpu_timers) { + span.create(flags); + } + gpu_timers_initialized = true; + } + + void EnableGpuTimers(unsigned int flags = cudaEventDefault) { InitGpuEvents(flags); } + void SetDeferGpuTimerAccumulation(bool defer) { defer_gpu_timer_accumulation = defer; } + bool GetDeferGpuTimerAccumulation() const { return defer_gpu_timer_accumulation; } + + void DestroyGpuEvents() { + if (!gpu_timers_initialized) { + return; + } + for (auto& [name, span] : m_gpu_timers) { + span.destroy(); + } + gpu_timers_initialized = false; } + Timer& GetTimer(const std::string& name) { return m_timers.at(name); } + + void StartGpuTimer(const std::string& name, cudaStream_t stream) { + if (!gpu_timers_initialized) { + return; + } + auto it = m_gpu_timers.find(name); + if (it != m_gpu_timers.end()) { + it->second.begin(stream, m_timers.at(name), !defer_gpu_timer_accumulation); + } + } + + void StopGpuTimer(const std::string& name, cudaStream_t stream) { + if (!gpu_timers_initialized) { + return; + } + auto it = m_gpu_timers.find(name); + if (it != m_gpu_timers.end()) { + it->second.end(stream); + } + } + + void AccumulateGpuTimer(const std::string& name) { + if (!gpu_timers_initialized) { + return; + } + auto it = m_gpu_timers.find(name); + if (it != m_gpu_timers.end()) { + it->second.accumulateAll(m_timers.at(name), !defer_gpu_timer_accumulation); + } + } + + void AccumulateGpuTimers(const std::vector& names) { + for (const auto& name : names) { + AccumulateGpuTimer(name); + } + } + + void FlushGpuTimers() { + if (!gpu_timers_initialized) { + return; + } + for (auto& [name, span] : m_gpu_timers) { + span.accumulateAll(m_timers.at(name), true); + } + } + + void ResetGpuTimer(const std::string& name) { + if (!gpu_timers_initialized) { + return; + } + auto it = m_gpu_timers.find(name); + if (it != m_gpu_timers.end()) { + it->second.reset(); + } + } + + void ResetGpuTimers() { + if (!gpu_timers_initialized) { + return; + } + for (auto& [name, span] : m_gpu_timers) { + span.reset(); + } + } }; // Manager of the collabortation between the main thread and worker threads @@ -480,8 +489,9 @@ class ClumpTemplateFlatten { }; struct SolverFlags { - // Sort contact pair arrays (based on contact type) before sending to dT - bool should_sort_pairs = true; + // Sort contact pair arrays (based on contact type) before sending to dT. + // NOTE: The current logic requires this to be true. We cannot set it to false now. + const bool should_sort_pairs = true; // This run is historyless bool isHistoryless = false; // This run uses contact detection in an async fashion (kT and dT working at different points in simulation time) @@ -493,19 +503,17 @@ struct SolverFlags { // Some output-related flags unsigned int outputFlags = OUTPUT_CONTENT::QUAT | OUTPUT_CONTENT::ABSV; unsigned int cntOutFlags; + unsigned int meshOutFlags = static_cast(MESH_OUTPUT_CONTENT::XYZ); // Time step constant-ness and expand factor constant-ness bool isStepConst = true; bool isExpandFactorFixed = false; // The strategy for selecting the variable time step size VAR_TS_STRAT stepSizeStrat = VAR_TS_STRAT::DEME_CONST; - // Whether instructed to use jitification for mass properties and clump components (default to no and it is - // recommended) - bool useClumpJitify = false; - bool useMassJitify = false; - // Whether the simulation involves meshes - bool hasMeshes = false; - // Whether the force collection (acceleration calc and reduction) process should be using CUB - bool useCubForceCollect = false; + // Whether instructed to use jitification for mass properties and clump components + bool useClumpJitify = true; + bool useMassJitify = true; + // Whether meshes can have contacts with entities other than spheres + bool meshUniversalContact = false; // Does not record contact forces, contact point etc. bool useNoContactRecord = false; // Collect force (reduce to acc) right in the force calculation kernel @@ -521,13 +529,20 @@ struct SolverFlags { bool autoBinSize = true; bool autoUpdateFreq = true; - // The max number of average contacts per sphere has before the solver errors out. The reason why I didn't use the - // number of contacts for the sphere that has the most is that, well, we can have a huge sphere and it just will - // have more contacts. But if avg cnt is high, that means probably the contact margin is out of control now. - float errOutAvgSphCnts = 100.; + // The max average primitive contacts before the solver errors out. We check primitive average rather than the + // worst primitive so one legitimately large sphere/triangle patch does not dominate the error policy. A sustained + // high average usually means the contact margin or dynamics are out of control. + float errOutAvgPrimitiveCnts = 300.; // Whether there are contacts that can never be removed. bool hasPersistentContacts = false; + // Whether to use the simple patch ID-based triangle combination. This is the default safe path because it preserves + // dT's expected per-contact-type, patch-pair sorted contact-history mapping invariant. Set false only to opt into + // the connected-component flooding route. + bool useSimplePatchCombination = true; + // Whether the opt-in flooding route remaps raw island labels to stable IDs across contact detection steps. This + // preserves contact history when the raw representative triangle changes. + bool useStablePatchIslandIDs = true; }; class DEMMaterial { @@ -557,21 +572,158 @@ class DEMTriangle { float3 p3; }; +// Structs defined here will be used by some host classes in DEM. +// NOTE: Data structs here need to be those complex ones (such as needing to include CudaAllocator.hpp), which may +// not be jitifiable. + +// DEMSolverScratchData mainly contains space allocated as system scratch pad and as thread temporary arrays +class DEMSolverScratchData { + private: + // NOTE! The type MUST be scratch_t, since all DEMSolverScratchData's allocation methods use num of bytes as + // arguments, but DeviceVectorPool's resize considers number of elements + DeviceVectorPool m_deviceVecPool; + DualArrayPool m_dualArrPool; + DualStructPool m_dualStructPool; + + public: + // Number of contacts (in terms of convex patches) in this CD step + DualStruct numContacts = DualStruct(0); + // Number of contacts in the previous CD step + DualStruct numPrevContacts = DualStruct(0); + // Number of contacts between the primitives (spheres/triangles) in this CD step + DualStruct numPrimitiveContacts = DualStruct(0); + // Number of previous step's primitive contacts + DualStruct numPrevPrimitiveContacts = DualStruct(0); + // Number of spheres in the previous CD step (in case user added/removed clumps from the system) + DualStruct numPrevSpheres = DualStruct(0); + // Prev number of triangles + DualStruct numPrevTriangles = DualStruct(0); + // Number of previous step's mesh patches + DualStruct numPrevMeshPatches = DualStruct(0); + + DEMSolverScratchData(size_t* external_host_counter = nullptr, size_t* external_device_counter = nullptr) + : m_deviceVecPool(external_device_counter), m_dualArrPool(external_host_counter, external_device_counter) { + m_deviceVecPool.claim("ScratchSpace", 42); + } + ~DEMSolverScratchData() { releaseMemory(); } + + // Return raw pointer to swath of device memory that is at least "sizeNeeded" large + scratch_t* allocateScratchSpace(size_t sizeNeeded) { + m_deviceVecPool.resize("ScratchSpace", sizeNeeded); + return m_deviceVecPool.get("ScratchSpace"); + } + + // This flavor does not prevent you from forgeting to recycle before this time step ends + scratch_t* allocateVector(const std::string& name, size_t sizeNeeded) { + return m_deviceVecPool.claim(name, sizeNeeded, /*allow_duplicate=*/true); + } + + // This flavor prevents you from forgeting to recycle before this time step ends + scratch_t* allocateTempVector(const std::string& name, size_t sizeNeeded) { + return m_deviceVecPool.claim(name, sizeNeeded); + } + + // Dual arrays allocated here will always be temporary. If you need permanent dual array, create it as a member of + // your worker. + DualArray* allocateDualArray(const std::string& name, size_t sizeNeeded) { + return m_dualArrPool.claim(name, sizeNeeded); + } + scratch_t* getDualArrayHost(const std::string& name) { return m_dualArrPool.getHost(name); } + scratch_t* getDualArrayDevice(const std::string& name) { return m_dualArrPool.getDevice(name); } + void syncDualArrayDeviceToHost(const std::string& name) { m_dualArrPool.get(name)->toHost(); } + void syncDualArrayHostToDevice(const std::string& name) { m_dualArrPool.get(name)->toDevice(); } + // When using these methods, remember the type is scratch_t + void syncDualArrayDeviceToHost(const std::string& name, size_t start, size_t n) { + m_dualArrPool.get(name)->toHost(start, n); + } + void syncDualArrayHostToDevice(const std::string& name, size_t start, size_t n) { + m_dualArrPool.get(name)->toDevice(start, n); + } + // Likewise, all DualStruct allocated using this class will be temporary + DualStruct* allocateDualStruct(const std::string& name) { return m_dualStructPool.claim(name); } + size_t* getDualStructHost(const std::string& name) { return m_dualStructPool.getHost(name); } + size_t* getDualStructDevice(const std::string& name) { return m_dualStructPool.getDevice(name); } + void syncDualStructDeviceToHost(const std::string& name) { m_dualStructPool.get(name)->toHost(); } + void syncDualStructHostToDevice(const std::string& name) { m_dualStructPool.get(name)->toDevice(); } + + void finishUsingTempVector(const std::string& name) { m_deviceVecPool.unclaim(name); } + void finishUsingVector(const std::string& name) { finishUsingTempVector(name); } + void finishUsingDualArray(const std::string& name) { m_dualArrPool.unclaim(name); } + void finishUsingDualStruct(const std::string& name) { m_dualStructPool.unclaim(name); } + + bool existDualArray(const std::string& name) { return m_dualArrPool.exist(name); } + bool existDualStruct(const std::string& name) { return m_dualStructPool.exist(name); } + bool existTempVector(const std::string& name) { return m_deviceVecPool.exist(name); } + + // Debug util + void printVectorUsage() const { + m_deviceVecPool.printStatus(); + m_dualArrPool.printStatus(); + m_dualStructPool.printStatus(); + } + + void releaseMemory() { + m_deviceVecPool.releaseAll(); + m_dualArrPool.releaseAll(); + m_dualStructPool.releaseAll(); + } +}; + +struct kTStateParams { + // The `top speed' of the change of bin size + float binTopChangeRate = 0.05; + // The `current speed' fo the change of bin size + float binCurrentChangeRate = 0.0; + // The `acceleration' of bin size change rate, (0, 1]: 1 means each time a change is applied, it's at top speed + float binChangeRateAcc = 0.1; + // Number of CD steps before the solver makes a decision on how to change the bin size + unsigned int binChangeObserveSteps = 25; + // Past the point that (this number * error out bin geometry count)-many geometries found in a bin, the solver will + // force the bin to shrink + float binChangeUpperSafety = 0.25; + // Past the point that (this number * max num of bin)-many bins in the domain, the solver will force the bin to + // expand + float binChangeLowerSafety = 0.3; + + // The max num of geometries in a bin that appeared in the CD process + size_t maxSphFoundInBin; + size_t maxTriFoundInBin; + + // Num of bins, currently + size_t numBins = 0; + + // Current average number of contacts per primitive. + float avgCntsPerPrimitive = 0.; + // Consecutive contact-detection steps above errOutAvgPrimitiveCnts. + unsigned int avgCntsOverLimitStreak = 0; + // Peak average contacts observed during the current over-limit streak. + float avgCntsOverLimitPeak = 0.f; + + // float maxVel_buffer; // buffer for the current max vel sent by dT + DualStruct maxVel = DualStruct(0.f); // kT's own storage of max vel + DualStruct maxAngVel = DualStruct(0.f); // kT's own storage of max ang vel + DualStruct ts_buffer; // buffer for the current ts size sent by dT + DualStruct ts; // kT's own storage of ts size + DualStruct maxDrift_buffer; // buffer for max dT future drift steps + DualStruct maxDrift; // kT's own storage for max future drift +}; + // A struct that defines a `clump' (one of the core concepts of this solver). A clump is typically small which consists // of several sphere components, but it can be as large as having thousands of spheres. class DEMClumpTemplate { private: void assertLength(size_t len, const std::string name) { if (nComp == 0) { - std::cerr << "The settings at the " << name - << " call were applied to 0 sphere components.\nPlease consider using " << name - << " only after loading the clump template." << std::endl; + DEME_WARNING( + "The settings at the %s call were applied to 0 sphere components.\nPlease consider using %s " + "only after loading the clump template.", + name.c_str(), name.c_str()); } if (len != nComp) { - std::stringstream ss; - ss << name << " input argument must have length " << nComp << " (not " << len - << "), same as the number of sphere components in the clump template." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s input argument must have length %zu (not %zu), same as the number of sphere components in " + "the clump template.", + name.c_str(), nComp, len); } } @@ -585,6 +737,8 @@ class DEMClumpTemplate { /// Set mass. void SetMass(float mass) { this->mass = mass; } + /// Get mass. + float GetMass() const { return mass; } /// Set MOI (in principal frame). void SetMOI(float3 MOI) { this->MOI = MOI; } /// Set MOI (in principal frame). @@ -592,6 +746,8 @@ class DEMClumpTemplate { assertThreeElements(MOI, "SetMOI", "MOI"); SetMOI(make_float3(MOI[0], MOI[1], MOI[2])); } + /// Get MOI (in principal frame). + float3 GetMOI() const { return MOI; } /// Set material types for the mesh. Technically, you can set that for each individual mesh facet. void SetMaterial(const std::vector>& input) { @@ -701,6 +857,7 @@ class DEMClumpTemplate { // small, and is mainly there for the purpose of pyDEME entry point. class DEMInitializer { public: + virtual ~DEMInitializer() = default; // The type of a clump batch is CLUMP (it is used by tracker objs) OWNER_TYPE obj_type; // Its offset when this obj got loaded into the API-level user raw-input array @@ -713,10 +870,10 @@ class DEMClumpBatch : public DEMInitializer { size_t nExistContacts = 0; void assertLength(size_t len, const std::string name) { if (len != nClumps) { - std::stringstream ss; - ss << name << " input argument must have length " << nClumps << " (not " << len - << "), same as the number of clumps you originally added via AddClumps." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "%s input argument must have length %zu (not %zu), same as the number of clumps you originally " + "added via AddClumps.", + name.c_str(), nClumps, len); } } @@ -837,17 +994,15 @@ class DEMClumpBatch : public DEMInitializer { SetOriQ(Q); } - /// Specify the `family' code for each clump. Then you can specify if they should go with some prescribed motion or + /// Specify the "family" code for each clump. Then you can specify if they should use some prescribed motion or /// some special physics (for example, being fixed). The default behavior (without specification) for every family - /// is using `normal' physics. + /// uses "normal" physics. void SetFamilies(const std::vector& input) { assertLength(input.size(), "SetFamilies"); if (any_of(input.begin(), input.end(), [](unsigned int i) { return i > std::numeric_limits::max(); })) { - std::stringstream ss; - ss << "Some clumps are instructed to have a family number larger than the max allowance " - << std::numeric_limits::max() << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR("Some clumps are instructed to have a family number larger than the max allowance %u", + std::numeric_limits::max()); } families = input; family_isSpecified = true; @@ -860,44 +1015,39 @@ class DEMClumpBatch : public DEMInitializer { } void SetExistingContactWildcards(const std::unordered_map>& wildcards) { if (wildcards.begin()->second.size() != nExistContacts) { - std::stringstream ss; - ss << "SetExistingContactWildcards needs to be called after SetExistingContacts, with each wildcard array " - "having the same length as the number of contact pairs.\nThis way, each wildcard will have an " - "associated contact pair." - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR(std::string( + "SetExistingContactWildcards needs to be called after SetExistingContacts, with each wildcard " + "array having the same length as the number of contact pairs.\nThis way, each wildcard will have " + "an associated contact pair.")); } contact_wildcards = wildcards; } void AddExistingContactWildcard(const std::string& name, const std::vector& vals) { if (vals.size() != nClumps) { - std::stringstream ss; - ss << "AddExistingContactWildcard needs to be called after SetExistingContacts, with the input wildcard " - "array having the same length as the number of contact pairs.\nThis way, each wildcard will have an " - "associated contact pair." - << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR(std::string( + "AddExistingContactWildcard needs to be called after SetExistingContacts, with the input " + "wildcard array having the same length as the number of contact pairs.\nThis way, each wildcard " + "will have an associated contact pair.")); } contact_wildcards[name] = vals; } void SetOwnerWildcards(const std::unordered_map>& wildcards) { if (wildcards.begin()->second.size() != nClumps) { - std::stringstream ss; - ss << "Input owner wildcard arrays in a SetOwnerWildcards call must all have the same size as the number " - "of clumps in this batch.\nHere, the input array has length " - << wildcards.begin()->second.size() << " but this batch has " << nClumps << " clumps." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input owner wildcard arrays in a SetOwnerWildcards call must all have the same size as the " + "number of clumps in this batch.\nHere, the input array has length %zu but this batch has %zu " + "clumps.", + wildcards.begin()->second.size(), nClumps); } owner_wildcards = wildcards; } void AddOwnerWildcard(const std::string& name, const std::vector& vals) { if (vals.size() != nClumps) { - std::stringstream ss; - ss << "Input owner wildcard array in a AddOwnerWildcard call must have the same size as the number of " - "clumps in this batch.\nHere, the input array has length " - << vals.size() << " but this batch has " << nClumps << " clumps." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input owner wildcard array in a AddOwnerWildcard call must have the same size as the number of " + "clumps in this batch.\nHere, the input array has length %zu but this batch has %zu clumps.", + vals.size(), nClumps); } owner_wildcards[name] = vals; } @@ -907,21 +1057,21 @@ class DEMClumpBatch : public DEMInitializer { void SetGeometryWildcards(const std::unordered_map>& wildcards) { if (wildcards.begin()->second.size() != nSpheres) { - std::stringstream ss; - ss << "Input gemometry wildcard arrays in a SetGeometryWildcards call must all have the same size as the " - "number of spheres in this batch.\nHere, the input array has length " - << wildcards.begin()->second.size() << " but this batch has " << nSpheres << " spheres." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input gemometry wildcard arrays in a SetGeometryWildcards call must all have the same size as " + "the number of spheres in this batch.\nHere, the input array has length %zu but this batch has " + "%zu spheres.", + wildcards.begin()->second.size(), nSpheres); } geo_wildcards = wildcards; } void AddGeometryWildcard(const std::string& name, const std::vector& vals) { if (vals.size() != nSpheres) { - std::stringstream ss; - ss << "Input gemometry wildcard array in a AddGeometryWildcard call must have the same size as the number " - "of spheres in this batch.\nHere, the input array has length " - << vals.size() << " but this batch has " << nSpheres << " spheres." << std::endl; - throw std::runtime_error(ss.str()); + DEME_ERROR( + "Input gemometry wildcard array in a AddGeometryWildcard call must have the same size as the " + "number of spheres in this batch.\nHere, the input array has length %zu but this batch has %zu " + "spheres.", + vals.size(), nSpheres); } geo_wildcards[name] = vals; } @@ -946,11 +1096,16 @@ class DEMTrackedObj : public DEMInitializer { // If this tracked object is broken b/c the owner it points to has been removed from the simulation system bool isBroken = false; // The offset for its first geometric compoent in the tracked objects. For example, if it is mesh, then this is the - // first triangle ID. + // first convex patch's ID. size_t geoID; // The number of geometric entities (sphere components, triangles or analytical components) the tracked objects // have. size_t nGeos; + // If non-zero, override the computed nSpanOwners with this value. Used when tracking CombinedInstances where the + // tracker must span all member owners, not just the first member's batch. + size_t nSpanOwnersOverride = 0; + // If non-zero, override the computed nGeos with this value. Used when tracking CombinedInstances. + size_t nGeosOverride = 0; }; // General-purpose data container that can hold any type of data, indexed by string keys. @@ -961,7 +1116,7 @@ class DataContainer { template void Insert(const std::string& key, std::vector vec) { if (data_.count(key)) - throw std::runtime_error("Key already exists: " + key); + DEME_ERROR("Key already exists: %s", key.c_str()); data_[key] = std::make_shared>(std::move(vec)); types_[key] = &typeid(T); } @@ -982,7 +1137,7 @@ class DataContainer { const std::type_info& type_of(const std::string& key) const { if (!Contains(key)) - throw std::runtime_error("Key not found: " + key); + DEME_ERROR("Key not found: %s", key.c_str()); return *types_.at(key); // dereference the pointer } @@ -1007,7 +1162,7 @@ class DataContainer { } size_t Size() const { if (data_.empty()) { - throw std::runtime_error("DataContainer is empty."); + DEME_ERROR(std::string("DataContainer is empty.")); } return data_.begin()->second->Size(); } @@ -1028,9 +1183,7 @@ class DataContainer { std::size_t Size() const override { return data.size(); } }; - virtual void on_missing_key(const std::string& key) const { - throw std::runtime_error("Key not found: '" + key + "'"); - } + virtual void on_missing_key(const std::string& key) const { DEME_ERROR("Key not found: '%s'", key.c_str()); } template void check_type(const std::string& key) const { @@ -1038,7 +1191,7 @@ class DataContainer { on_missing_key(key); } if (*types_.at(key) != typeid(T)) { - throw std::runtime_error("Type mismatch for key: " + key); + DEME_ERROR("Type mismatch for key: %s", key.c_str()); } } @@ -1097,15 +1250,37 @@ class ContactInfoContainer : public DataContainer { protected: void on_missing_key(const std::string& key) const override { - throw std::runtime_error("ContactInfoContainer does not have field: '" + key + - "', you may need to turn on the output of this field by correctly calling " - "SetContactOutputContent before Initialize()."); + DEME_ERROR( + "ContactInfoContainer does not have field: '%s', you may need to turn on the output of this field " + "by correctly calling SetContactOutputContent before Initialize().", + key.c_str()); } private: unsigned int m_cnt_out_content; }; +// A map that inherents from std::unordered_map, which has pre-defined contact type keys, and can store type T data +template +class ContactTypeMap : public std::unordered_map { + public: + ContactTypeMap() { + this->emplace(SPHERE_SPHERE_CONTACT, T()); + this->emplace(SPHERE_TRIANGLE_CONTACT, T()); + this->emplace(SPHERE_ANALYTICAL_CONTACT, T()); + this->emplace(TRIANGLE_TRIANGLE_CONTACT, T()); + this->emplace(TRIANGLE_ANALYTICAL_CONTACT, T()); + } + ContactTypeMap(const T& default_value) : ContactTypeMap() { SetAll(default_value); } + + // Set values of all keys + void SetAll(const T& value) { + for (auto& [key, val] : *this) { + val = value; + } + } +}; + } // namespace deme #endif diff --git a/src/DEM/VariableTypes.h b/src/DEM/VariableTypes.h index 0570ca3d..6de2040e 100644 --- a/src/DEM/VariableTypes.h +++ b/src/DEM/VariableTypes.h @@ -16,12 +16,15 @@ typedef long long ssize_t; typedef long ssize_t; #endif -typedef uint16_t subVoxelPos_t; ///< uint16 or uint32 +// Most simulation entities' ID type +typedef uint32_t bodyID_t; +// The type used to concatenate two mesh patch IDs together (for voting contact pairs) +typedef uint64_t patchIDPair_t; +typedef uint16_t subVoxelPos_t; ///< uint16 or uint32 typedef uint64_t voxelID_t; typedef float oriQ_t; -typedef unsigned int bodyID_t; -typedef unsigned int binID_t; +typedef uint32_t binID_t; typedef uint8_t objID_t; typedef uint16_t materialsOffset_t; typedef uint16_t inertiaOffset_t; @@ -31,36 +34,39 @@ typedef double realFine_t; typedef char scratch_t; ///< Data type for DEM scratch-pad array (this should NOT be changed, MUST be 1-byte) // How many bin--sphere touch pairs can there be for one sphere, tops? This type should not need to be large. -typedef unsigned short int binsSphereTouches_t; +typedef uint16_t binsSphereTouches_t; // This type needs to be large enough to hold the result of a prefix scan of the type binsSphereTouches_t (and objID_t); // but normally, it should be the same magnitude as bodyID_t. -typedef unsigned int binSphereTouchPairs_t; +typedef uint32_t binSphereTouchPairs_t; // How many spheres a bin can touch, tops? We can assume it will not be too large to save GPU memory. -typedef unsigned short int spheresBinTouches_t; +typedef uint16_t spheresBinTouches_t; // How many contact pairs can there be in one bin? Sometimes, the geometry overlap is significant and there can be a // lot. -typedef unsigned int binContactPairs_t; +typedef uint32_t binContactPairs_t; // Need to be large enough to hold the number of total contact pairs. In general this number should be in the same // magnitude as bodyID_t. -typedef unsigned int contactPairs_t; -// How many other entities can a sphere touch, tops? It does not need to be large unless you have spheres that have -// magnitudes of difference in size, which you should preferrably avoid. -typedef unsigned short int geoSphereTouches_t; +typedef uint32_t contactPairs_t; +// How many other primitives can a primitive (sphere or triangle) touch, tops? It does not need to be large unless you +// have primitives that have magnitudes of difference in size, which you should preferrably avoid. +typedef uint16_t primitivesPrimTouches_t; // How many triangle--sphere touch pairs can there be for one sphere, tops? This type should not need to be large. -typedef unsigned int binsTriangleTouches_t; +typedef uint32_t binsTriangleTouches_t; // This type needs to be large enough to hold the result of a prefix scan of the type binsTriangleTouches_t (and // objID_t). -typedef unsigned int binsTriangleTouchPairs_t; +typedef uint32_t binsTriangleTouchPairs_t; // How many triangles a bin can touch, tops? We can assume it will not be too large to save GPU memory. -typedef unsigned short int trianglesBinTouches_t; +typedef uint16_t trianglesBinTouches_t; typedef uint8_t notStupidBool_t; ///< Ad-hoc bool (array) type -typedef uint8_t contact_t; ///< Contact type (sphere--sphere is 1, etc.) typedef uint8_t family_t; ///< Data type for clump presecription type -typedef uint8_t ownerType_t; ///< The type of a owner entity +typedef uint8_t ownerType_t; ///< The type of a owner entity (clump, analytical, mesh...) +typedef uint8_t geoType_t; ///< The type of a geometry entity (sphere, triangles, analytical component...) +typedef uint8_t contact_t; ///< Contact type as concatenation of geoType_t +typedef int16_t patchID_t; ///< Patch ID type for mesh patches, within a mesh (global mesh patch ID uses bodyID_t) -typedef uint8_t objType_t; +typedef uint8_t objType_t; ///< Analytical object type, such as plane, plate, cylinder, cone... Can be seen as even + ///< finer grain of geoType_t's analytical case typedef bool objNormal_t; } // namespace deme diff --git a/src/DEM/dT.cpp b/src/DEM/dT.cpp index 1fe9cad9..2154c82f 100644 --- a/src/DEM/dT.cpp +++ b/src/DEM/dT.cpp @@ -4,20 +4,17 @@ // SPDX-License-Identifier: BSD-3-Clause #include +#include #include #include #include -#include -#ifdef DEME_USE_CHPF - #include -#endif #include #include #include #include -#include -#include +#include +#include #include #include @@ -52,14 +49,28 @@ void DEMDynamicThread::packDataPointers() { alphaZ.bindDevicePointer(&(granData->alphaZ)); accSpecified.bindDevicePointer(&(granData->accSpecified)); angAccSpecified.bindDevicePointer(&(granData->angAccSpecified)); - idGeometryA.bindDevicePointer(&(granData->idGeometryA)); - idGeometryB.bindDevicePointer(&(granData->idGeometryB)); - contactType.bindDevicePointer(&(granData->contactType)); + idPrimitiveA.bindDevicePointer(&(granData->idPrimitiveA)); + idPrimitiveB.bindDevicePointer(&(granData->idPrimitiveB)); + contactTypePrimitive.bindDevicePointer(&(granData->contactTypePrimitive)); + geomToPatchMap.bindDevicePointer(&(granData->geomToPatchMap)); + + // NEW: Bind separate patch ID and mapping array pointers + idPatchA.bindDevicePointer(&(granData->idPatchA)); + idPatchB.bindDevicePointer(&(granData->idPatchB)); + contactTypePatch.bindDevicePointer(&(granData->contactTypePatch)); + contactPatchIsland.bindDevicePointer(&(granData->contactPatchIsland)); + familyMaskMatrix.bindDevicePointer(&(granData->familyMasks)); familyExtraMarginSize.bindDevicePointer(&(granData->familyExtraMarginSize)); + ownerCombinedMaster.bindDevicePointer(&(granData->ownerCombinedMaster)); + ownerCombinedRelPos.bindDevicePointer(&(granData->ownerCombinedRelPos)); + ownerCombinedRelOriQ.bindDevicePointer(&(granData->ownerCombinedRelOriQ)); + ownerCombinedMasterMass.bindDevicePointer(&(granData->ownerCombinedMasterMass)); + ownerCombinedMasterMOI.bindDevicePointer(&(granData->ownerCombinedMasterMOI)); contactForces.bindDevicePointer(&(granData->contactForces)); contactTorque_convToForce.bindDevicePointer(&(granData->contactTorque_convToForce)); + contactNormals.bindDevicePointer(&(granData->contactNormals)); contactPointGeometryA.bindDevicePointer(&(granData->contactPointGeometryA)); contactPointGeometryB.bindDevicePointer(&(granData->contactPointGeometryB)); // granData->contactHistory = contactHistory.data(); @@ -84,13 +95,25 @@ void DEMDynamicThread::packDataPointers() { sphereMaterialOffset.bindDevicePointer(&(granData->sphereMaterialOffset)); volumeOwnerBody.bindDevicePointer(&(granData->volumeOwnerBody)); - // Mesh-related - ownerMesh.bindDevicePointer(&(granData->ownerMesh)); + // Mesh and analytical-related + ownerTriMesh.bindDevicePointer(&(granData->ownerTriMesh)); + ownerPatchMesh.bindDevicePointer(&(granData->ownerPatchMesh)); + triPatchID.bindDevicePointer(&(granData->triPatchID)); ownerAnalBody.bindDevicePointer(&(granData->ownerAnalBody)); + ownerMeshConvex.bindDevicePointer(&(granData->ownerMeshConvex)); + ownerMeshNeverWinner.bindDevicePointer(&(granData->ownerMeshNeverWinner)); + ownerMeshWatertight.bindDevicePointer(&(granData->ownerMeshWatertight)); + ownerMeshShellHalfThickness.bindDevicePointer(&(granData->ownerMeshShellHalfThickness)); + triNeighborIndex.bindDevicePointer(&(granData->triNeighborIndex)); + triNeighbor1.bindDevicePointer(&(granData->triNeighbor1)); + triNeighbor2.bindDevicePointer(&(granData->triNeighbor2)); + triNeighbor3.bindDevicePointer(&(granData->triNeighbor3)); relPosNode1.bindDevicePointer(&(granData->relPosNode1)); relPosNode2.bindDevicePointer(&(granData->relPosNode2)); relPosNode3.bindDevicePointer(&(granData->relPosNode3)); - triMaterialOffset.bindDevicePointer(&(granData->triMaterialOffset)); + relPosPatch.bindDevicePointer(&(granData->relPosPatch)); + patchMaterialOffset.bindDevicePointer(&(granData->patchMaterialOffset)); + maxTriTriPenetration.bindDevicePointer(&(granData->maxTriTriPenetration)); // Template array pointers radiiSphere.bindDevicePointer(&(granData->radiiSphere)); @@ -129,13 +152,31 @@ void DEMDynamicThread::migrateDataToDevice() { alphaZ.toDeviceAsync(streamInfo.stream); accSpecified.toDeviceAsync(streamInfo.stream); angAccSpecified.toDeviceAsync(streamInfo.stream); - idGeometryA.toDeviceAsync(streamInfo.stream); - idGeometryB.toDeviceAsync(streamInfo.stream); - contactType.toDeviceAsync(streamInfo.stream); + + // Primitive contact info + idPrimitiveA.toDeviceAsync(streamInfo.stream); + idPrimitiveB.toDeviceAsync(streamInfo.stream); + contactTypePrimitive.toDeviceAsync(streamInfo.stream); + geomToPatchMap.toDeviceAsync(streamInfo.stream); + + // Separate patch contact info + contactTypePatch.toDeviceAsync(streamInfo.stream); + idPatchA.toDeviceAsync(streamInfo.stream); + idPatchB.toDeviceAsync(streamInfo.stream); + contactPatchIsland.toDeviceAsync(streamInfo.stream); + familyMaskMatrix.toDeviceAsync(streamInfo.stream); familyExtraMarginSize.toDeviceAsync(streamInfo.stream); + if (ownerCombinedMaster.size() > 0) { + ownerCombinedMaster.toDeviceAsync(streamInfo.stream); + ownerCombinedRelPos.toDeviceAsync(streamInfo.stream); + ownerCombinedRelOriQ.toDeviceAsync(streamInfo.stream); + ownerCombinedMasterMass.toDeviceAsync(streamInfo.stream); + ownerCombinedMasterMOI.toDeviceAsync(streamInfo.stream); + } contactForces.toDeviceAsync(streamInfo.stream); + contactNormals.toDeviceAsync(streamInfo.stream); contactTorque_convToForce.toDeviceAsync(streamInfo.stream); contactPointGeometryA.toDeviceAsync(streamInfo.stream); contactPointGeometryB.toDeviceAsync(streamInfo.stream); @@ -158,12 +199,28 @@ void DEMDynamicThread::migrateDataToDevice() { sphereMaterialOffset.toDeviceAsync(streamInfo.stream); volumeOwnerBody.toDeviceAsync(streamInfo.stream); - ownerMesh.toDeviceAsync(streamInfo.stream); + ownerTriMesh.toDeviceAsync(streamInfo.stream); + ownerPatchMesh.toDeviceAsync(streamInfo.stream); + triPatchID.toDeviceAsync(streamInfo.stream); ownerAnalBody.toDeviceAsync(streamInfo.stream); + ownerMeshConvex.toDeviceAsync(streamInfo.stream); + ownerMeshNeverWinner.toDeviceAsync(streamInfo.stream); + ownerMeshWatertight.toDeviceAsync(streamInfo.stream); + ownerMeshShellHalfThickness.toDeviceAsync(streamInfo.stream); + triNeighborIndex.toDeviceAsync(streamInfo.stream); + triNeighbor1.toDeviceAsync(streamInfo.stream); + triNeighbor2.toDeviceAsync(streamInfo.stream); + triNeighbor3.toDeviceAsync(streamInfo.stream); relPosNode1.toDeviceAsync(streamInfo.stream); relPosNode2.toDeviceAsync(streamInfo.stream); relPosNode3.toDeviceAsync(streamInfo.stream); - triMaterialOffset.toDeviceAsync(streamInfo.stream); + relPosPatch.toDeviceAsync(streamInfo.stream); + patchMaterialOffset.toDeviceAsync(streamInfo.stream); + triPVGlobalTriToLocal.toDeviceAsync(streamInfo.stream); + triPVStepP.toDeviceAsync(streamInfo.stream); + triPVStepPV.toDeviceAsync(streamInfo.stream); + triPVAccumP.toDeviceAsync(streamInfo.stream); + triPVAccumPV.toDeviceAsync(streamInfo.stream); radiiSphere.toDeviceAsync(streamInfo.stream); relPosSphereX.toDeviceAsync(streamInfo.stream); @@ -216,10 +273,21 @@ void DEMDynamicThread::migrateClumpPosInfoToHost() { } void DEMDynamicThread::migrateContactInfoToHost() { - idGeometryA.toHost(); - idGeometryB.toHost(); - contactType.toHost(); + // Primitive contact info + idPrimitiveA.toHost(); + idPrimitiveB.toHost(); + contactTypePrimitive.toHost(); + geomToPatchMap.toHost(); + + // Separate patch contact info + contactTypePatch.toHost(); + idPatchA.toHost(); + idPatchB.toHost(); + contactPatchIsland.toHost(); + + // Contact results contactForces.toHost(); + contactNormals.toHost(); contactTorque_convToForce.toHost(); contactPointGeometryA.toHost(); contactPointGeometryB.toHost(); @@ -255,17 +323,30 @@ void DEMDynamicThread::migrateAnalGeoWildcardToHost() { } } -bodyID_t DEMDynamicThread::getGeoOwnerID(const bodyID_t& geoB, const contact_t& type) const { +bodyID_t DEMDynamicThread::getGeoOwnerID(const bodyID_t& geo, const geoType_t& type) const { // These arrays can't change on device switch (type) { - case (NOT_A_CONTACT): + case (GEO_T_SPHERE): + return ownerClumpBody[geo]; + case (GEO_T_TRIANGLE): + return ownerTriMesh[geo]; + case (GEO_T_ANALYTICAL): + return ownerAnalBody[geo]; + default: + return NULL_BODYID; + } +} + +bodyID_t DEMDynamicThread::getPatchOwnerID(const bodyID_t& patchID, const geoType_t& type) const { + switch (type) { + case (GEO_T_TRIANGLE): + return ownerPatchMesh[patchID]; + case (GEO_T_SPHERE): + return ownerClumpBody[patchID]; + case (GEO_T_ANALYTICAL): + return ownerAnalBody[patchID]; + default: return NULL_BODYID; - case (SPHERE_SPHERE_CONTACT): - return ownerClumpBody[geoB]; - case (SPHERE_MESH_CONTACT): - return ownerMesh[geoB]; - default: // Default is sphere--analytical - return ownerAnalBody[geoB]; } } @@ -273,6 +354,7 @@ bodyID_t DEMDynamicThread::getGeoOwnerID(const bodyID_t& geoB, const contact_t& void DEMDynamicThread::packTransferPointers(DEMKinematicThread* kT) { // These are the pointers for sending data to dT granData->pKTOwnedBuffer_absVel = kT->absVel_buffer.data(); + granData->pKTOwnedBuffer_absAngVel = kT->absAngVel_buffer.data(); granData->pKTOwnedBuffer_voxelID = kT->voxelID_buffer.data(); granData->pKTOwnedBuffer_locX = kT->locX_buffer.data(); granData->pKTOwnedBuffer_locY = kT->locY_buffer.data(); @@ -289,6 +371,7 @@ void DEMDynamicThread::packTransferPointers(DEMKinematicThread* kT) { // Single-number data are now not packaged in granData... granData->pKTOwnedBuffer_ts = &(kT->stateParams.ts_buffer); granData->pKTOwnedBuffer_maxDrift = &(kT->stateParams.maxDrift_buffer); + granData->pKTOwnedBuffer_maxTriTriPenetration = kT->maxTriTriPenetration_buffer.data(); } void DEMDynamicThread::changeFamily(unsigned int ID_from, unsigned int ID_to) { @@ -318,8 +401,11 @@ void DEMDynamicThread::setSimParams(unsigned char nvXp2, double ts_size, float expand_factor, float approx_max_vel, + double max_tritri_penetration, + float tritri_contact_rejection_ratio, float expand_safety_param, float expand_safety_adder, + bool use_angvel_margin, const std::set& contact_wildcards, const std::set& owner_wildcards, const std::set& geo_wildcards) { @@ -328,18 +414,22 @@ void DEMDynamicThread::setSimParams(unsigned char nvXp2, simParams->nvZp2 = nvZp2; simParams->l = l; simParams->voxelSize = voxelSize; - simParams->binSize = binSize; + simParams->dyn.binSize = binSize; + simParams->dyn.inv_binSize = 1. / binSize; simParams->LBFX = LBFPoint.x; simParams->LBFY = LBFPoint.y; simParams->LBFZ = LBFPoint.z; simParams->Gx = G.x; simParams->Gy = G.y; simParams->Gz = G.z; - simParams->h = ts_size; - simParams->beta = expand_factor; // If beta is auto-adapting, this assignment has no effect - simParams->approxMaxVel = approx_max_vel; - simParams->expSafetyMulti = expand_safety_param; - simParams->expSafetyAdder = expand_safety_adder; + simParams->dyn.h = ts_size; + simParams->dyn.beta = expand_factor; // If beta is auto-adapting, this assignment has no effect + simParams->dyn.approxMaxVel = approx_max_vel; + simParams->dyn.expSafetyMulti = expand_safety_param; + simParams->dyn.expSafetyAdder = expand_safety_adder; + simParams->capTriTriPenetration = max_tritri_penetration; + simParams->triTriContactRejectionRatio = tritri_contact_rejection_ratio; + simParams->useAngVelMargin = use_angvel_margin ? 1 : 0; simParams->nbX = nbX; simParams->nbY = nbY; simParams->nbZ = nbZ; @@ -355,64 +445,14 @@ void DEMDynamicThread::setSimParams(unsigned char nvXp2, m_geo_wildcard_names = geo_wildcards; } -void DEMDynamicThread::changeOwnerSizes(const std::vector& IDs, const std::vector& factors) { - // Set the gpu for this thread - cudaSetDevice(streamInfo.device); - // cudaStream_t new_stream; - // cudaStreamCreate(&new_stream); - - // First get IDs and factors to device side - size_t IDSize = IDs.size() * sizeof(bodyID_t); - bodyID_t* dIDs = (bodyID_t*)solverScratchSpace.allocateTempVector("dIDs", IDSize); - DEME_GPU_CALL(cudaMemcpy(dIDs, IDs.data(), IDSize, cudaMemcpyHostToDevice)); - size_t factorSize = factors.size() * sizeof(float); - float* dFactors = (float*)solverScratchSpace.allocateTempVector("dFactors", factorSize); - DEME_GPU_CALL(cudaMemcpy(dFactors, factors.data(), factorSize, cudaMemcpyHostToDevice)); - - size_t idBoolSize = (size_t)simParams->nOwnerBodies * sizeof(notStupidBool_t); - size_t ownerFactorSize = (size_t)simParams->nOwnerBodies * sizeof(float); - // Bool table for whether this owner should change - notStupidBool_t* idBool = (notStupidBool_t*)solverScratchSpace.allocateTempVector("idBool", idBoolSize); - DEME_GPU_CALL(cudaMemset(idBool, 0, idBoolSize)); - float* ownerFactors = (float*)solverScratchSpace.allocateTempVector("ownerFactors", ownerFactorSize); - size_t blocks_needed_for_marking = (IDs.size() + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - - // Mark on the bool array those owners that need a change - misc_kernels->kernel("markOwnerToChange") - .instantiate() - .configure(dim3(blocks_needed_for_marking), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(idBool, ownerFactors, dIDs, dFactors, (size_t)IDs.size()); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - - // Change the size of the sphere components in question - size_t blocks_needed_for_changing = - (simParams->nSpheresGM + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - misc_kernels->kernel("modifyComponents") - .instantiate("deme::DEMDataDT") - .configure(dim3(blocks_needed_for_changing), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&granData, idBool, ownerFactors, (size_t)simParams->nSpheresGM); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - - solverScratchSpace.finishUsingTempVector("dIDs"); - solverScratchSpace.finishUsingTempVector("dFactors"); - solverScratchSpace.finishUsingTempVector("idBool"); - solverScratchSpace.finishUsingTempVector("ownerFactors"); - - // cudaStreamDestroy(new_stream); - - // Update them back to host - relPosSphereX.toHost(); - relPosSphereY.toHost(); - relPosSphereZ.toHost(); - radiiSphere.toHost(); -} - void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, size_t nOwnerClumps, unsigned int nExtObj, size_t nTriMeshes, size_t nSpheresGM, size_t nTriGM, + size_t nTriNeighbors, + size_t nMeshPatches, unsigned int nAnalGM, size_t nExtraContacts, unsigned int nMassProperties, @@ -426,6 +466,7 @@ void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, // Sizes of these arrays simParams->nSpheresGM = nSpheresGM; simParams->nTriGM = nTriGM; + simParams->nMeshPatches = nMeshPatches; simParams->nAnalGM = nAnalGM; simParams->nOwnerBodies = nOwnerBodies; simParams->nOwnerClumps = nOwnerClumps; @@ -461,6 +502,10 @@ void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DUAL_ARRAY_RESIZE(alphaZ, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(accSpecified, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(angAccSpecified, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshConvex, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshNeverWinner, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshWatertight, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshShellHalfThickness, nOwnerBodies, 0); // Resize the family mask `matrix' (in fact it is flattened) DEME_DUAL_ARRAY_RESIZE(familyMaskMatrix, (NUM_AVAL_FAMILIES + 1) * NUM_AVAL_FAMILIES / 2, DONT_PREVENT_CONTACT); @@ -487,11 +532,36 @@ void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, } // Resize to the number of triangle facets - DEME_DUAL_ARRAY_RESIZE(ownerMesh, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(ownerTriMesh, nTriGM, 0); DEME_DUAL_ARRAY_RESIZE(relPosNode1, nTriGM, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(relPosNode2, nTriGM, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(relPosNode3, nTriGM, make_float3(0)); - DEME_DUAL_ARRAY_RESIZE(triMaterialOffset, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(triPatchID, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(triNeighborIndex, nTriGM, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor1, nTriNeighbors, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor2, nTriNeighbors, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor3, nTriNeighbors, NULL_BODYID); + DEME_DEVICE_ARRAY_RESIZE(maxTriTriPenetration, nTriGM); + if (nTriGM > 0) { + DEME_GPU_CALL(cudaMemset(maxTriTriPenetration.data(), 0, nTriGM * sizeof(float))); + } + triPVTrackingEnabled = false; + triPVNumTrackedTriangles = 0; + triPVWindowSteps = 0; + triPVOwnerOrder.clear(); + triPVOwnerOffsets.clear(); + triPVOwnerCounts.clear(); + triPVOwnerToSlot.clear(); + DEME_DUAL_ARRAY_RESIZE(triPVGlobalTriToLocal, nTriGM, -1); + DEME_DUAL_ARRAY_RESIZE(triPVStepP, 1, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVStepPV, 1, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVAccumP, 1, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVAccumPV, 1, 0.f); + + // Resize to the number of mesh patches + DEME_DUAL_ARRAY_RESIZE(ownerPatchMesh, nMeshPatches, 0); + DEME_DUAL_ARRAY_RESIZE(patchMaterialOffset, nMeshPatches, 0); + DEME_DUAL_ARRAY_RESIZE(relPosPatch, nMeshPatches, make_float3(0)); // Resize to the number of analytical geometries DEME_DUAL_ARRAY_RESIZE(ownerAnalBody, nAnalGM, 0); @@ -515,30 +585,36 @@ void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DUAL_ARRAY_RESIZE(volumeOwnerBody, nMassProperties, 0); // Arrays for contact info - // The lengths of contact event-based arrays are just estimates. My estimate of total contact pairs is ~ 2n, and I - // think the max is 6n (although I can't prove it). Note the estimate should be large enough to decrease the number - // of reallocations in the simulation, but not too large that eats too much memory. + // The lengths of contact event-based arrays are just estimates. { // In any case, in this initialization process we should not make contact arrays smaller than it used to be, or - // we may lose data. Also, if this is a new-boot, we allocate this array for at least - // nSpheresGM*DEME_INIT_CNT_MULTIPLIER elements. + // we may lose data. Also, if this is a new-boot, we allocate this array for at least INITIAL_CONTACT_ARRAY_SIZE + // elements. + //// TODO: Resizing contact arrays at initialization is a must and almost like a liability at this point. If you + /// forget one of them, then if the sim entity number is small, you are likely to get segfault when you use them + /// because some of them may never experienced resizing. This is not a good design. size_t cnt_arr_size = - DEME_MAX(*solverScratchSpace.numContacts + nExtraContacts, nSpheresGM * DEME_INIT_CNT_MULTIPLIER); - DEME_DUAL_ARRAY_RESIZE(idGeometryA, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(idGeometryB, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(contactType, cnt_arr_size, NOT_A_CONTACT); - - if (!solverFlags.useNoContactRecord) { + DEME_MAX(*solverScratchSpace.numPrimitiveContacts + nExtraContacts, INITIAL_CONTACT_ARRAY_SIZE); + DEME_DUAL_ARRAY_RESIZE(idPrimitiveA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(idPrimitiveB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePrimitive, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(geomToPatchMap, cnt_arr_size, 0); + + DEME_DUAL_ARRAY_RESIZE(idPatchA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(idPatchB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePatch, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(contactPatchIsland, cnt_arr_size, NULL_BODYID); + + // If there are meshes, then sph--mesh case always use force storage, no getting around; if no mesh, then if no + // need to store forces, we can choose to not resize these arrays. + if (!(solverFlags.useNoContactRecord && simParams->nTriGM == 0)) { DEME_DUAL_ARRAY_RESIZE(contactForces, cnt_arr_size, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(contactTorque_convToForce, cnt_arr_size, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(contactPointGeometryA, cnt_arr_size, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(contactPointGeometryB, cnt_arr_size, make_float3(0)); - // The contact point device ranges must be zeroed for the same reason as in - // contactEventArraysResize: resize fills host values only, and these two arrays - // are never per-step cleared, so entries that no kernel writes (margin-only - // pairs) would otherwise expose recycled device memory. - DEME_GPU_CALL(cudaMemset(contactPointGeometryA.data(), 0, cnt_arr_size * sizeof(float3))); - DEME_GPU_CALL(cudaMemset(contactPointGeometryB.data(), 0, cnt_arr_size * sizeof(float3))); + if (simParams->storeNormal) { + DEME_DUAL_ARRAY_RESIZE(contactNormals, cnt_arr_size, make_float3(0)); + } } // Allocate memory for each wildcard array contactWildcards.resize(simParams->nContactWildcards); @@ -563,17 +639,14 @@ void DEMDynamicThread::allocateGPUArrays(size_t nOwnerBodies, std::make_unique>(nTriGM, 0, &m_approxHostBytesUsed, &m_approxDeviceBytesUsed); } } + // existingContactTypes has a fixed size depending on how many contact types are defined + DEME_DUAL_ARRAY_RESIZE(existingContactTypes, NUM_SUPPORTED_CONTACT_TYPES + 1, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(typeStartOffsetsPrimitive, NUM_SUPPORTED_CONTACT_TYPES + 1, 0); + DEME_DUAL_ARRAY_RESIZE(typeStartOffsetsPatch, NUM_SUPPORTED_CONTACT_TYPES + 1, 0); - // You know what, let's not init dT buffers, since kT will change it when needed anyway. Besides, changing it here + // You know what, let's not init dT's buffers, since kT will change it when needed anyway. Besides, changing it here // will cause problems in the case of a re-init-ed simulation with more clumps added to system, since we may // accidentally clamp those arrays. - /* - buffer_size = DEME_MAX(buffer_size, nSpheresGM * DEME_INIT_CNT_MULTIPLIER); - DEME_DEVICE_ARRAY_RESIZE(idGeometryA_buffer, buffer_size); - DEME_DEVICE_ARRAY_RESIZE(idGeometryB_buffer, buffer_size); - DEME_DEVICE_ARRAY_RESIZE(contactType_buffer, buffer_size); - DEME_DEVICE_ARRAY_RESIZE(contactMapping_buffer, buffer_size); - */ } void DEMDynamicThread::registerPolicies(const std::unordered_map& template_number_name_map, @@ -646,22 +719,32 @@ void DEMDynamicThread::populateEntityArrays(const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, const std::vector& ext_obj_moi_types, const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_offsets, size_t nExistOwners, size_t nExistSpheres, - size_t nExistingFacets) { + size_t nExistingFacets, + size_t nExistingMeshPatches, + size_t nExistingTriNeighbors) { // Load in clump components info (but only if instructed to use jitified clump templates). This step will be // repeated even if we are just adding some more clumps to system, not a complete re-initialization. size_t k = 0; @@ -858,9 +941,9 @@ void DEMDynamicThread::populateEntityArrays(const std::vectorcontact_pairs.at(jj); // idPair.first + n_processed_sp_comp can take into account the sphere components that have been loaded // in previous batches, makes this loading process scalable. - idGeometryA[cnt_arr_offset] = idPair.first + n_processed_sp_comp + nExistSpheres; - idGeometryB[cnt_arr_offset] = idPair.second + n_processed_sp_comp + nExistSpheres; - contactType[cnt_arr_offset] = SPHERE_SPHERE_CONTACT; // Only sph--sph cnt for now + idPatchA[cnt_arr_offset] = idPair.first + n_processed_sp_comp + nExistSpheres; + idPatchB[cnt_arr_offset] = idPair.second + n_processed_sp_comp + nExistSpheres; + contactTypePatch[cnt_arr_offset] = SPHERE_SPHERE_CONTACT; // Only sph--sph cnt for now unsigned int w_num = 0; for (const auto& w_name : m_contact_wildcard_names) { (*contactWildcards[w_num])[cnt_arr_offset] = a_batch->contact_wildcards.at(w_name).at(jj); @@ -938,7 +1021,8 @@ void DEMDynamicThread::populateEntityArrays(const std::vectorIsWatertight() ? 1 : 0; + ownerMeshShellHalfThickness[owner_id] = std::max(input_mesh_objs.at(i)->GetShellHalfThickness(), 0.f); + + // Store inherent geo wildcards (per-triangle: one value per triangle facet). { unsigned int w_num = 0; for (const auto& w_name : m_geo_wildcard_names) { @@ -975,64 +1067,104 @@ void DEMDynamicThread::populateEntityArrays(const std::vectorowner = i + owner_offset_for_mesh_obj; + input_mesh_objs.at(i)->owner = owner_id; input_mesh_objs.at(i)->cache_offset = m_meshes.size(); m_meshes.push_back(input_mesh_objs.at(i)); - inertiaPropOffsets[i + owner_offset_for_mesh_obj] = i + offset_for_mesh_obj_mass_template; + inertiaPropOffsets[owner_id] = solverFlags.useMassJitify + ? offset_for_mesh_obj_mass_template + mesh_obj_mass_offsets.at(i) + : i + offset_for_mesh_obj_mass_template; if (!solverFlags.useMassJitify) { - massOwnerBody[i + owner_offset_for_mesh_obj] = mesh_obj_mass_types.at(i); + massOwnerBody[owner_id] = mesh_obj_mass_types.at(i); const float3 this_moi = mesh_obj_moi_types.at(i); - mmiXX[i + owner_offset_for_mesh_obj] = this_moi.x; - mmiYY[i + owner_offset_for_mesh_obj] = this_moi.y; - mmiZZ[i + owner_offset_for_mesh_obj] = this_moi.z; + mmiXX[owner_id] = this_moi.x; + mmiYY[owner_id] = this_moi.y; + mmiZZ[owner_id] = this_moi.z; } auto this_CoM_coord = input_mesh_obj_xyz.at(i) - LBF; positionToVoxelID( - voxelID[i + owner_offset_for_mesh_obj], locX[i + owner_offset_for_mesh_obj], - locY[i + owner_offset_for_mesh_obj], locZ[i + owner_offset_for_mesh_obj], (double)this_CoM_coord.x, + voxelID[owner_id], locX[owner_id], locY[owner_id], locZ[owner_id], (double)this_CoM_coord.x, (double)this_CoM_coord.y, (double)this_CoM_coord.z, simParams->nvXp2, simParams->nvYp2, simParams->voxelSize, simParams->l); // Set mesh owner's oriQ auto oriQ_of_this = input_mesh_obj_rot.at(i); - oriQw[i + owner_offset_for_mesh_obj] = oriQ_of_this.w; - oriQx[i + owner_offset_for_mesh_obj] = oriQ_of_this.x; - oriQy[i + owner_offset_for_mesh_obj] = oriQ_of_this.y; - oriQz[i + owner_offset_for_mesh_obj] = oriQ_of_this.z; + oriQw[owner_id] = oriQ_of_this.w; + oriQx[owner_id] = oriQ_of_this.x; + oriQy[owner_id] = oriQ_of_this.y; + oriQz[owner_id] = oriQ_of_this.z; + + //// For setting initial vel ang angvel, DEME's expected usage is now setting them using trackers after + /// initialization. / For clumps, their init vel can be set via initializers because of historical reasons. + + // Populate patch info for this mesh + + // Populate patch locations for this mesh + // If explicitly set, use those; otherwise compute them + std::vector this_mesh_patch_locations; + if (input_mesh_objs.at(i)->patch_locations_explicitly_set) { + this_mesh_patch_locations = input_mesh_objs.at(i)->m_patch_locations; + } else { + this_mesh_patch_locations = input_mesh_objs.at(i)->ComputePatchLocations(); + } - //// TODO: and initial vel? + // mesh_patch_owner run length is the num of patches in this mesh entity + //// TODO: This flatten-then-init approach is historical and too ugly. + size_t this_patch_owner = mesh_patch_owner.at(p); + size_t p_start = p; // Record where patch ID of this run starts + for (; p < mesh_patch_owner.size(); p++) { + if (mesh_patch_owner.at(p) != this_patch_owner) + break; + ownerPatchMesh[nExistingMeshPatches + p] = owner_offset_for_mesh_obj + this_patch_owner; + patchMaterialOffset[nExistingMeshPatches + p] = mesh_patch_materials.at(p); + relPosPatch[nExistingMeshPatches + p] = this_mesh_patch_locations[p - p_start]; + } // Per-facet info //// TODO: This flatten-then-init approach is historical and too ugly. size_t this_facet_owner = mesh_facet_owner.at(k); + const bool mesh_needs_neighbors = + !(input_mesh_obj_convex.at(this_facet_owner) != 0 && input_mesh_obj_never_winner.at(this_facet_owner) != 0); for (; k < mesh_facet_owner.size(); k++) { // mesh_facet_owner run length is the num of facets in this mesh entity if (mesh_facet_owner.at(k) != this_facet_owner) break; - ownerMesh[nExistingFacets + k] = owner_offset_for_mesh_obj + this_facet_owner; - triMaterialOffset[nExistingFacets + k] = mesh_facet_materials.at(k); + const size_t global_tri = nExistingFacets + k; + ownerTriMesh[global_tri] = owner_offset_for_mesh_obj + this_facet_owner; + // Tri's patch belonging needs to take into account those patches that are previously added + triPatchID[global_tri] = nExistingMeshPatches + mesh_facet_patch.at(k); + if (mesh_needs_neighbors) { + triNeighborIndex[global_tri] = neighbor_write; + triNeighbor1[neighbor_write] = mesh_facet_neighbor1.at(k); + triNeighbor2[neighbor_write] = mesh_facet_neighbor2.at(k); + triNeighbor3[neighbor_write] = mesh_facet_neighbor3.at(k); + neighbor_write++; + } else { + triNeighborIndex[global_tri] = NULL_BODYID; + } DEMTriangle this_tri = mesh_facets.at(k); - relPosNode1[nExistingFacets + k] = this_tri.p1; - relPosNode2[nExistingFacets + k] = this_tri.p2; - relPosNode3[nExistingFacets + k] = this_tri.p3; + relPosNode1[global_tri] = this_tri.p1; + relPosNode2[global_tri] = this_tri.p2; + relPosNode3[global_tri] = this_tri.p3; } family_t this_family_num = input_mesh_obj_family.at(i); - familyID[i + owner_offset_for_mesh_obj] = this_family_num; + familyID[owner_id] = this_family_num; - // To save some mem + // Cached initial values for wildcards of this mesh is not needed anymore m_meshes.back()->ClearWildcards(); - DEME_DEBUG_PRINTF("dT just loaded a mesh in family %u", +(this_family_num)); - DEME_DEBUG_PRINTF("This mesh is owner %zu", (i + owner_offset_for_mesh_obj)); - DEME_DEBUG_PRINTF("Number of triangle facets loaded thus far: %zu", k); + // DEME_DEBUG_PRINTF("dT just loaded a mesh in family %u", +(this_family_num)); + // DEME_DEBUG_PRINTF("This mesh is owner %zu", (i + owner_offset_for_mesh_obj)); } + DEME_DEBUG_PRINTF("Number of meshes loaded this time: %zu", input_mesh_objs.size()); + DEME_DEBUG_PRINTF("Number of mesh patches loaded this time: %zu", p); + DEME_DEBUG_PRINTF("Number of triangle facets loaded this time: %zu", k); } void DEMDynamicThread::buildTrackedObjs(const std::vector>& input_clump_batches, const std::vector& ext_obj_comp_num, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, std::vector>& tracked_objs, size_t nExistOwners, size_t nExistSpheres, @@ -1052,7 +1184,7 @@ void DEMDynamicThread::buildTrackedObjs(const std::vector prescans_mesh_size; prescans_mesh_size.push_back(0); for (const auto& a_mesh : input_mesh_objs) { @@ -1087,11 +1219,18 @@ void DEMDynamicThread::buildTrackedObjs(const std::vectorload_order; tracked_obj->nSpanOwners = 1; tracked_obj->geoID = nExistingFacets + prescans_mesh_size.at(tracked_obj->load_order); + // For mesh, nGeos is the number of triangles (per-triangle geo wildcard). tracked_obj->nGeos = prescans_mesh_size.at(tracked_obj->load_order + 1) - prescans_mesh_size.at(tracked_obj->load_order); break; default: - DEME_ERROR("A DEM tracked object has an unknown type."); + DEME_ERROR(std::string("A DEM tracked object has an unknown type.")); + } + if (tracked_obj->nSpanOwnersOverride > 0) { + tracked_obj->nSpanOwners = tracked_obj->nSpanOwnersOverride; + } + if (tracked_obj->nGeosOverride > 0) { + tracked_obj->nGeos = tracked_obj->nGeosOverride; } } nTrackersProcessed = tracked_objs.size(); @@ -1102,13 +1241,20 @@ void DEMDynamicThread::initGPUArrays(const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const std::unordered_map& template_number_name_map, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, @@ -1116,6 +1262,9 @@ void DEMDynamicThread::initGPUArrays(const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_jit_types, + const std::vector& mesh_obj_moi_jit_types, + const std::vector& mesh_obj_mass_offsets, const std::vector>& loaded_materials, const std::vector& family_mask_matrix, const std::set& no_output_families, @@ -1124,13 +1273,17 @@ void DEMDynamicThread::initGPUArrays(const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, const std::vector& ext_obj_moi_types, const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_jit_types, + const std::vector& mesh_obj_moi_jit_types, + const std::vector& mesh_obj_mass_offsets, const std::vector>& loaded_materials, const std::vector& family_mask_matrix, const std::set& no_output_families, @@ -1161,111 +1324,39 @@ void DEMDynamicThread::updateClumpMeshArrays(const std::vector posX(simParams->nSpheresGM); - std::vector posY(simParams->nSpheresGM); - std::vector posZ(simParams->nSpheresGM); - std::vector spRadii(simParams->nSpheresGM); - std::vector families; - if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.resize(simParams->nSpheresGM); - } - size_t num_output_spheres = 0; - - for (size_t i = 0; i < simParams->nSpheresGM; i++) { - auto this_owner = ownerClumpBody[i]; - family_t this_family = familyID[this_owner]; - // If this (impl-level) family is in the no-output list, skip it - if (familiesNoOutput.find(this_family) != familiesNoOutput.end()) { - continue; - } - - float3 CoM; - float X, Y, Z; - voxelID_t voxel = voxelID[this_owner]; - subVoxelPos_t subVoxX = locX[this_owner]; - subVoxelPos_t subVoxY = locY[this_owner]; - subVoxelPos_t subVoxZ = locZ[this_owner]; - voxelIDToPosition(X, Y, Z, voxel, subVoxX, subVoxY, subVoxZ, simParams->nvXp2, - simParams->nvYp2, simParams->voxelSize, simParams->l); - CoM.x = X + simParams->LBFX; - CoM.y = Y + simParams->LBFY; - CoM.z = Z + simParams->LBFZ; - - size_t compOffset = (solverFlags.useClumpJitify) ? clumpComponentOffsetExt[i] : i; - float this_sp_deviation_x = relPosSphereX[compOffset]; - float this_sp_deviation_y = relPosSphereY[compOffset]; - float this_sp_deviation_z = relPosSphereZ[compOffset]; - float this_sp_rot_0 = oriQw[this_owner]; - float this_sp_rot_1 = oriQx[this_owner]; - float this_sp_rot_2 = oriQy[this_owner]; - float this_sp_rot_3 = oriQz[this_owner]; - applyOriQToVector3(this_sp_deviation_x, this_sp_deviation_y, this_sp_deviation_z, this_sp_rot_0, - this_sp_rot_1, this_sp_rot_2, this_sp_rot_3); - posX.at(num_output_spheres) = CoM.x + this_sp_deviation_x; - posY.at(num_output_spheres) = CoM.y + this_sp_deviation_y; - posZ.at(num_output_spheres) = CoM.z + this_sp_deviation_z; - // std::cout << "Sphere Pos: " << posX.at(i) << ", " << posY.at(i) << ", " << posZ.at(i) << std::endl; - - spRadii.at(num_output_spheres) = radiiSphere[compOffset]; - - // Family number - if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.at(num_output_spheres) = this_family; - } - - num_output_spheres++; - } - // Write basics - posX.resize(num_output_spheres); - posY.resize(num_output_spheres); - posZ.resize(num_output_spheres); - spRadii.resize(num_output_spheres); - // TODO: Set {} to the list of column names - pw.write(ptFile, chpf::Compressor::Type::USE_DEFAULT, - {OUTPUT_FILE_X_COL_NAME, OUTPUT_FILE_Y_COL_NAME, OUTPUT_FILE_Z_COL_NAME, OUTPUT_FILE_R_COL_NAME}, posX, - posY, posZ, spRadii); - // Write family numbers - if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.resize(num_output_spheres); - //// TODO: How to do that? - // pw.write(ptFile, chpf::Compressor::Type::USE_DEFAULT, {}, families); - } -} -#endif - void DEMDynamicThread::writeSpheresAsCsv(std::ofstream& ptFile) { - std::ostringstream outstrstream; - migrateFamilyToHost(); migrateClumpPosInfoToHost(); migrateClumpHighOrderInfoToHost(); migrateOwnerWildcardToHost(); migrateSphGeoWildcardToHost(); + writeSpheresAsCsvFromHost(ptFile); +} + +void DEMDynamicThread::writeSpheresAsCsvFromHost(std::ofstream& ptFile) { + std::ostringstream outstrstream; outstrstream << OUTPUT_FILE_X_COL_NAME + "," + OUTPUT_FILE_Y_COL_NAME + "," + OUTPUT_FILE_Z_COL_NAME + "," + OUTPUT_FILE_R_COL_NAME; @@ -1410,99 +1501,160 @@ void DEMDynamicThread::writeSpheresAsCsv(std::ofstream& ptFile) { ptFile << outstrstream.str(); } -#ifdef DEME_USE_CHPF -void DEMDynamicThread::writeClumpsAsChpf(std::ofstream& ptFile, unsigned int accuracy) { - //// TODO: Note using accuracy - chpf::Writer pw; +void DEMDynamicThread::writeSpheresAsVtk(std::ofstream& ptFile) { migrateFamilyToHost(); migrateClumpPosInfoToHost(); migrateClumpHighOrderInfoToHost(); + migrateOwnerWildcardToHost(); + migrateSphGeoWildcardToHost(); + writeSpheresAsVtkFromHost(ptFile); +} - // simParams host version should not be different from device version, so no need to update - std::vector posX(simParams->nOwnerBodies); - std::vector posY(simParams->nOwnerBodies); - std::vector posZ(simParams->nOwnerBodies); - std::vector Qw(simParams->nOwnerBodies); - std::vector Qx(simParams->nOwnerBodies); - std::vector Qy(simParams->nOwnerBodies); - std::vector Qz(simParams->nOwnerBodies); - std::vector clump_type(simParams->nOwnerBodies); - std::vector families; - if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.resize(simParams->nOwnerBodies); - } - size_t num_output_clumps = 0; - - for (size_t i = 0; i < simParams->nOwnerBodies; i++) { - auto this_owner = ownerClumpBody[i]; - family_t this_family = familyID[this_owner]; - // If this (impl-level) family is in the no-output list, skip it - if (familiesNoOutput.find(this_family) != familiesNoOutput.end()) { +void DEMDynamicThread::writeSpheresAsVtkFromHost(std::ofstream& ptFile) { + // Keep one compact point record per component sphere. ParaView can turn these points into rendered spheres with a + // Glyph filter using `r` as the scale array, without DEME writing a triangle tessellation for every sphere. + struct SpherePointData { + float3 pos; + float radius; + float3 vel; + float3 ang_vel; + float3 acc; + float3 ang_acc; + family_t family; + std::vector owner_wildcards; + std::vector geo_wildcards; + }; + + std::vector spheres; + spheres.reserve(simParams->nSpheresGM); + + // Resolve owner-frame component locations and collect all enabled attributes before writing the VTK section + // counts. Filtering is done here so every point-data array has exactly the same length as the POINTS section. + for (size_t i = 0; i < simParams->nSpheresGM; i++) { + const bodyID_t owner = ownerClumpBody[i]; + const family_t family = familyID[owner]; + if (familiesNoOutput.find(family) != familiesNoOutput.end()) { continue; } - float3 CoM; float X, Y, Z; - voxelID_t voxel = voxelID[i]; - subVoxelPos_t subVoxX = locX[i]; - subVoxelPos_t subVoxY = locY[i]; - subVoxelPos_t subVoxZ = locZ[i]; - voxelIDToPosition(X, Y, Z, voxel, subVoxX, subVoxY, subVoxZ, simParams->nvXp2, - simParams->nvYp2, simParams->voxelSize, simParams->l); - CoM.x = X + simParams->LBFX; - CoM.y = Y + simParams->LBFY; - CoM.z = Z + simParams->LBFZ; - posX.at(num_output_clumps) = CoM.x; - posY.at(num_output_clumps) = CoM.y; - posZ.at(num_output_clumps) = CoM.z; + voxelIDToPosition(X, Y, Z, voxelID[owner], locX[owner], locY[owner], + locZ[owner], simParams->nvXp2, simParams->nvYp2, + simParams->voxelSize, simParams->l); + const float3 owner_pos = make_float3(X + simParams->LBFX, Y + simParams->LBFY, Z + simParams->LBFZ); + const size_t component = solverFlags.useClumpJitify ? clumpComponentOffsetExt[i] : i; + float3 rel_pos = make_float3(relPosSphereX[component], relPosSphereY[component], relPosSphereZ[component]); + applyOriQToVector3(rel_pos.x, rel_pos.y, rel_pos.z, oriQw[owner], oriQx[owner], oriQy[owner], + oriQz[owner]); + + SpherePointData sphere; + sphere.pos = owner_pos + rel_pos; + sphere.radius = radiiSphere[component]; + sphere.vel = make_float3(vX[owner], vY[owner], vZ[owner]); + sphere.ang_vel = make_float3(omgBarX[owner], omgBarY[owner], omgBarZ[owner]); + sphere.acc = make_float3(aX[owner], aY[owner], aZ[owner]); + sphere.ang_acc = make_float3(alphaX[owner], alphaY[owner], alphaZ[owner]); + sphere.family = family; + if (solverFlags.outputFlags & OUTPUT_CONTENT::OWNER_WILDCARD) { + sphere.owner_wildcards.reserve(m_owner_wildcard_names.size()); + for (const auto& wildcard : ownerWildcards) { + sphere.owner_wildcards.push_back((*wildcard)[owner]); + } + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::GEO_WILDCARD) { + sphere.geo_wildcards.reserve(m_geo_wildcard_names.size()); + for (const auto& wildcard : sphereWildcards) { + sphere.geo_wildcards.push_back((*wildcard)[i]); + } + } + spheres.push_back(std::move(sphere)); + } - // Then quaternions - Qw.at(num_output_clumps) = oriQw[i]; - Qx.at(num_output_clumps) = oriQx[i]; - Qy.at(num_output_clumps) = oriQy[i]; - Qz.at(num_output_clumps) = oriQz[i]; + std::ostringstream out; + out << "# vtk DataFile Version 2.0\n"; + out << "DEME component spheres\n"; + out << "ASCII\n"; + out << "DATASET POLYDATA\n"; + out << "POINTS " << spheres.size() << " float\n"; + for (const auto& sphere : spheres) { + out << sphere.pos.x << " " << sphere.pos.y << " " << sphere.pos.z << "\n"; + } + out << "VERTICES " << spheres.size() << " " << 2 * spheres.size() << "\n"; + for (size_t i = 0; i < spheres.size(); i++) { + out << "1 " << i << "\n"; + } + out << "POINT_DATA " << spheres.size() << "\n"; - // Then type of clump - unsigned int clump_mark = inertiaPropOffsets[i]; - clump_type.at(num_output_clumps) = templateNumNameMap.at(clump_mark); + auto write_scalar = [&out, &spheres](const std::string& name, const auto& value) { + out << "SCALARS " << name << " float 1\n"; + out << "LOOKUP_TABLE default\n"; + for (const auto& sphere : spheres) { + out << value(sphere) << "\n"; + } + }; + auto write_vector = [&out, &spheres](const std::string& name, const auto& value) { + out << "VECTORS " << name << " float\n"; + for (const auto& sphere : spheres) { + const float3 vector = value(sphere); + out << vector.x << " " << vector.y << " " << vector.z << "\n"; + } + }; - // Family number - if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.at(num_output_clumps) = this_family; - } - - num_output_clumps++; - } - // Write basics - posX.resize(num_output_clumps); - posY.resize(num_output_clumps); - posZ.resize(num_output_clumps); - Qw.resize(num_output_clumps); - Qx.resize(num_output_clumps); - Qy.resize(num_output_clumps); - Qz.resize(num_output_clumps); - clump_type.resize(num_output_clumps); - pw.write(ptFile, chpf::Compressor::Type::USE_DEFAULT, - {OUTPUT_FILE_X_COL_NAME, OUTPUT_FILE_Y_COL_NAME, OUTPUT_FILE_Z_COL_NAME, OUTPUT_FILE_QW_COL_NAME, - OUTPUT_FILE_QX_COL_NAME, OUTPUT_FILE_QY_COL_NAME, OUTPUT_FILE_QZ_COL_NAME, OUTPUT_FILE_CLUMP_TYPE_NAME}, - posX, posY, posZ, Qw, Qx, Qy, Qz, clump_type); - // Write family numbers + write_scalar("r", [](const SpherePointData& sphere) { return sphere.radius; }); + if (solverFlags.outputFlags & OUTPUT_CONTENT::ABSV) { + write_scalar("absv", [](const SpherePointData& sphere) { return length(sphere.vel); }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::VEL) { + write_vector("velocity", [](const SpherePointData& sphere) { return sphere.vel; }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::ANG_VEL) { + write_vector("angular_velocity", [](const SpherePointData& sphere) { return sphere.ang_vel; }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::ABS_ACC) { + write_scalar("abs_acc", [](const SpherePointData& sphere) { return length(sphere.acc); }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::ACC) { + write_vector("acceleration", [](const SpherePointData& sphere) { return sphere.acc; }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::ANG_ACC) { + write_vector("angular_acceleration", [](const SpherePointData& sphere) { return sphere.ang_acc; }); + } if (solverFlags.outputFlags & OUTPUT_CONTENT::FAMILY) { - families.resize(num_output_clumps); - //// TODO: How to do that? - // pw.write(ptFile, chpf::Compressor::Type::USE_DEFAULT, {}, families); + write_scalar("family", [](const SpherePointData& sphere) { return +sphere.family; }); + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::OWNER_WILDCARD) { + size_t wildcard_index = 0; + for (const auto& wildcard_name : m_owner_wildcard_names) { + write_scalar(wildcard_name, [wildcard_index](const SpherePointData& sphere) { + return sphere.owner_wildcards[wildcard_index]; + }); + wildcard_index++; + } + } + if (solverFlags.outputFlags & OUTPUT_CONTENT::GEO_WILDCARD) { + size_t wildcard_index = 0; + for (const auto& wildcard_name : m_geo_wildcard_names) { + write_scalar(wildcard_name, [wildcard_index](const SpherePointData& sphere) { + return sphere.geo_wildcards[wildcard_index]; + }); + wildcard_index++; + } } + + ptFile << out.str(); } -#endif void DEMDynamicThread::writeClumpsAsCsv(std::ofstream& ptFile, unsigned int accuracy) { - std::ostringstream outstrstream; - outstrstream.precision(accuracy); - migrateFamilyToHost(); migrateClumpPosInfoToHost(); migrateClumpHighOrderInfoToHost(); migrateOwnerWildcardToHost(); + writeClumpsAsCsvFromHost(ptFile, accuracy); +} + +void DEMDynamicThread::writeClumpsAsCsvFromHost(std::ofstream& ptFile, unsigned int accuracy) { + std::ostringstream outstrstream; + outstrstream.precision(accuracy); // xyz and quaternion are always there outstrstream << OUTPUT_FILE_X_COL_NAME + "," + OUTPUT_FILE_Y_COL_NAME + "," + OUTPUT_FILE_Z_COL_NAME + @@ -1624,11 +1776,13 @@ void DEMDynamicThread::writeClumpsAsCsv(std::ofstream& ptFile, unsigned int accu } std::shared_ptr DEMDynamicThread::generateContactInfo(float force_thres) { - // Migrate contact info to host migrateFamilyToHost(); migrateClumpPosInfoToHost(); migrateContactInfoToHost(); + return generateContactInfoFromHost(force_thres); +} +std::shared_ptr DEMDynamicThread::generateContactInfoFromHost(float force_thres) { size_t total_contacts = *(solverScratchSpace.numContacts); // Wildcards supports only floats now std::vector> existing_wildcards(m_contact_wildcard_names.size()); @@ -1642,12 +1796,12 @@ std::shared_ptr DEMDynamicThread::generateContactInfo(floa size_t useful_cnt = 0; for (size_t i = 0; i < total_contacts; i++) { // Geos that are involved in this contact - auto geoA = idGeometryA[i]; - auto geoB = idGeometryB[i]; - auto type = contactType[i]; - // We don't output fake contacts; but right now, no contact will be marked fake by kT, so no need to check that - // if (type == NOT_A_CONTACT) - // continue; + auto geoA = idPatchA[i]; + auto geoB = idPatchB[i]; + auto type = contactTypePatch[i]; + if (type == NOT_A_CONTACT) { + DEME_ERROR("NOT_A_CONTACT reached dT contact output; kT should compact null contacts before handoff."); + } float3 forcexyz = contactForces[i]; float3 torque = contactTorque_convToForce[i]; @@ -1656,11 +1810,8 @@ std::shared_ptr DEMDynamicThread::generateContactInfo(floa continue; } - // geoA's owner must be a sphere - auto ownerA = ownerClumpBody[geoA]; - bodyID_t ownerB; - // geoB's owner depends... - ownerB = getGeoOwnerID(geoB, type); + bodyID_t ownerA = getPatchOwnerID(geoA, decodeTypeA(type)); + bodyID_t ownerB = getPatchOwnerID(geoB, decodeTypeB(type)); // Type is mapped to SS, SM and such.... contactInfo.Get("ContactType")[useful_cnt] = contact_type_out_name_map.at(type); @@ -1720,16 +1871,8 @@ std::shared_ptr DEMDynamicThread::generateContactInfo(floa // To get contact normal: it's just contact point - sphereA center, that gives you the outward normal for body A if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::NORMAL) { - size_t compOffset = (solverFlags.useClumpJitify) ? clumpComponentOffsetExt[geoA] : geoA; - float3 this_sp_deviation; - this_sp_deviation.x = relPosSphereX[compOffset]; - this_sp_deviation.y = relPosSphereY[compOffset]; - this_sp_deviation.z = relPosSphereZ[compOffset]; - applyOriQToVector3(this_sp_deviation.x, this_sp_deviation.y, this_sp_deviation.z, oriQA.w, - oriQA.x, oriQA.y, oriQA.z); - float3 pos = CoM + this_sp_deviation; - float3 normal = normalize(cntPntA - pos); - contactInfo.Get("Normal")[useful_cnt] = normal; + // If CNT_OUTPUT_CONTENT::NORMAL is on, then contactNormals is always stored + contactInfo.Get("Normal")[useful_cnt] = contactNormals[i]; } // Torque is in global already... @@ -1762,9 +1905,16 @@ std::shared_ptr DEMDynamicThread::generateContactInfo(floa } void DEMDynamicThread::writeContactsAsCsv(std::ofstream& ptFile, float force_thres) { + migrateFamilyToHost(); + migrateClumpPosInfoToHost(); + migrateContactInfoToHost(); + writeContactsAsCsvFromHost(ptFile, force_thres); +} + +void DEMDynamicThread::writeContactsAsCsvFromHost(std::ofstream& ptFile, float force_thres) { std::ostringstream outstrstream; - std::shared_ptr contactInfo = generateContactInfo(force_thres); + std::shared_ptr contactInfo = generateContactInfoFromHost(force_thres); outstrstream << OUTPUT_FILE_CNT_TYPE_NAME; if (solverFlags.cntOutFlags & CNT_OUTPUT_CONTENT::OWNER) { @@ -1855,8 +2005,27 @@ void DEMDynamicThread::writeContactsAsCsv(std::ofstream& ptFile, float force_thr } void DEMDynamicThread::writeMeshesAsVtk(std::ofstream& ptFile) { - std::ostringstream ostream; migrateFamilyToHost(); + migrateClumpPosInfoToHost(); + writeMeshesAsVtkFromHost(ptFile); +} + +void DEMDynamicThread::writeMeshesAsVtkFromHost(std::ofstream& ptFile) { + std::ostringstream ostream; + + auto ownerPosFromHost = [this](bodyID_t owner) { + double X, Y, Z; + voxelID_t voxel = voxelID[owner]; + subVoxelPos_t subVoxX = locX[owner]; + subVoxelPos_t subVoxY = locY[owner]; + subVoxelPos_t subVoxZ = locZ[owner]; + voxelIDToPosition(X, Y, Z, voxel, subVoxX, subVoxY, subVoxZ, simParams->nvXp2, + simParams->nvYp2, simParams->voxelSize, simParams->l); + return make_float3(X + simParams->LBFX, Y + simParams->LBFY, Z + simParams->LBFZ); + }; + auto ownerOriQFromHost = [this](bodyID_t owner) { + return make_float4(oriQx[owner], oriQy[owner], oriQz[owner], oriQw[owner]); + }; std::vector vertexOffset(m_meshes.size() + 1, 0); size_t total_f = 0; @@ -1901,8 +2070,8 @@ void DEMDynamicThread::writeMeshesAsVtk(std::ofstream& ptFile) { for (const auto& mmesh : m_meshes) { if (!thisMeshSkip[mesh_num]) { bodyID_t mowner = mmesh->owner; - float3 ownerPos = this->getOwnerPos(mowner)[0]; - float4 ownerOriQ = this->getOwnerOriQ(mowner)[0]; + float3 ownerPos = ownerPosFromHost(mowner); + float4 ownerOriQ = ownerOriQFromHost(mowner); for (const auto& v : mmesh->GetCoordsVertices()) { float3 point = v; applyFrameTransformLocalToGlobal(point, ownerPos, ownerOriQ); @@ -1939,78 +2108,418 @@ void DEMDynamicThread::writeMeshesAsVtk(std::ofstream& ptFile) { mesh_num++; } + const unsigned int mesh_flags = solverFlags.meshOutFlags; + if (mesh_flags != static_cast(MESH_OUTPUT_CONTENT::XYZ)) { + ostream << "\nCELL_DATA " << total_f << "\n"; + + // Visit triangles in exactly the same order as the CELLS section. The global triangle ID advances through + // skipped meshes as well, preserving IDs used by DEME contact and wildcard APIs. + auto for_each_output_triangle = [&](const auto& callback) { + size_t global_triangle = 0; + for (size_t output_mesh = 0; output_mesh < m_meshes.size(); output_mesh++) { + const auto& mesh = m_meshes[output_mesh]; + const size_t triangle_count = mesh->GetIndicesVertexes().size(); + if (!thisMeshSkip[output_mesh]) { + for (size_t local_triangle = 0; local_triangle < triangle_count; local_triangle++) { + callback(*mesh, output_mesh, local_triangle, global_triangle, mesh->owner); + global_triangle++; + } + } else { + global_triangle += triangle_count; + } + } + }; + auto write_scalar = [&](const std::string& name, const std::string& type, const auto& value) { + ostream << "SCALARS " << name << " " << type << " 1\nLOOKUP_TABLE default\n"; + for_each_output_triangle([&](const DEMMesh& mesh, size_t mesh_id, size_t local_triangle, + size_t global_triangle, bodyID_t owner) { + ostream << value(mesh, mesh_id, local_triangle, global_triangle, owner) << "\n"; + }); + }; + auto write_vector = [&](const std::string& name, const auto& value) { + ostream << "VECTORS " << name << " float\n"; + for_each_output_triangle([&](const DEMMesh& mesh, size_t mesh_id, size_t local_triangle, + size_t global_triangle, bodyID_t owner) { + const float3 vector = value(mesh, mesh_id, local_triangle, global_triangle, owner); + ostream << vector.x << " " << vector.y << " " << vector.z << "\n"; + }); + }; + + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::QUAT)) { + ostream << "SCALARS quaternion float 4\nLOOKUP_TABLE default\n"; + for_each_output_triangle([&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + ostream << oriQw[owner] << " " << oriQx[owner] << " " << oriQy[owner] << " " << oriQz[owner] << "\n"; + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::ABSV)) { + write_scalar("absv", "float", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return length(make_float3(vX[owner], vY[owner], vZ[owner])); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::VEL)) { + write_vector("velocity", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return make_float3(vX[owner], vY[owner], vZ[owner]); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::ANG_VEL)) { + write_vector("angular_velocity", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return make_float3(omgBarX[owner], omgBarY[owner], omgBarZ[owner]); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::ABS_ACC)) { + write_scalar("abs_acc", "float", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return length(make_float3(aX[owner], aY[owner], aZ[owner])); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::ACC)) { + write_vector("acceleration", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return make_float3(aX[owner], aY[owner], aZ[owner]); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::ANG_ACC)) { + write_vector("angular_acceleration", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return make_float3(alphaX[owner], alphaY[owner], alphaZ[owner]); + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::FAMILY)) { + write_scalar("family", "int", + [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { return +familyID[owner]; }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::MAT)) { + write_scalar("material", "int", [&](const DEMMesh&, size_t, size_t, size_t global_triangle, bodyID_t) { + return +patchMaterialOffset[triPatchID[global_triangle]]; + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::OWNER)) { + write_scalar("owner", "int", [&](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { return owner; }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::MESH_ID)) { + write_scalar("mesh_id", "int", + [&](const DEMMesh&, size_t mesh_id, size_t, size_t, bodyID_t) { return mesh_id; }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::TRI_ID)) { + write_scalar("tri_id", "int", [&](const DEMMesh&, size_t, size_t, size_t global_triangle, bodyID_t) { + return global_triangle; + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::PATCH_ID)) { + write_scalar("patch_id", "int", [&](const DEMMesh&, size_t, size_t, size_t global_triangle, bodyID_t) { + return +triPatchID[global_triangle]; + }); + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::OWNER_WILDCARD)) { + size_t wildcard_index = 0; + for (const auto& wildcard_name : m_owner_wildcard_names) { + write_scalar(wildcard_name, "float", + [&, wildcard_index](const DEMMesh&, size_t, size_t, size_t, bodyID_t owner) { + return (*ownerWildcards[wildcard_index])[owner]; + }); + wildcard_index++; + } + } + if (mesh_flags & static_cast(MESH_OUTPUT_CONTENT::GEO_WILDCARD)) { + size_t wildcard_index = 0; + for (const auto& wildcard_name : m_geo_wildcard_names) { + write_scalar(wildcard_name, "float", + [&, wildcard_index](const DEMMesh&, size_t, size_t, size_t global_triangle, bodyID_t) { + return (*triWildcards[wildcard_index])[global_triangle]; + }); + wildcard_index++; + } + } + } + ptFile << ostream.str(); } -inline void DEMDynamicThread::contactEventArraysResize(size_t nContactPairs) { - DEME_DUAL_ARRAY_RESIZE(idGeometryA, nContactPairs, 0); - DEME_DUAL_ARRAY_RESIZE(idGeometryB, nContactPairs, 0); - DEME_DUAL_ARRAY_RESIZE(contactType, nContactPairs, NOT_A_CONTACT); +void DEMDynamicThread::writeMeshesAsStl(std::ofstream& ptFile) { + migrateFamilyToHost(); + migrateClumpPosInfoToHost(); + writeMeshesAsStlFromHost(ptFile); +} - if (!solverFlags.useNoContactRecord) { - DEME_DUAL_ARRAY_RESIZE(contactForces, nContactPairs, make_float3(0)); - DEME_DUAL_ARRAY_RESIZE(contactTorque_convToForce, nContactPairs, make_float3(0)); - DEME_DUAL_ARRAY_RESIZE(contactPointGeometryA, nContactPairs, make_float3(0)); - DEME_DUAL_ARRAY_RESIZE(contactPointGeometryB, nContactPairs, make_float3(0)); - // DualArray::resize(n, val) fills host values only, so the grown device range holds - // whatever the allocator recycled. contactForces and contactTorque_convToForce are - // cleared every time step by prepareForceArrays before use, but the contact point - // arrays are only ever written for entries whose ContactType is a real contact, and - // collectContactForcesAccStyle reads them for ALL entries: cross(garbage, 0) is 0 for - // finite garbage but NaN for NaN/inf garbage, which poisons that owner's angular - // acceleration and cascades (quaternion -> position -> full-grid bin ranges in kT's - // contact detection, presenting as a GPU-pegged stall). Zero them on device here. - DEME_GPU_CALL(cudaMemset(contactPointGeometryA.data(), 0, nContactPairs * sizeof(float3))); - DEME_GPU_CALL(cudaMemset(contactPointGeometryB.data(), 0, nContactPairs * sizeof(float3))); +void DEMDynamicThread::writeMeshesAsStlFromHost(std::ofstream& ptFile) { + std::ostringstream ostream; + + auto ownerPosFromHost = [this](bodyID_t owner) { + double X, Y, Z; + voxelID_t voxel = voxelID[owner]; + subVoxelPos_t subVoxX = locX[owner]; + subVoxelPos_t subVoxY = locY[owner]; + subVoxelPos_t subVoxZ = locZ[owner]; + voxelIDToPosition(X, Y, Z, voxel, subVoxX, subVoxY, subVoxZ, simParams->nvXp2, + simParams->nvYp2, simParams->voxelSize, simParams->l); + return make_float3(X + simParams->LBFX, Y + simParams->LBFY, Z + simParams->LBFZ); + }; + auto ownerOriQFromHost = [this](bodyID_t owner) { + return make_float4(oriQx[owner], oriQy[owner], oriQz[owner], oriQw[owner]); + }; + + std::vector thisMeshSkip(m_meshes.size(), 0); + unsigned int mesh_num = 0; + for (const auto& mmesh : m_meshes) { + bodyID_t mowner = mmesh->owner; + family_t this_family = familyID[mowner]; + if (familiesNoOutput.find(this_family) != familiesNoOutput.end()) { + thisMeshSkip[mesh_num] = 1; + } + mesh_num++; } - // Re-packing pointers now is automatic + ostream << "solid DEMSimulation" << std::endl; + mesh_num = 0; + for (const auto& mmesh : m_meshes) { + if (!thisMeshSkip[mesh_num]) { + bodyID_t mowner = mmesh->owner; + float3 ownerPos = ownerPosFromHost(mowner); + float4 ownerOriQ = ownerOriQFromHost(mowner); + const auto& vertices = mmesh->GetCoordsVertices(); + const auto& faces = mmesh->GetIndicesVertexes(); + + for (const auto& f : faces) { + float3 v0 = vertices[f.x]; + float3 v1 = vertices[f.y]; + float3 v2 = vertices[f.z]; + + applyFrameTransformLocalToGlobal(v0, ownerPos, ownerOriQ); + applyFrameTransformLocalToGlobal(v1, ownerPos, ownerOriQ); + applyFrameTransformLocalToGlobal(v2, ownerPos, ownerOriQ); + + float3 normal = face_normal(v0, v1, v2); + ostream << " facet normal " << normal.x << " " << normal.y << " " << normal.z << std::endl; + ostream << " outer loop" << std::endl; + ostream << " vertex " << v0.x << " " << v0.y << " " << v0.z << std::endl; + ostream << " vertex " << v1.x << " " << v1.y << " " << v1.z << std::endl; + ostream << " vertex " << v2.x << " " << v2.y << " " << v2.z << std::endl; + ostream << " endloop" << std::endl; + ostream << " endfacet" << std::endl; + } + } + mesh_num++; + } + ostream << "endsolid DEMSimulation" << std::endl; + ptFile << ostream.str(); +} - // Sync pointers to device can be delayed... we'll only need to do that before kernel calls +void DEMDynamicThread::writeMeshesAsPly(std::ofstream& ptFile, bool patch_colors) { + migrateFamilyToHost(); + migrateClumpPosInfoToHost(); + writeMeshesAsPlyFromHost(ptFile, patch_colors); } -inline void DEMDynamicThread::unpackMyBuffer() { - // Make a note on the contact number of the previous time step - *solverScratchSpace.numPrevContacts = *solverScratchSpace.numContacts; - // kT's batch of produce is made with this max drift in mind - pSchedSupport->dynamicMaxFutureDrift = (pSchedSupport->kinematicMaxFutureDrift).load(); - // DEME_DEBUG_PRINTF("dynamicMaxFutureDrift is %u", (pSchedSupport->dynamicMaxFutureDrift).load()); +void DEMDynamicThread::writeMeshesAsPlyFromHost(std::ofstream& ptFile, bool patch_colors) { + std::ostringstream ostream; - DEME_GPU_CALL( - cudaMemcpy(&(solverScratchSpace.numContacts), &nContactPairs_buffer, sizeof(size_t), cudaMemcpyDeviceToDevice)); - solverScratchSpace.numContacts.toHost(); - // Need to resize those contact event-based arrays before usage - if (*solverScratchSpace.numContacts > idGeometryA.size() || *solverScratchSpace.numContacts > buffer_size) { - contactEventArraysResize(*solverScratchSpace.numContacts); - } + auto ownerPosFromHost = [this](bodyID_t owner) { + double X, Y, Z; + voxelID_t voxel = voxelID[owner]; + subVoxelPos_t subVoxX = locX[owner]; + subVoxelPos_t subVoxY = locY[owner]; + subVoxelPos_t subVoxZ = locZ[owner]; + voxelIDToPosition(X, Y, Z, voxel, subVoxX, subVoxY, subVoxZ, simParams->nvXp2, + simParams->nvYp2, simParams->voxelSize, simParams->l); + return make_float3(X + simParams->LBFX, Y + simParams->LBFY, Z + simParams->LBFZ); + }; + auto ownerOriQFromHost = [this](bodyID_t owner) { + return make_float4(oriQx[owner], oriQy[owner], oriQz[owner], oriQw[owner]); + }; - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryA, idGeometryA_buffer.data(), - *solverScratchSpace.numContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryB, idGeometryB_buffer.data(), - *solverScratchSpace.numContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->contactType, contactType_buffer.data(), - *solverScratchSpace.numContacts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); - if (!solverFlags.isHistoryless) { - // Note we don't have to use dedicated memory space for unpacking contactMapping_buffer contents, because we - // only use it once per kT update, at the time of unpacking. So let us just use a temp vector to store it. - size_t mapping_bytes = (*solverScratchSpace.numContacts) * sizeof(contactPairs_t); - granData->contactMapping = - (contactPairs_t*)solverScratchSpace.allocateTempVector("contactMapping", mapping_bytes); - DEME_GPU_CALL(cudaMemcpy(granData->contactMapping, contactMapping_buffer.data(), mapping_bytes, - cudaMemcpyDeviceToDevice)); - } - // Prepare for kernel calls immediately after - granData.toDevice(); -} + std::vector vertexOffset(m_meshes.size() + 1, 0); + size_t total_f = 0; + size_t total_v = 0; + unsigned int mesh_num = 0; -inline void DEMDynamicThread::sendToTheirBuffer() { - DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_voxelID, granData->voxelID, - simParams->nOwnerBodies * sizeof(voxelID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locX, granData->locX, - simParams->nOwnerBodies * sizeof(subVoxelPos_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locY, granData->locY, - simParams->nOwnerBodies * sizeof(subVoxelPos_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locZ, granData->locZ, + std::vector thisMeshSkip(m_meshes.size(), 0); + for (const auto& mmesh : m_meshes) { + bodyID_t mowner = mmesh->owner; + family_t this_family = familyID[mowner]; + if (familiesNoOutput.find(this_family) != familiesNoOutput.end()) { + thisMeshSkip[mesh_num] = 1; + } else { + vertexOffset[mesh_num + 1] = mmesh->GetCoordsVertices().size(); + total_v += mmesh->GetCoordsVertices().size(); + total_f += mmesh->GetIndicesVertexes().size(); + } + mesh_num++; + } + + for (unsigned int i = 1; i < m_meshes.size(); i++) { + vertexOffset[i] = vertexOffset[i] + vertexOffset[i - 1]; + } + + ostream << "ply" << std::endl; + ostream << "format ascii 1.0" << std::endl; + ostream << "comment DEM simulation mesh export" << std::endl; + ostream << "element vertex " << total_v << std::endl; + ostream << "property float x" << std::endl; + ostream << "property float y" << std::endl; + ostream << "property float z" << std::endl; + ostream << "element face " << total_f << std::endl; + ostream << "property list uchar int vertex_indices" << std::endl; + if (patch_colors) { + ostream << "property uchar red" << std::endl; + ostream << "property uchar green" << std::endl; + ostream << "property uchar blue" << std::endl; + } + ostream << "end_header" << std::endl; + + mesh_num = 0; + for (const auto& mmesh : m_meshes) { + if (!thisMeshSkip[mesh_num]) { + bodyID_t mowner = mmesh->owner; + float3 ownerPos = ownerPosFromHost(mowner); + float4 ownerOriQ = ownerOriQFromHost(mowner); + for (const auto& v : mmesh->GetCoordsVertices()) { + float3 point = v; + applyFrameTransformLocalToGlobal(point, ownerPos, ownerOriQ); + ostream << point.x << " " << point.y << " " << point.z << std::endl; + } + } + mesh_num++; + } + + ostream << std::endl; + auto hash32 = [](uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352d; + x ^= x >> 15; + x *= 0x846ca68b; + x ^= x >> 16; + return x; + }; + + mesh_num = 0; + for (const auto& mmesh : m_meshes) { + if (!thisMeshSkip[mesh_num]) { + const auto& faces = mmesh->GetIndicesVertexes(); + const auto& patch_ids = mmesh->GetPatchIDs(); + bool has_patch_ids = (patch_ids.size() == faces.size()); + + for (size_t fi = 0; fi < faces.size(); ++fi) { + const auto& f = faces[fi]; + ostream << "3 " << (size_t)f.x + vertexOffset[mesh_num] << " " << (size_t)f.y + vertexOffset[mesh_num] + << " " << (size_t)f.z + vertexOffset[mesh_num]; + if (patch_colors) { + uint32_t patch_id = has_patch_ids ? static_cast(patch_ids[fi]) : 0u; + uint32_t key = patch_id + 0x9e3779b9u * (mesh_num + 1u); + uint32_t h = hash32(key); + unsigned int r = (h >> 16) & 0xFFu; + unsigned int g = (h >> 8) & 0xFFu; + unsigned int b = h & 0xFFu; + ostream << " " << r << " " << g << " " << b; + } + ostream << std::endl; + } + } + mesh_num++; + } + + ptFile << ostream.str(); +} + +inline void DEMDynamicThread::contactPrimitivesArraysResize(size_t nContactPairs) { + DEME_DUAL_ARRAY_RESIZE(idPrimitiveA, nContactPairs, 0); + DEME_DUAL_ARRAY_RESIZE(idPrimitiveB, nContactPairs, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePrimitive, nContactPairs, NOT_A_CONTACT); + + // NEW: Resize geomToPatchMap to match geometry array size + DEME_DUAL_ARRAY_RESIZE(geomToPatchMap, nContactPairs, 0); + + // If there are meshes, then sph--mesh case always use force storage, no getting around; if no mesh, then if no need + // to store forces, we can choose to not resize these arrays. + if (!(solverFlags.useNoContactRecord && simParams->nTriGM == 0)) { + DEME_DUAL_ARRAY_RESIZE(contactForces, nContactPairs, make_float3(0)); + DEME_DUAL_ARRAY_RESIZE(contactTorque_convToForce, nContactPairs, make_float3(0)); + DEME_DUAL_ARRAY_RESIZE(contactPointGeometryA, nContactPairs, make_float3(0)); + DEME_DUAL_ARRAY_RESIZE(contactPointGeometryB, nContactPairs, make_float3(0)); + if (simParams->storeNormal) { + DEME_DUAL_ARRAY_RESIZE(contactNormals, nContactPairs, make_float3(0)); + } + } + + // Re-packing pointers now is automatic + + // Sync pointers to device can be delayed... we'll only need to do that before kernel calls + + // Also note that dT does not have to worry about contact persistence, because kT handles that +} + +inline void DEMDynamicThread::contactPatchArrayResize(size_t nPatchPairs) { + // NEW: Resize separate patch ID arrays (sized to patch pairs, the shorter array) + DEME_DUAL_ARRAY_RESIZE(idPatchA, nPatchPairs, 0); + DEME_DUAL_ARRAY_RESIZE(idPatchB, nPatchPairs, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePatch, nPatchPairs, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(contactPatchIsland, nPatchPairs, NULL_BODYID); + + // Re-packing pointers to device now is automatic + // Sync pointers to device can be delayed... we'll only need to do that before kernel calls +} + +inline void DEMDynamicThread::unpackMyBuffer() { + // Make a note on the contact number of the previous time step + *solverScratchSpace.numPrevContacts = *solverScratchSpace.numContacts; + *solverScratchSpace.numPrevPrimitiveContacts = *solverScratchSpace.numPrimitiveContacts; + // kT's batch of produce is made with this max drift in mind + pSchedSupport->dynamicMaxFutureDrift = (pSchedSupport->kinematicMaxFutureDrift).load(); + // DEME_DEBUG_PRINTF("dynamicMaxFutureDrift is %u", (pSchedSupport->dynamicMaxFutureDrift).load()); + + DEME_GPU_CALL(cudaMemcpy(&(solverScratchSpace.numPrimitiveContacts), &nPrimitiveContactPairs_buffer, sizeof(size_t), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(&(solverScratchSpace.numContacts), &nPatchContactPairs_buffer, sizeof(size_t), + cudaMemcpyDeviceToDevice)); + solverScratchSpace.numContacts.toHost(); + solverScratchSpace.numPrimitiveContacts.toHost(); + // Need to resize those contact event-based arrays before usage + if (*solverScratchSpace.numPrimitiveContacts > idPrimitiveA.size()) { + contactPrimitivesArraysResize(*solverScratchSpace.numPrimitiveContacts); + } + if (*solverScratchSpace.numContacts > idPatchA.size()) { + contactPatchArrayResize(*solverScratchSpace.numContacts); + } + + DEME_GPU_CALL(cudaMemcpy(granData->idPrimitiveA, idPrimitiveA_buffer.data(), + *solverScratchSpace.numPrimitiveContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->idPrimitiveB, idPrimitiveB_buffer.data(), + *solverScratchSpace.numPrimitiveContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactTypePrimitive, contactTypePrimitive_buffer.data(), + *solverScratchSpace.numPrimitiveContacts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->geomToPatchMap, geomToPatchMap_buffer.data(), + *solverScratchSpace.numPrimitiveContacts * sizeof(contactPairs_t), + cudaMemcpyDeviceToDevice)); + + // Unpack separate patch ID arrays + DEME_GPU_CALL(cudaMemcpy(granData->idPatchA, idPatchA_buffer.data(), + *solverScratchSpace.numContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->idPatchB, idPatchB_buffer.data(), + *solverScratchSpace.numContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactTypePatch, contactTypePatch_buffer.data(), + *solverScratchSpace.numContacts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactPatchIsland, contactPatchIsland_buffer.data(), + *solverScratchSpace.numContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + + if (!solverFlags.isHistoryless) { + // Note we don't have to use dedicated memory space for unpacking contactMapping_buffer contents, because we + // only use it once per kT update, at the time of unpacking. So let us just use a temp vector to store it. + size_t mapping_bytes = (*solverScratchSpace.numContacts) * sizeof(contactPairs_t); + granData->contactMapping = + (contactPairs_t*)solverScratchSpace.allocateTempVector("contactMapping", mapping_bytes); + DEME_GPU_CALL(cudaMemcpy(granData->contactMapping, contactMapping_buffer.data(), mapping_bytes, + cudaMemcpyDeviceToDevice)); + // std::cout << "Unpacked contactMapping: " << std::endl; + // displayDeviceArray(granData->contactMapping, *solverScratchSpace.numContacts); + } + // Prepare for kernel calls immediately after; queue this pointer-bundle refresh after the unpack copies. + granData.toDeviceAsync(streamInfo.stream); +} + +inline void DEMDynamicThread::sendToTheirBuffer() { + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_voxelID, granData->voxelID, + simParams->nOwnerBodies * sizeof(voxelID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locX, granData->locX, + simParams->nOwnerBodies * sizeof(subVoxelPos_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locY, granData->locY, + simParams->nOwnerBodies * sizeof(subVoxelPos_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_locZ, granData->locZ, simParams->nOwnerBodies * sizeof(subVoxelPos_t), cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_oriQ0, granData->oriQw, simParams->nOwnerBodies * sizeof(oriQ_t), cudaMemcpyDeviceToDevice)); @@ -2022,14 +2531,22 @@ inline void DEMDynamicThread::sendToTheirBuffer() { cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_absVel, pCycleVel, simParams->nOwnerBodies * sizeof(float), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_absAngVel, pCycleAngVel, simParams->nOwnerBodies * sizeof(float), + cudaMemcpyDeviceToDevice)); // Send simulation metrics for kT's reference. - DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_ts, &(simParams->h), sizeof(float), cudaMemcpyHostToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_ts, &(simParams->dyn.h), sizeof(float), cudaMemcpyHostToDevice)); // Note that perhapsIdealFutureDrift is non-negative, and it will be used to determine the margin size; however, if // scheduleHelper is instructed to have negative future drift then perhapsIdealFutureDrift no longer affects them. DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_maxDrift, perhapsIdealFutureDrift.getHostPointer(), sizeof(unsigned int), cudaMemcpyHostToDevice)); + // Send per-triangle max tri-tri penetration values for kT's margin computation. + if (!simParams->meshParticlesLowPoly && simParams->nTriGM > 0) { + DEME_GPU_CALL(cudaMemcpy(granData->pKTOwnedBuffer_maxTriTriPenetration, maxTriTriPenetration.data(), + (size_t)simParams->nTriGM * sizeof(float), cudaMemcpyDeviceToDevice)); + } + // Family number is a typical changable quantity on-the-fly. If this flag is on, dT is responsible for sending this // info to kT. if (solverFlags.canFamilyChangeOnDevice) { @@ -2054,7 +2571,317 @@ inline void DEMDynamicThread::sendToTheirBuffer() { pSchedSupport->kinematicIngredProdDateStamp = (pSchedSupport->currentStampOfDynamic).load(); } -inline void DEMDynamicThread::migrateEnduringContacts() { +inline void DEMDynamicThread::reportLostContactDebugDetails(const LostContactDebugSnapshot& oldContactSnapshot, + const notStupidBool_t* contactSentry, + float* const* newWildcards, + size_t lostContactCount) { + if (DEME_GET_VERBOSITY() < VERBOSITY_DEBUG || lostContactCount == 0) { + return; + } + + // This method is intentionally DEBUG-only. It performs blocking host copies and prints detailed identity data for + // old live patch contacts that failed to find a migration partner in the newly received kT contact array. + const size_t nPrevContacts = *solverScratchSpace.numPrevContacts; + const size_t nNewContacts = *solverScratchSpace.numContacts; + const size_t nPrevPrimitiveContacts = *solverScratchSpace.numPrevPrimitiveContacts; + const size_t nNewPrimitiveContacts = *solverScratchSpace.numPrimitiveContacts; + + std::vector sentryHost(nPrevContacts); + DEME_GPU_CALL( + cudaMemcpy(sentryHost.data(), contactSentry, nPrevContacts * sizeof(notStupidBool_t), cudaMemcpyDeviceToHost)); + + std::vector wildcardNames(m_contact_wildcard_names.begin(), m_contact_wildcard_names.end()); + auto contactTypeName = [](contact_t type) { + switch (type) { + case SPHERE_SPHERE_CONTACT: + return "SPHERE_SPHERE_CONTACT"; + case SPHERE_TRIANGLE_CONTACT: + return "SPHERE_TRIANGLE_CONTACT"; + case SPHERE_ANALYTICAL_CONTACT: + return "SPHERE_ANALYTICAL_CONTACT"; + case TRIANGLE_TRIANGLE_CONTACT: + return "TRIANGLE_TRIANGLE_CONTACT"; + case TRIANGLE_ANALYTICAL_CONTACT: + return "TRIANGLE_ANALYTICAL_CONTACT"; + case NOT_A_CONTACT: + return "NOT_A_CONTACT"; + default: + return "UNKNOWN_CONTACT"; + } + }; + auto geoTypeName = [](geoType_t type) { + switch (type) { + case GEO_T_SPHERE: + return "sphere"; + case GEO_T_TRIANGLE: + return "triangle"; + case GEO_T_ANALYTICAL: + return "analytical"; + default: + return "unknown"; + } + }; + auto safePatchOwner = [&](bodyID_t patchID, geoType_t type) { + if (patchID == NULL_BODYID) { + return NULL_BODYID; + } + switch (type) { + case GEO_T_SPHERE: + return patchID < ownerClumpBody.size() ? ownerClumpBody[patchID] : NULL_BODYID; + case GEO_T_TRIANGLE: + return patchID < ownerPatchMesh.size() ? ownerPatchMesh[patchID] : NULL_BODYID; + case GEO_T_ANALYTICAL: + return patchID < ownerAnalBody.size() ? ownerAnalBody[patchID] : NULL_BODYID; + default: + return NULL_BODYID; + } + }; + auto safeGeoOwner = [&](bodyID_t geoID, geoType_t type) { + if (geoID == NULL_BODYID) { + return NULL_BODYID; + } + switch (type) { + case GEO_T_SPHERE: + return geoID < ownerClumpBody.size() ? ownerClumpBody[geoID] : NULL_BODYID; + case GEO_T_TRIANGLE: + return geoID < ownerTriMesh.size() ? ownerTriMesh[geoID] : NULL_BODYID; + case GEO_T_ANALYTICAL: + return geoID < ownerAnalBody.size() ? ownerAnalBody[geoID] : NULL_BODYID; + default: + return NULL_BODYID; + } + }; + auto safeCombinedMaster = [&](bodyID_t ownerID) { + if (ownerID == NULL_BODYID || ownerID >= ownerCombinedMaster.size()) { + return NULL_BODYID; + } + return ownerCombinedMaster[ownerID]; + }; + auto printBodyID = [](std::ostream& os, bodyID_t id) -> std::ostream& { + if (id == NULL_BODYID) { + os << "NULL/invalid"; + } else { + os << id; + } + return os; + }; + auto printWildcardAverages = [&](const char* label, const std::vector& averages, size_t count) { + std::cout << "[DEME LOST CONTACT DEBUG] " << label << "ContactWildcardAverages(count=" << count << ")={"; + for (unsigned int wc = 0; wc < simParams->nContactWildcards; wc++) { + if (wc > 0) { + std::cout << ", "; + } + if (wc < wildcardNames.size()) { + std::cout << wildcardNames[wc] << "#" << wc << "=" << averages[wc]; + } else { + std::cout << "#" << wc << "=" << averages[wc]; + } + } + std::cout << "}\n"; + }; + auto unpackStoredDouble = [](const float3& storage) { + static_assert(sizeof(double) == 2 * sizeof(float), + "Double must be exactly twice the size of float for contact-geometry debug " + "unpacking."); + union { + double d; + float f[2]; + } converter; + converter.f[0] = storage.x; + converter.f[1] = storage.y; + return converter.d; + }; + + std::vector> oldWildcardValues(simParams->nContactWildcards); + std::vector oldWildcardAverages(simParams->nContactWildcards, 0.0); + std::vector migratedNewWildcardAverages(simParams->nContactWildcards, 0.0); + for (unsigned int wc = 0; wc < simParams->nContactWildcards; wc++) { + oldWildcardValues[wc].resize(nPrevContacts); + DEME_GPU_CALL(cudaMemcpy(oldWildcardValues[wc].data(), granData->contactWildcards[wc], + nPrevContacts * sizeof(float), cudaMemcpyDeviceToHost)); + double oldSum = 0.0; + for (float val : oldWildcardValues[wc]) { + oldSum += val; + } + if (nPrevContacts > 0) { + oldWildcardAverages[wc] = oldSum / static_cast(nPrevContacts); + } + + if (nNewContacts > 0) { + std::vector migratedValues(nNewContacts); + DEME_GPU_CALL(cudaMemcpy(migratedValues.data(), newWildcards[wc], nNewContacts * sizeof(float), + cudaMemcpyDeviceToHost)); + double migratedSum = 0.0; + for (float val : migratedValues) { + migratedSum += val; + } + migratedNewWildcardAverages[wc] = migratedSum / static_cast(nNewContacts); + } + } + + std::cout << "\n[DEME LOST CONTACT DEBUG] " << lostContactCount + << " old live patch contact(s) had no migration partner at sim time " << simParams->dyn.timeElapsed + << ". This detailed dump is DEBUG-verbosity only.\n"; + std::cout << "[DEME LOST CONTACT DEBUG] oldPatchContacts=" << nPrevContacts << ", newPatchContacts=" << nNewContacts + << ", oldPrimitiveContacts=" << nPrevPrimitiveContacts + << ", newPrimitiveContacts=" << nNewPrimitiveContacts + << ", dTStamp=" << pSchedSupport->currentStampOfDynamic.load() + << ", lastKinematicIngredStamp=" << pSchedSupport->stampLastDynamicUpdateProdDate.load() + << ", async=" << solverFlags.isAsync << "\n"; + printWildcardAverages("oldAll", oldWildcardAverages, nPrevContacts); + printWildcardAverages("migratedNewAll", migratedNewWildcardAverages, nNewContacts); + + if (oldContactSnapshot.nPatchContacts != nPrevContacts || + oldContactSnapshot.nPrimitiveContacts != nPrevPrimitiveContacts) { + std::cout << "[DEME LOST CONTACT DEBUG] WARNING: old-contact snapshot size mismatch: " + << "snapshotPatch=" << oldContactSnapshot.nPatchContacts << ", sentryPatch=" << nPrevContacts + << ", snapshotPrimitive=" << oldContactSnapshot.nPrimitiveContacts + << ", expectedPrimitive=" << nPrevPrimitiveContacts << "\n"; + } + + for (size_t oldCnt = 0; oldCnt < sentryHost.size(); oldCnt++) { + if (!sentryHost[oldCnt]) { + continue; + } + bool havePatchIdentity = false; + contact_t patchType = NOT_A_CONTACT; + geoType_t patchTypeA = GEO_T_SPHERE; + geoType_t patchTypeB = GEO_T_SPHERE; + bodyID_t patchA = NULL_BODYID; + bodyID_t patchB = NULL_BODYID; + bodyID_t patchOwnerA = NULL_BODYID; + bodyID_t patchOwnerB = NULL_BODYID; + + std::cout << "[DEME LOST CONTACT DEBUG] lostOldPatchIndex=" << oldCnt; + if (oldCnt < oldContactSnapshot.contactTypePatch.size() && oldCnt < oldContactSnapshot.idPatchA.size() && + oldCnt < oldContactSnapshot.idPatchB.size() && oldCnt < oldContactSnapshot.contactPatchIsland.size()) { + havePatchIdentity = true; + patchType = oldContactSnapshot.contactTypePatch[oldCnt]; + patchTypeA = decodeTypeA(patchType); + patchTypeB = decodeTypeB(patchType); + patchA = oldContactSnapshot.idPatchA[oldCnt]; + patchB = oldContactSnapshot.idPatchB[oldCnt]; + patchOwnerA = safePatchOwner(patchA, patchTypeA); + patchOwnerB = safePatchOwner(patchB, patchTypeB); + + std::cout << ", type=" << contactTypeName(patchType) << "(" << static_cast(patchType) << ")" + << ", patchA=" << patchA << "[" << geoTypeName(patchTypeA) << "]" + << ", patchB=" << patchB << "[" << geoTypeName(patchTypeB) << "]" + << ", ownerA="; + printBodyID(std::cout, patchOwnerA); + std::cout << ", ownerB="; + printBodyID(std::cout, patchOwnerB); + std::cout << ", combinedMasterA="; + printBodyID(std::cout, safeCombinedMaster(patchOwnerA)); + std::cout << ", combinedMasterB="; + printBodyID(std::cout, safeCombinedMaster(patchOwnerB)); + std::cout << ", patchIsland="; + printBodyID(std::cout, oldContactSnapshot.contactPatchIsland[oldCnt]); + } else { + std::cout << ", old patch identity unavailable"; + } + + std::cout << ", wildcards={"; + for (unsigned int wc = 0; wc < simParams->nContactWildcards; wc++) { + if (wc > 0) { + std::cout << ", "; + } + float wildcardVal = oldCnt < oldWildcardValues[wc].size() ? oldWildcardValues[wc][oldCnt] : 0.f; + if (wc < wildcardNames.size()) { + std::cout << wildcardNames[wc] << "#" << wc << "=" << wildcardVal; + } else { + std::cout << "#" << wc << "=" << wildcardVal; + } + } + std::cout << "}\n"; + + size_t primitiveContributors = 0; + size_t penetrationSamples = 0; + size_t positivePenetrationSamples = 0; + double minPenetration = 0.0; + double maxPenetration = 0.0; + double sumPenetration = 0.0; + double sumArea = 0.0; + double maxArea = 0.0; + for (size_t prim = 0; prim < oldContactSnapshot.geomToPatchMap.size(); prim++) { + if (oldContactSnapshot.geomToPatchMap[prim] != oldCnt) { + continue; + } + primitiveContributors++; + const bool hasPenetration = prim < oldContactSnapshot.primitivePenetrationStorage.size(); + const bool hasArea = prim < oldContactSnapshot.primitiveAreaStorage.size(); + double penetration = 0.0; + double area = 0.0; + if (hasPenetration) { + penetration = unpackStoredDouble(oldContactSnapshot.primitivePenetrationStorage[prim]); + if (penetrationSamples == 0) { + minPenetration = penetration; + maxPenetration = penetration; + } else { + minPenetration = std::min(minPenetration, penetration); + maxPenetration = std::max(maxPenetration, penetration); + } + sumPenetration += penetration; + penetrationSamples++; + if (penetration > 0.0) { + positivePenetrationSamples++; + } + } + if (hasArea) { + area = unpackStoredDouble(oldContactSnapshot.primitiveAreaStorage[prim]); + sumArea += area; + maxArea = std::max(maxArea, area); + } + if (prim < oldContactSnapshot.contactTypePrimitive.size() && + prim < oldContactSnapshot.idPrimitiveA.size() && prim < oldContactSnapshot.idPrimitiveB.size()) { + contact_t primType = oldContactSnapshot.contactTypePrimitive[prim]; + geoType_t primTypeA = decodeTypeA(primType); + geoType_t primTypeB = decodeTypeB(primType); + bodyID_t primA = oldContactSnapshot.idPrimitiveA[prim]; + bodyID_t primB = oldContactSnapshot.idPrimitiveB[prim]; + bodyID_t primOwnerA = safeGeoOwner(primA, primTypeA); + bodyID_t primOwnerB = safeGeoOwner(primB, primTypeB); + std::cout << "[DEME LOST CONTACT DEBUG] primitiveIndex=" << prim + << ", primitiveType=" << contactTypeName(primType) << "(" + << static_cast(primType) << ")" + << ", geomA=" << primA << "[" << geoTypeName(primTypeA) << "]" + << ", geomB=" << primB << "[" << geoTypeName(primTypeB) << "]" + << ", ownerA="; + printBodyID(std::cout, primOwnerA); + std::cout << ", ownerB="; + printBodyID(std::cout, primOwnerB); + std::cout << ", patchMap=" << oldContactSnapshot.geomToPatchMap[prim]; + } else { + std::cout << "[DEME LOST CONTACT DEBUG] primitiveIndex=" << prim + << ", primitive identity unavailable, patchMap=" << oldContactSnapshot.geomToPatchMap[prim]; + } + if (hasPenetration) { + std::cout << ", penetration=" << penetration; + } else { + std::cout << ", penetration=unavailable"; + } + if (hasArea) { + std::cout << ", area=" << area; + } else { + std::cout << ", area=unavailable"; + } + std::cout << "\n"; + } + if (primitiveContributors == 0) { + std::cout << "[DEME LOST CONTACT DEBUG] no old primitive contributors pointed to lost " + "patch index " + << oldCnt << "\n"; + } else if (penetrationSamples > 0) { + const double avgPenetration = sumPenetration / static_cast(penetrationSamples); + std::cout << "[DEME LOST CONTACT DEBUG] primitivePenetrationSummary(count=" << penetrationSamples + << ", positive=" << positivePenetrationSamples << ", min=" << minPenetration + << ", max=" << maxPenetration << ", avg=" << avgPenetration << ", areaSum=" << sumArea + << ", areaMax=" << maxArea << ")\n"; + } + } +} + +inline void DEMDynamicThread::migrateEnduringContacts(const LostContactDebugSnapshot& oldContactSnapshot) { // Use granData->contactMapping's information (stored in temp device vector) to map old and new contacts // All contact wildcards are the same type, so we can just allocate one temp array for all of them @@ -2075,37 +2902,19 @@ inline void DEMDynamicThread::migrateEnduringContacts() { // history array. This is just a quick and rough check: we only look at the last contact wildcard to see if it is // non-0, whatever it represents. size_t blocks_needed_for_rearrange; - if (verbosity >= VERBOSITY::STEP_METRIC) { + if (DEME_GET_VERBOSITY() >= VERBOSITY_METRIC) { if (*solverScratchSpace.numPrevContacts > 0) { - // DEME_GPU_CALL(cudaMemset(contactSentry, 0, sentry_bytes)); - blocks_needed_for_rearrange = - (*solverScratchSpace.numPrevContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_rearrange > 0) { - prep_force_kernels->kernel("markAliveContacts") - .instantiate() - .configure(dim3(blocks_needed_for_rearrange), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, - streamInfo.stream) - .launch(granData->contactWildcards[simParams->nContactWildcards - 1], contactSentry, - *solverScratchSpace.numPrevContacts); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } + markAliveContacts(granData->contactWildcards[simParams->nContactWildcards - 1], contactSentry, + *solverScratchSpace.numPrevContacts, streamInfo.stream); } } // Rearrange contact histories based on kT instruction - blocks_needed_for_rearrange = - (*solverScratchSpace.numContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_rearrange > 0) { - prep_force_kernels->kernel("rearrangeContactWildcards") - .instantiate() - .configure(dim3(blocks_needed_for_rearrange), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&granData, newWildcards[0], contactSentry, simParams->nContactWildcards, - *solverScratchSpace.numContacts); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } + rearrangeContactWildcards(&granData, newWildcards[0], contactSentry, simParams->nContactWildcards, + *solverScratchSpace.numContacts, streamInfo.stream); // Take a look, does the sentry indicate that there is an `alive' contact got lost? - if (verbosity >= VERBOSITY::STEP_METRIC) { + if (DEME_GET_VERBOSITY() >= VERBOSITY_METRIC) { if (*solverScratchSpace.numPrevContacts > 0 && simParams->nContactWildcards > 0) { // Temp DualStruct defaults to size_t type solverScratchSpace.allocateDualStruct("lostContact"); @@ -2114,28 +2923,16 @@ inline void DEMDynamicThread::migrateEnduringContacts() { streamInfo.stream, solverScratchSpace); solverScratchSpace.syncDualStructDeviceToHost("lostContact"); lostContact = solverScratchSpace.getDualStructHost("lostContact"); - if (*lostContact && solverFlags.isAsync) { - DEME_STEP_METRIC( - "%zu contacts were active at time %.9g on dT, but they are not detected on kT, therefore being " - "removed unexpectedly!", - *lostContact, simParams->timeElapsed); - DEME_STEP_DEBUG_PRINTF("New number of contacts: %zu", *solverScratchSpace.numContacts); - DEME_STEP_DEBUG_PRINTF("Old number of contacts: %zu", *solverScratchSpace.numPrevContacts); - DEME_STEP_DEBUG_PRINTF("New contact A:"); - DEME_STEP_DEBUG_EXEC( - displayDeviceArray(granData->idGeometryA, *solverScratchSpace.numContacts)); - DEME_STEP_DEBUG_PRINTF("New contact B:"); - DEME_STEP_DEBUG_EXEC( - displayDeviceArray(granData->idGeometryB, *solverScratchSpace.numContacts)); - DEME_STEP_DEBUG_PRINTF("Old version of the last contact wildcard:"); - DEME_STEP_DEBUG_EXEC(displayDeviceArray( - granData->contactWildcards[simParams->nContactWildcards - 1], *solverScratchSpace.numPrevContacts)); - DEME_STEP_DEBUG_PRINTF("Old--new mapping:"); - DEME_STEP_DEBUG_EXEC( - displayDeviceArray(granData->contactMapping, *solverScratchSpace.numContacts)); - DEME_STEP_DEBUG_PRINTF("Sentry:"); - DEME_STEP_DEBUG_EXEC( - displayDeviceArray(contactSentry, *solverScratchSpace.numPrevContacts)); + if (*lostContact) { + if (solverFlags.isAsync) { + // This prints when verbosity is METRIC or higher. Detailed records are DEBUG-only. + DEME_STATUS( + "ALIVE_CONTACT_NOT_DETECTED", + "%zu contacts were active at time %.9g on dT, but they are not detected on kT, therefore being " + "removed unexpectedly!", + *lostContact, simParams->dyn.timeElapsed); + } + reportLostContactDebugDetails(oldContactSnapshot, contactSentry, newWildcards, *lostContact); } solverScratchSpace.finishUsingDualStruct("lostContact"); } @@ -2156,88 +2953,390 @@ inline void DEMDynamicThread::migrateEnduringContacts() { solverScratchSpace.finishUsingTempVector("newWildcards"); solverScratchSpace.finishUsingTempVector("contactSentry"); - // granData may have changed in some of the earlier steps - granData.toDevice(); + // granData may have changed in some of the earlier steps. Queue the pointer-bundle refresh before the following + // kernels instead of forcing a host barrier here. + granData.toDeviceAsync(streamInfo.stream); } -inline void DEMDynamicThread::calculateForces() { +// The argument is two maps: contact type -> (start offset, count), contact type -> list of [(program bundle name, +// kernel name)] +inline void DEMDynamicThread::dispatchPrimitiveForceKernels( + const ContactTypeMap>& typeStartCountMap, + const ContactTypeMap, std::string>>>& + typeKernelMap) { + // For each contact type that exists, call its corresponding kernel(s) + for (size_t i = 0; i < m_numExistingTypes; i++) { + contact_t contact_type = existingContactTypes[i]; + const auto& start_count = typeStartCountMap.at(contact_type); + // Offset and count being contactPairs_t is very important, as CUDA kernel arguments cannot safely implicitly + // convert type (from size_t to unsigned int, for example) + contactPairs_t startOffset = start_count.first; + contactPairs_t count = start_count.second; + + // For this contact type, get its list of (program bundle name, kernel name) + if (typeKernelMap.count(contact_type) == 0) { + // displayDeviceArray(granData->idPrimitiveA, *solverScratchSpace.numPrimitiveContacts); + // displayDeviceArray(granData->idPrimitiveB, *solverScratchSpace.numPrimitiveContacts); + // displayDeviceArray(granData->contactTypePrimitive, *solverScratchSpace.numPrimitiveContacts); + // for (size_t j = 0; j < m_numExistingTypes; j++) { + // DEME_PRINTF("existingContactTypes[%zu] = %d\n", j, existingContactTypes[j]); + // } + DEME_ERROR("Contact type %d has no associated force kernel in to execute!", contact_type); + } + const auto& kernelList = typeKernelMap.at(contact_type); + for (const auto& [progName, kernelName] : kernelList) { + size_t blocks = (count + DT_FORCE_CALC_NTHREADS_PER_BLOCK - 1) / DT_FORCE_CALC_NTHREADS_PER_BLOCK; + if (blocks > 0) { + progName->kernel(kernelName) + .instantiate() + .configure(dim3(blocks), dim3(DT_FORCE_CALC_NTHREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, startOffset, count); + } + } + } + DEME_GPU_DEBUG_SYNC(streamInfo.stream); +} + +inline void DEMDynamicThread::dispatchPatchBasedForceCorrections( + const ContactTypeMap>& typeStartCountPrimitiveMap, + const ContactTypeMap>& typeStartCountPatchMap, + const ContactTypeMap, std::string>>>& + typeKernelMap) { + // For each contact type that exists, check if it is patch(mesh)-related type... + for (size_t i = 0; i < m_numExistingTypes; i++) { + contact_t contact_type = existingContactTypes[i]; + if (contact_type == SPHERE_TRIANGLE_CONTACT || contact_type == TRIANGLE_TRIANGLE_CONTACT || + contact_type == TRIANGLE_ANALYTICAL_CONTACT) { + const auto& start_count_primitive = typeStartCountPrimitiveMap.at(contact_type); + const auto& start_count_patch = typeStartCountPatchMap.at(contact_type); + contactPairs_t startOffsetPrimitive = start_count_primitive.first; + contactPairs_t countPrimitive = start_count_primitive.second; + contactPairs_t startOffsetPatch = start_count_patch.first; + contactPairs_t countPatch = start_count_patch.second; + + // Vote for the contact direction; voting power depends on the contact area. + // Multiple contact quantities (areas, penetrations, contact points) are computed by a single fused kernel, + // then reduced independently per contact type. + if (countPrimitive > 0) { + // geomToPatchMap maps each primitive contact to its patch pair. Contacts are type-sorted, so the + // segment beginning at startOffsetPrimitive can be used directly as the reduce key stream. + contactPairs_t* keys = granData->geomToPatchMap + startOffsetPrimitive; + + // Allocate arrays for reduce-by-key results (uniqueKeys uses contactPairs_t, not patchIDPair_t) + contactPairs_t* uniqueKeys = (contactPairs_t*)solverScratchSpace.allocateTempVector( + "uniqueKeys", countPrimitive * sizeof(contactPairs_t)); + solverScratchSpace.allocateDualStruct("numUniqueKeys"); + size_t* numUniqueKeys = solverScratchSpace.getDualStructDevice("numUniqueKeys"); + + // Step 1: Prepare weighted normals for voting (normal * area). + float3* weightedNormals = + (float3*)solverScratchSpace.allocateTempVector("weightedNormals", countPrimitive * sizeof(float3)); + prepareWeightedNormalsForVoting(&granData, weightedNormals, startOffsetPrimitive, countPrimitive, + streamInfo.stream); + + // Step 2: Reduce-by-key for weighted normals (sum) + // The keys are geomToPatchMap values (contactPairs_t), which group primitives by patch pair + float3* votedWeightedNormals = + (float3*)solverScratchSpace.allocateTempVector("votedWeightedNormals", countPatch * sizeof(float3)); + cubSumReduceByKey(keys, uniqueKeys, weightedNormals, votedWeightedNormals, + numUniqueKeys, countPrimitive, streamInfo.stream, + solverScratchSpace); + solverScratchSpace.finishUsingTempVector("weightedNormals"); + DEME_DEBUG_EXEC({ + solverScratchSpace.syncDualStructDeviceToHost("numUniqueKeys"); + size_t numUniqueKeysHost = *(solverScratchSpace.getDualStructHost("numUniqueKeys")); + if (numUniqueKeysHost != countPatch) { + DEME_ERROR( + "Patch-based contact voting produced %zu unique patch pairs, but expected %zu pairs for " + "contact type %d!", + numUniqueKeysHost, countPatch, contact_type); + } + }); + + // Step 3: Normalize the voted normals by total area and scatter back to a temp array. + float3* votedNormals = + (float3*)solverScratchSpace.allocateTempVector("votedNormals", countPatch * sizeof(float3)); + normalizeAndScatterVotedNormals(votedWeightedNormals, votedNormals, countPatch, streamInfo.stream); + solverScratchSpace.finishUsingTempVector("votedWeightedNormals"); + // displayDeviceFloat3(votedNormals, countPatch); + + // Step 4: Fused kernel: compute projected areas, projected penetrations, contact-point weights, and + // weighted contact points in one pass. + double* primitiveProjectedAreas = (double*)solverScratchSpace.allocateTempVector( + "primitiveProjectedAreas", countPrimitive * sizeof(double)); + double* primitiveProjectedPens = (double*)solverScratchSpace.allocateTempVector( + "primitiveProjectedPens", countPrimitive * sizeof(double)); + double* primitiveWeights = + (double*)solverScratchSpace.allocateTempVector("primitiveWeights", countPrimitive * sizeof(double)); + double3* primitiveWeightedCPs = (double3*)solverScratchSpace.allocateTempVector( + "primitiveWeightedCPs", countPrimitive * sizeof(double3)); + computePerPrimitiveWeightedQuantities( + &granData, votedNormals, keys, primitiveProjectedAreas, primitiveProjectedPens, primitiveWeights, + primitiveWeightedCPs, startOffsetPrimitive, startOffsetPatch, countPrimitive, streamInfo.stream); + + // Step 5a: Sum-reduce-by-key for total projected areas per patch. + double* totalProjAreas = + (double*)solverScratchSpace.allocateTempVector("totalProjAreas", countPatch * sizeof(double)); + cubSumReduceByKey(keys, uniqueKeys, primitiveProjectedAreas, totalProjAreas, + numUniqueKeys, countPrimitive, streamInfo.stream, + solverScratchSpace); + solverScratchSpace.finishUsingTempVector("primitiveProjectedAreas"); + + // Step 5b: Max-reduce-by-key for per-patch max projected penetration. + // finalPen is the max (deepest) projected penetration among all primitives in the patch. + double* maxProjPens = + (double*)solverScratchSpace.allocateTempVector("maxProjPens", countPatch * sizeof(double)); + cubMaxReduceByKey(keys, uniqueKeys, primitiveProjectedPens, maxProjPens, + numUniqueKeys, countPrimitive, streamInfo.stream, + solverScratchSpace); + solverScratchSpace.finishUsingTempVector("primitiveProjectedPens"); + + // Step 5c: Sum-reduce-by-key for total weights per patch (used for contact point averaging). + double* totalWeights = + (double*)solverScratchSpace.allocateTempVector("totalWeights", countPatch * sizeof(double)); + cubSumReduceByKey(keys, uniqueKeys, primitiveWeights, totalWeights, + numUniqueKeys, countPrimitive, streamInfo.stream, + solverScratchSpace); + + // Step 5d: Sum-reduce-by-key for total weighted contact points per patch. + double3* totalWeightedCPs = + (double3*)solverScratchSpace.allocateTempVector("totalWeightedCPs", countPatch * sizeof(double3)); + cubSumReduceByKey(keys, uniqueKeys, primitiveWeightedCPs, totalWeightedCPs, + numUniqueKeys, countPrimitive, streamInfo.stream, + solverScratchSpace); + solverScratchSpace.finishUsingTempVector("primitiveWeightedCPs"); + + // Step 6a: Extract primitive penetrations for the zero-area fallback (max-negative reduce). + double* primitivePenetrations = (double*)solverScratchSpace.allocateTempVector( + "primitivePenetrations", countPrimitive * sizeof(double)); + extractPrimitivePenetrations(&granData, primitivePenetrations, startOffsetPrimitive, countPrimitive, + streamInfo.stream); + + // Step 6b: Max-negative-reduce-by-key to get max negative penetration per patch. + // This finds the largest negative value (smallest absolute value among negatives). + // Positive values are treated as very negative to indicate invalid/non-physical state. + double* maxPenetrations = + (double*)solverScratchSpace.allocateTempVector("maxPenetrations", countPatch * sizeof(double)); + cubMaxNegativeReduceByKey(keys, uniqueKeys, primitivePenetrations, + maxPenetrations, numUniqueKeys, countPrimitive, + streamInfo.stream, solverScratchSpace); + solverScratchSpace.finishUsingTempVector("primitivePenetrations"); + + // Step 6c: Find max-penetration primitives for zero-area patches and extract their normals, + // penetrations, and contact points. + float3* zeroAreaNormals = + (float3*)solverScratchSpace.allocateTempVector("zeroAreaNormals", countPatch * sizeof(float3)); + double* zeroAreaPenetrations = + (double*)solverScratchSpace.allocateTempVector("zeroAreaPenetrations", countPatch * sizeof(double)); + double3* zeroAreaContactPoints = (double3*)solverScratchSpace.allocateTempVector( + "zeroAreaContactPoints", countPatch * sizeof(double3)); + findMaxPenetrationPrimitiveForZeroAreaPatches( + &granData, maxPenetrations, zeroAreaNormals, zeroAreaPenetrations, zeroAreaContactPoints, keys, + startOffsetPrimitive, startOffsetPatch, countPrimitive, streamInfo.stream); + solverScratchSpace.finishUsingTempVector("maxPenetrations"); + + // Clean up: uniqueKeys and numUniqueKeys are no longer needed after all reductions. + // keys is a direct pointer into granData->geomToPatchMap and does not need cleanup. + solverScratchSpace.finishUsingTempVector("uniqueKeys"); + solverScratchSpace.finishUsingDualStruct("numUniqueKeys"); + + // Step 7: Finalize patch results. + // finalPen = max projected penetration (maxProjPens), not an average. + // finalCP = weight-averaged contact point (weight = projArea * projPen). + // Zero-area patches use the max-penetration primitive's fallback values. + double* finalAreas = + (double*)solverScratchSpace.allocateTempVector("finalAreas", countPatch * sizeof(double)); + float3* finalNormals = + (float3*)solverScratchSpace.allocateTempVector("finalNormals", countPatch * sizeof(float3)); + // Resize permanent finalPenetrations array for this patch contact batch. + // Note: I made it a permanent array in case that in the future, we want to transfer this entire array + // to kT for better margin derivation. + DEME_DEVICE_ARRAY_RESIZE(finalPenetrations, countPatch); + + double3* finalContactPoints = + (double3*)solverScratchSpace.allocateTempVector("finalContactPoints", countPatch * sizeof(double3)); + finalizePatchResults(totalProjAreas, maxProjPens, totalWeights, votedNormals, totalWeightedCPs, + zeroAreaNormals, zeroAreaPenetrations, zeroAreaContactPoints, finalAreas, + finalNormals, finalPenetrations.data(), finalContactPoints, countPatch, + streamInfo.stream); + solverScratchSpace.finishUsingTempVector("zeroAreaNormals"); + solverScratchSpace.finishUsingTempVector("zeroAreaPenetrations"); + solverScratchSpace.finishUsingTempVector("zeroAreaContactPoints"); + solverScratchSpace.finishUsingTempVector("totalProjAreas"); + solverScratchSpace.finishUsingTempVector("maxProjPens"); + solverScratchSpace.finishUsingTempVector("totalWeightedCPs"); + + // Now we have: + // - finalAreas: final contact area per patch pair (countPatch elements) + // - finalNormals: final normal direction per patch pair (countPatch elements) + // - finalPenetrations: final penetration depth per patch pair (countPatch elements) + // - finalContactPoints: final contact point per patch pair (countPatch elements) + // These can be used for subsequent force calculations + // std::cout << "Patch-based contact penetration, area, normal, contact point for contact type " + // << (int)contact_type << ":" << std::endl; + // displayDeviceArray(finalPenetrations.data(), countPatch); + // displayDeviceArray(finalAreas, countPatch); + // displayDeviceFloat3(finalNormals, countPatch); + // displayDeviceFloat3(finalContactPoints, countPatch); + + // Call specialized patch-based force correction kernels here + if (contactTypePatchKernelMap.count(contact_type) > 0) { + const auto& kernelList = contactTypePatchKernelMap.at(contact_type); + for (const auto& [progName, kernelName] : kernelList) { + size_t blocks = + (countPatch + DT_FORCE_CALC_NTHREADS_PER_BLOCK - 1) / DT_FORCE_CALC_NTHREADS_PER_BLOCK; + if (blocks > 0) { + progName->kernel(kernelName) + .instantiate() + .configure(dim3(blocks), dim3(DT_FORCE_CALC_NTHREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, finalAreas, finalNormals, finalPenetrations.data(), + finalContactPoints, startOffsetPatch, countPatch); + } + } + } else { + DEME_ERROR("Patch contact type %d has no associated force kernel to execute!", contact_type); + } + DEME_GPU_DEBUG_SYNC(streamInfo.stream); + + // Optional per-triangle wear/tracking diagnostics. This runs after patch force correction so + // granData->contactForces contains patch-level forces, while primitiveWeights/totalWeights still + // describe how each primitive contributed to its patch contact. + if (triPVTrackingEnabled && triPVNumTrackedTriangles > 0) { + float* patchNormalForce = + (float*)solverScratchSpace.allocateTempVector("patchNormalForce", countPatch * sizeof(float)); + float* patchSlipSpeed = + (float*)solverScratchSpace.allocateTempVector("patchSlipSpeed", countPatch * sizeof(float)); + computePatchPVScalars(&simParams, &granData, finalNormals, finalContactPoints, startOffsetPatch, + countPatch, patchNormalForce, patchSlipSpeed, streamInfo.stream); + accumulateTrianglePVFromPatchContacts( + &simParams, &granData, keys, primitiveWeights, totalWeights, patchNormalForce, patchSlipSpeed, + startOffsetPrimitive, startOffsetPatch, countPrimitive, triPVGlobalTriToLocal.device(), + triPVAccumP.device(), triPVAccumPV.device(), streamInfo.stream); + solverScratchSpace.finishUsingTempVector("patchNormalForce"); + solverScratchSpace.finishUsingTempVector("patchSlipSpeed"); + } + + // Final clean up + solverScratchSpace.finishUsingTempVector("totalWeights"); + solverScratchSpace.finishUsingTempVector("primitiveWeights"); + solverScratchSpace.finishUsingTempVector("votedNormals"); + solverScratchSpace.finishUsingTempVector("finalAreas"); + solverScratchSpace.finishUsingTempVector("finalNormals"); + // Note: finalPenetrations is now a permanent array, not freed here + solverScratchSpace.finishUsingTempVector("finalContactPoints"); + } + } + } + // std::cout << "===========================" << std::endl; +} + +void DEMDynamicThread::calculateForces() { // Reset force (acceleration) arrays for this time step size_t nContactPairs = *solverScratchSpace.numContacts; + size_t nPrimitiveContactPairs = *solverScratchSpace.numPrimitiveContacts; + if (!simParams->meshParticlesLowPoly && simParams->nTriGM > 0) { + DEME_GPU_CALL(cudaMemset(maxTriTriPenetration.data(), 0, (size_t)simParams->nTriGM * sizeof(float))); + } - timers.GetTimer("Clear force array").start(); + timers.StartGpuTimer("Clear force array", streamInfo.stream); { - size_t blocks_needed_for_force_prep = - (nContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - size_t blocks_needed_for_acc_prep = - (simParams->nOwnerBodies + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - - prep_force_kernels->kernel("prepareAccArrays") - .instantiate() - .configure(dim3(blocks_needed_for_acc_prep), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + prepareAccArrays(&simParams, &granData, simParams->nOwnerBodies, streamInfo.stream); - // prepareForceArrays needs to clear contact force arrays, only if the user asks us to record contact forces. - // So... - if (!solverFlags.useNoContactRecord) { - prep_force_kernels->kernel("prepareForceArrays") - .instantiate() - .configure(dim3(blocks_needed_for_force_prep), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData, nContactPairs); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } + // prepareForceArrays is no longer needed + // if (!solverFlags.useNoContactRecord) { + // // Pay attention that the force result-related arrays have nPrimitiveContactPairs elements, not + // // nContactPairs + // prepareForceArrays(&simParams, &granData, nPrimitiveContactPairs, streamInfo.stream); + // } } - timers.GetTimer("Clear force array").stop(); + timers.StopGpuTimer("Clear force array", streamInfo.stream); - size_t blocks_needed_for_contacts = - (nContactPairs + DT_FORCE_CALC_NTHREADS_PER_BLOCK - 1) / DT_FORCE_CALC_NTHREADS_PER_BLOCK; // If no contact then we don't have to calculate forces. Note there might still be forces, coming from prescription // or other sources. - if (blocks_needed_for_contacts > 0) { - timers.GetTimer("Calculate contact forces").start(); - // a custom kernel to compute forces - cal_force_kernels->kernel("calculateContactForces") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DT_FORCE_CALC_NTHREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData, nContactPairs); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (nContactPairs > 0) { + timers.StartGpuTimer("Calculate contact forces", streamInfo.stream); + + // Call specialized kernels for each contact type that exists + dispatchPrimitiveForceKernels(typeStartCountPrimitiveMap, contactTypePrimitiveKernelMap); + // Note: dispatchPrimitiveForceKernels calculates forces induced by the most basic primitives, aka spheres, + // triangles... However, for contacts to be truely physical, sometimes such contact pairs within a patch (which + // marks a convex component of a owner) need to vote to decide the true contact. This is where the second step + // comes in. + dispatchPatchBasedForceCorrections(typeStartCountPrimitiveMap, typeStartCountPatchMap, + contactTypePatchKernelMap); + // displayDeviceFloat3(granData->contactForces, nContactPairs); - // displayDeviceArray(granData->contactType, nContactPairs); + // displayDeviceArray(granData->contactTypePatch, nContactPairs); + // displayDeviceArray(granData->idPatchA, nContactPairs); + // displayDeviceArray(granData->idPatchB, nContactPairs); // std::cout << "===========================" << std::endl; - timers.GetTimer("Calculate contact forces").stop(); + timers.StopGpuTimer("Calculate contact forces", streamInfo.stream); if (!solverFlags.useForceCollectInPlace) { - timers.GetTimer("Optional force reduction").start(); + timers.StartGpuTimer("Optional force reduction", streamInfo.stream); // Reflect those body-wise forces on their owner clumps - if (solverFlags.useCubForceCollect) { - collectContactForcesThruCub(collect_force_kernels, granData, nContactPairs, simParams->nOwnerBodies, - contactPairArr_isFresh, streamInfo.stream, solverScratchSpace, timers); - } else { - blocks_needed_for_contacts = - (nContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - // This does both acc and ang acc - collect_force_kernels->kernel("forceToAcc") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&granData, nContactPairs); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } + size_t blocks_needed_for_contacts = + (nContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + // This does both acc and ang acc + collect_force_kernels->kernel("forceToAcc") + .instantiate() + .configure(dim3(blocks_needed_for_contacts), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, nContactPairs); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); // displayDeviceArray(granData->aZ, simParams->nOwnerBodies); // displayDeviceFloat3(granData->contactForces, nContactPairs); // std::cout << nContactPairs << std::endl; - timers.GetTimer("Optional force reduction").stop(); + timers.StopGpuTimer("Optional force reduction", streamInfo.stream); } } + + if (simParams->nCombinedOwners > 0) { + // Fold non-master member accelerations into their master before integration. This also covers prescribed + // accelerations in no-contact steps because it runs outside the contact-count branch. + timers.StartGpuTimer("Optional force reduction", streamInfo.stream); + constexpr unsigned int COMBINED_OWNER_AGGREGATION_BLOCK = 512; + const size_t blocks_needed_for_owners = + (simParams->nOwnerBodies + COMBINED_OWNER_AGGREGATION_BLOCK - 1) / COMBINED_OWNER_AGGREGATION_BLOCK; + collect_force_kernels->kernel("aggregateCombinedOwnersAcc") + .instantiate() + .configure(dim3(blocks_needed_for_owners), dim3(COMBINED_OWNER_AGGREGATION_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, simParams->nOwnerBodies); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); + timers.StopGpuTimer("Optional force reduction", streamInfo.stream); + } + + finalizeTrianglePVWindowStep(); +} + +void DEMDynamicThread::finalizeTrianglePVWindowStep() { + if (!triPVTrackingEnabled || triPVNumTrackedTriangles == 0) { + return; + } + triPVWindowSteps++; } inline void DEMDynamicThread::integrateOwnerMotions() { + timers.StartGpuTimer("Integration", streamInfo.stream); size_t blocks_needed_for_clumps = (simParams->nOwnerBodies + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; integrator_kernels->kernel("integrateOwners") .instantiate() .configure(dim3(blocks_needed_for_clumps), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + .launch(&simParams, &granData, (double)simParams->dyn.timeElapsed); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); + + if (simParams->nCombinedOwners > 0) { + // Re-impose each non-master member from the integrated master state and its fixed relative transform. + constexpr unsigned int COMBINED_OWNER_REIMPOSITION_BLOCK = 512; + const size_t blocks_needed_for_owners = + (simParams->nOwnerBodies + COMBINED_OWNER_REIMPOSITION_BLOCK - 1) / COMBINED_OWNER_REIMPOSITION_BLOCK; + collect_force_kernels->kernel("reimposeCombinedOwners") + .instantiate() + .configure(dim3(blocks_needed_for_owners), dim3(COMBINED_OWNER_REIMPOSITION_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, simParams->nOwnerBodies); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); + } + timers.StopGpuTimer("Integration", streamInfo.stream); } inline void DEMDynamicThread::routineChecks() { @@ -2248,15 +3347,49 @@ inline void DEMDynamicThread::routineChecks() { .instantiate() .configure(dim3(blocks_needed_for_clumps), dim3(DEME_NUM_MODERATORS_PER_BLOCK), 0, streamInfo.stream) .launch(&simParams, &granData, simParams->nOwnerBodies); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); } } -inline float* DEMDynamicThread::determineSysVel() { - return approxMaxVelFunc->dT_GetDeviceValue(); +inline void DEMDynamicThread::determineSysVel() { + // Get linear velocity + pCycleVel = approxVelFunc->dT_GetDeviceValues(); + // Get angular velocity magnitude + pCycleAngVel = approxAngVelFunc->dT_GetDeviceValues(); } inline void DEMDynamicThread::unpack_impl() { + LostContactDebugSnapshot oldContactSnapshot; + if (!solverFlags.isHistoryless && DEME_GET_VERBOSITY() >= VERBOSITY_DEBUG) { + oldContactSnapshot.nPatchContacts = *solverScratchSpace.numContacts; + oldContactSnapshot.nPrimitiveContacts = *solverScratchSpace.numPrimitiveContacts; + + // kT unpack overwrites dT's contact identity arrays before history migration checks the old wildcard sentry. + // DEBUG lost-contact diagnostics therefore snapshot the old identities before receiving the new contact arrays. + // The sync is DEBUG-only: it makes the host-side snapshot a reliable diagnostic without changing normal runs. + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + auto copySnapshot = [](auto& dst, auto* src, size_t count) { + dst.resize(count); + if (count > 0) { + DEME_GPU_CALL(cudaMemcpy(dst.data(), src, count * sizeof(*dst.data()), cudaMemcpyDeviceToHost)); + } + }; + copySnapshot(oldContactSnapshot.idPatchA, idPatchA.data(), oldContactSnapshot.nPatchContacts); + copySnapshot(oldContactSnapshot.idPatchB, idPatchB.data(), oldContactSnapshot.nPatchContacts); + copySnapshot(oldContactSnapshot.contactTypePatch, contactTypePatch.data(), oldContactSnapshot.nPatchContacts); + copySnapshot(oldContactSnapshot.contactPatchIsland, contactPatchIsland.data(), + oldContactSnapshot.nPatchContacts); + copySnapshot(oldContactSnapshot.idPrimitiveA, idPrimitiveA.data(), oldContactSnapshot.nPrimitiveContacts); + copySnapshot(oldContactSnapshot.idPrimitiveB, idPrimitiveB.data(), oldContactSnapshot.nPrimitiveContacts); + copySnapshot(oldContactSnapshot.contactTypePrimitive, contactTypePrimitive.data(), + oldContactSnapshot.nPrimitiveContacts); + copySnapshot(oldContactSnapshot.geomToPatchMap, geomToPatchMap.data(), oldContactSnapshot.nPrimitiveContacts); + copySnapshot(oldContactSnapshot.primitivePenetrationStorage, contactPointGeometryA.data(), + oldContactSnapshot.nPrimitiveContacts); + copySnapshot(oldContactSnapshot.primitiveAreaStorage, contactPointGeometryB.data(), + oldContactSnapshot.nPrimitiveContacts); + } + { // Acquire lock and use the content of the dynamic-owned transfer buffer std::lock_guard lock(pSchedSupport->dynamicOwnedBuffer_AccessCoordination); @@ -2266,10 +3399,18 @@ inline void DEMDynamicThread::unpack_impl() { // pSchedSupport->schedulingStats.nDynamicReceives++; } // dT got the produce, now mark its buffer to be no longer fresh. - pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh = false; + pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.store(false, std::memory_order_release); // Used for inspecting on average how stale kT's produce is. - pSchedSupport->schedulingStats.accumKinematicLagSteps += - (pSchedSupport->currentStampOfDynamic).load() - (pSchedSupport->stampLastDynamicUpdateProdDate).load(); + const int64_t current_dynamic_stamp = (pSchedSupport->currentStampOfDynamic).load(); + const int64_t previous_contact_prod_stamp = (pSchedSupport->stampLastDynamicUpdateProdDate).load(); + pSchedSupport->schedulingStats.accumKinematicLagSteps += current_dynamic_stamp - previous_contact_prod_stamp; + if (previous_contact_prod_stamp >= 0 && current_dynamic_stamp >= previous_contact_prod_stamp) { + // The contact set just replaced had to cover this many dT steps. This is the safest signal for future-drift + // tuning because it measures the actual two-thread handoff, not just kT's isolated runtime. + const int64_t covered_drift = current_dynamic_stamp - previous_contact_prod_stamp; + futureDriftRegulator.RecordContactSetCoverage(static_cast( + std::min(covered_drift, static_cast(solverFlags.upperBoundFutureDrift)))); + } // dT needs to know how fresh the contact pair info is, and that is determined by when kT received this batch of // ingredients. pSchedSupport->stampLastDynamicUpdateProdDate = (pSchedSupport->kinematicIngredProdDateStamp).load(); @@ -2277,87 +3418,81 @@ inline void DEMDynamicThread::unpack_impl() { // If this is a history-based run, then when contacts are received, we need to migrate the contact // history info, to match the structure of the new contact array if (!solverFlags.isHistoryless) { - migrateEnduringContacts(); + migrateEnduringContacts(oldContactSnapshot); } // With unpacking finished, contactMapping temp array is no longer needed solverScratchSpace.finishUsingTempVector("contactMapping"); + + // On dT side, we also calculate how many (the offsets in contact arrays) contacts they are for each type. + // But note here we are working on primitive-based contact types, not patch-based contact types yet. + solverScratchSpace.allocateDualStruct("numExistingTypes"); + contactPairs_t* typeCounts = (contactPairs_t*)solverScratchSpace.allocateTempVector( + "typeCounts", (NUM_SUPPORTED_CONTACT_TYPES + 1) * sizeof(contactPairs_t)); + // Recall existingContactTypes is pre-allocated for maximum possible types + cubRunLengthEncode( + granData->contactTypePrimitive, existingContactTypes.device(), typeCounts, + solverScratchSpace.getDualStructDevice("numExistingTypes"), *solverScratchSpace.numPrimitiveContacts, + streamInfo.stream, solverScratchSpace); + solverScratchSpace.syncDualStructDeviceToHost("numExistingTypes"); + m_numExistingTypes = *solverScratchSpace.getDualStructHost("numExistingTypes"); + cubPrefixScan(typeCounts, typeStartOffsetsPrimitive.device(), m_numExistingTypes, + streamInfo.stream, solverScratchSpace); + existingContactTypes.toHost(); + typeStartOffsetsPrimitive.toHost(); + typeStartCountPrimitiveMap.SetAll({0, 0}); + for (size_t i = 0; i < m_numExistingTypes; i++) { + DEME_DEBUG_PRINTF("Contact type %d starts at offset %u", existingContactTypes[i], typeStartOffsetsPrimitive[i]); + typeStartCountPrimitiveMap[existingContactTypes[i]] = + std::make_pair(typeStartOffsetsPrimitive[i], + (i + 1 < m_numExistingTypes ? typeStartOffsetsPrimitive[i + 1] + : (contactPairs_t)*solverScratchSpace.numPrimitiveContacts) - + typeStartOffsetsPrimitive[i]); + } + // Debug output of the map + // for (const auto& entry : typeStartCountPrimitiveMap) { + // printf("Contact type %d starts at offset %u and has count %u\n", entry.first, entry.second.first, + // entry.second.second); + // } + + // Now for patch-based contacts, we do the same thing. Note the unique types herein will be the same as thosein. + cubRunLengthEncode(granData->contactTypePatch, existingContactTypes.device(), typeCounts, + solverScratchSpace.getDualStructDevice("numExistingTypes"), + *solverScratchSpace.numContacts, streamInfo.stream, + solverScratchSpace); + cubPrefixScan(typeCounts, typeStartOffsetsPatch.device(), m_numExistingTypes, + streamInfo.stream, solverScratchSpace); + typeStartOffsetsPatch.toHost(); + typeStartCountPatchMap.SetAll({0, 0}); + for (size_t i = 0; i < m_numExistingTypes; i++) { + typeStartCountPatchMap[existingContactTypes[i]] = std::make_pair( + typeStartOffsetsPatch[i], (i + 1 < m_numExistingTypes ? typeStartOffsetsPatch[i + 1] + : (contactPairs_t)*solverScratchSpace.numContacts) - + typeStartOffsetsPatch[i]); + } + + solverScratchSpace.finishUsingTempVector("typeCounts"); + solverScratchSpace.finishUsingDualStruct("numExistingTypes"); } inline void DEMDynamicThread::ifProduceFreshThenUseIt() { - if (pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh) { + if (pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.load(std::memory_order_acquire)) { unpack_impl(); } } - -inline void DEMDynamicThread::checkStateIsFinite() { - // WHY THIS EXISTS, AND WHY IT IS HERE RATHER THAN IN kT. - // kT has two NaN tests already, and neither one can see angular velocity, because angular - // velocity never crosses to kT: it lives here on dT. kT is handed the translational speed - // array and the orientation quaternions, so no check placed in kT can cover this at any - // strength. That is the whole reason this function is on dT. - // The two kT tests, for the record, so nobody re-adds a third one there: - // 1. unpackMyBuffer() tests isfinite() on a max-reduced velocity. Weak even on the - // translational side: cub::DeviceReduce::Max compares with >, every comparison against - // NaN is false, so NaN operands are skipped and an all-NaN array reduces to the initial - // value -FLT_MAX, which is finite and passes. - // 2. computeMarginFromAbsv() does a per-element isfinite() with DEME_ABORT_KERNEL, which - // is strong, but it reads marginSize, i.e. translational speed. It is also skipped - // entirely when isExpandFactorFixed, since then the margin is not derived from speed. - // Measured on a settling bed with the pre-fix tree, 40 runs, automatic margin route active - // so test 2 was on duty throughout: three real corruptions, all three naming angular - // velocity (x) here, with translational speed still a healthy 0.570, 0.746 and 0.589 m/s and - // kinetic energy finite. Test 2 did not fire in any of those 40 runs, nor in 20 more. - // A SUM propagates NaN where a MAX does not, so probing each array with one is enough. The - // arrays are checked separately so the message can say which part of the state went bad. - const size_t n = simParams->nOwnerBodies; - const char* which = nullptr; - float* const arrays[4] = {pCycleVel, granData->omgBarX, granData->omgBarY, granData->omgBarZ}; - const char* const names[4] = {"velocity", "angular velocity (x)", "angular velocity (y)", - "angular velocity (z)"}; - for (int i = 0; i < 4; i++) { - if (!arrays[i]) - continue; - cubSumReduce(arrays[i], &(stateFiniteProbe), n, streamInfo.stream, solverScratchSpace); - stateFiniteProbe.toHost(); - if (!std::isfinite(*(stateFiniteProbe))) { - which = names[i]; - break; - } - } - if (which) { - DEME_ERROR( - "The system state is not finite: the %s array contains NaN or Inf.\nThis usually means the " - "simulation diverged. Decreasing the step size, softening material properties, or checking for " - "elements initialized inside walls may help.\nNote that a reported max velocity can look " - "entirely reasonable while this fires, because a max reduction cannot propagate NaN, and because " - "angular velocity is not part of that reduction at all.", - which); - } -} inline void DEMDynamicThread::calibrateParams() { // Unpacking is done; now we can use temp arrays again to derive max velocity and send to kT - pCycleVel = determineSysVel(); - - // Refuse to hand kT a corrupted state, and say so, rather than continuing silently. - checkStateIsFinite(); + determineSysVel(); // This will set pCycleVel and pCycleAngVel if (solverFlags.autoUpdateFreq) { - unsigned int comfortable_drift; - if (accumStepUpdater.Query(comfortable_drift)) { - // If perhapsIdealFutureDrift needs to increase, then the following value much = perhapsIdealFutureDrift. - comfortable_drift = - (float)comfortable_drift * solverFlags.targetDriftMultipleOfAvg + solverFlags.targetDriftMoreThanAvg; - if (*perhapsIdealFutureDrift > comfortable_drift) { - *perhapsIdealFutureDrift -= FUTURE_DRIFT_TWEAK_STEP_SIZE; - } else if (*perhapsIdealFutureDrift < comfortable_drift) { - *perhapsIdealFutureDrift += FUTURE_DRIFT_TWEAK_STEP_SIZE; - } - *perhapsIdealFutureDrift = clampBetween(*perhapsIdealFutureDrift, 0, - solverFlags.upperBoundFutureDrift); + if (futureDriftRegulator.HasContactSetCoverageSample()) { + *perhapsIdealFutureDrift = futureDriftRegulator.Recommend( + *perhapsIdealFutureDrift, solverFlags.targetDriftMultipleOfAvg, solverFlags.targetDriftMoreThanAvg, + solverFlags.upperBoundFutureDrift, FUTURE_DRIFT_TWEAK_STEP_SIZE); - DEME_DEBUG_PRINTF("Comfortable future drift is %u", comfortable_drift); + DEME_DEBUG_PRINTF("Observed covered future drift is %u", + futureDriftRegulator.LastObservedContactSetCoverage()); DEME_DEBUG_PRINTF("Current future drift is %u", *perhapsIdealFutureDrift); } } @@ -2365,7 +3500,7 @@ inline void DEMDynamicThread::calibrateParams() { } inline void DEMDynamicThread::ifProduceFreshThenUseItAndSendNewOrder() { - if (pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh) { + if (pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.load(std::memory_order_acquire)) { timers.GetTimer("Unpack updates from kT").start(); unpack_impl(); timers.GetTimer("Unpack updates from kT").stop(); @@ -2373,13 +3508,15 @@ inline void DEMDynamicThread::ifProduceFreshThenUseItAndSendNewOrder() { timers.GetTimer("Send to kT buffer").start(); // Acquire lock and refresh the work order for the kinematic { + // Scheduling safety invariant: consuming one kT product and posting the next work order stay coupled. The + // future-drift regulator may change the margin command, but it must not defer this send; deferring it lets + // dT and kT span multiple unmatched contact arrays and breaks contact-history migration. calibrateParams(); std::lock_guard lock(pSchedSupport->kinematicOwnedBuffer_AccessCoordination); sendToTheirBuffer(); } - pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh = true; + pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.store(true, std::memory_order_release); pSchedSupport->schedulingStats.nKinematicUpdates++; - accumStepUpdater.AddUpdate(); timers.GetTimer("Send to kT buffer").stop(); // Signal the kinematic that it has data for a new work order @@ -2437,20 +3574,19 @@ void DEMDynamicThread::workerThread() { // In this `new-boot' case, we send kT a work order, b/c dT needs results from CD to proceed. After this one // instance, kT and dT may work in an async fashion. { - pCycleVel = determineSysVel(); + determineSysVel(); // This will set pCycleVel and pCycleAngVel std::lock_guard lock(pSchedSupport->kinematicOwnedBuffer_AccessCoordination); sendToTheirBuffer(); } - pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh = true; + pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.store(true, std::memory_order_release); contactPairArr_isFresh = true; pSchedSupport->schedulingStats.nKinematicUpdates++; - accumStepUpdater.AddUpdate(); // Signal the kinematic that it has data for a new work order. pSchedSupport->cv_KinematicCanProceed.notify_all(); // Then dT will wait for kT to finish one initial run { std::unique_lock lock(pSchedSupport->dynamicCanProceed); - while (!pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh) { + while (!pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.load(std::memory_order_acquire)) { // loop to avoid spurious wakeups pSchedSupport->cv_DynamicCanProceed.wait(lock); } @@ -2464,7 +3600,7 @@ void DEMDynamicThread::workerThread() { } } - for (double cycle = 0.0; cycle < cycleDuration; cycle += (double)(simParams->h)) { + for (double cycle = 0.0; cycle < cycleDuration; cycle += (double)(simParams->dyn.h)) { // If the produce is fresh, use it, and then send kT a new work order. // We used to send work order to kT whenever kT unpacks its buffer. This can lead to a situation where dT // sends a new work order and then immediately bails out (user asks it to do something else). A bit later @@ -2480,14 +3616,16 @@ void DEMDynamicThread::workerThread() { timers.GetTimer("Wait for kT update").start(); // Wait for a signal from kT to indicate that kT has caught up std::unique_lock lock(pSchedSupport->dynamicCanProceed); - while (!pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh) { + while (!pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.load(std::memory_order_acquire)) { // Loop to avoid spurious wakeups pSchedSupport->cv_DynamicCanProceed.wait(lock); } pSchedSupport->schedulingStats.nTimesDynamicHeldBack++; // If dT waits, it is penalized, since waiting means double-wait, very bad. - if (solverFlags.autoUpdateFreq) - *perhapsIdealFutureDrift += FUTURE_DRIFT_TWEAK_STEP_SIZE; + if (solverFlags.autoUpdateFreq) { + *perhapsIdealFutureDrift = futureDriftRegulator.BumpAfterWait( + *perhapsIdealFutureDrift, solverFlags.upperBoundFutureDrift, FUTURE_DRIFT_TWEAK_STEP_SIZE); + } timers.GetTimer("Wait for kT update").stop(); } // NOTE: This ShouldWait check should follow the ifProduceFreshThenUseItAndSendNewOrder call. Because we @@ -2504,13 +3642,35 @@ void DEMDynamicThread::workerThread() { routineChecks(); - timers.GetTimer("Integration").start(); integrateOwnerMotions(); - timers.GetTimer("Integration").stop(); + + timers.AccumulateGpuTimer("Clear force array"); + timers.AccumulateGpuTimer("Calculate contact forces"); + timers.AccumulateGpuTimer("Optional force reduction"); + timers.AccumulateGpuTimer("Integration"); step_accepted = true; } while ((!solverFlags.isStepConst) || (!step_accepted)); + //// TODO: make changes for variable time step size cases + // Keep the host copy authoritative for API queries. integrateOwners also writes the same next time into + // device simParams so downstream stream work can observe the advanced time before this host-to-device + // refresh executes. + simParams->dyn.timeElapsed += (double)simParams->dyn.h; + simParams.syncMemberToDeviceAsync( + offsetof(DEMSimParams, dyn) + offsetof(DEMSimParamsDynamic, timeElapsed), streamInfo.stream); + + // Required step-completion barrier. + // + // dT/kT scheduling treats currentStampOfDynamic, nTotalSteps, and the kT handoff buffers as facts about + // completed dynamics steps. CUDA launches above are asynchronous, so without this barrier the CPU thread + // can advance those counters and hand stale positions/velocities to kT while the GPU is still finishing + // this step. That makes kT derive contact margins from an inflated future drift, which is especially + // dangerous for mesh contacts because enlarged triangle margins can explode the primitive contact count. + // + // This synchronization is therefore part of the solver's correctness contract, not debug error checking. + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + // CalculateForces is done, set contactPairArr_isFresh to false // This will be set to true next time it receives an update from kT contactPairArr_isFresh = false; @@ -2523,13 +3683,8 @@ void DEMDynamicThread::workerThread() { // Dynamic wrapped up one cycle, record this fact into schedule support pSchedSupport->currentStampOfDynamic++; nTotalSteps++; - accumStepUpdater.AddStep(); - //// TODO: make changes for variable time step size cases - simParams->timeElapsed += (double)simParams->h; - // timeElapsed needs to be updated to the device each time step - // simParams.syncMemberToDevice(offsetof(DEMSimParams, timeElapsed)); - simParams.toDevice(); + DEME_DEBUG_PRINTF("Completed step %zu, time %.9g", nTotalSteps, simParams->dyn.timeElapsed); } // Unless the user did something critical, must we wait for a kT update before next step @@ -2564,7 +3719,7 @@ void DEMDynamicThread::resetUserCallStat() { // Reset dT stats variables, making ready for next user call pSchedSupport->dynamicDone = false; contactPairArr_isFresh = true; - accumStepUpdater.Clear(); + futureDriftRegulator.Clear(); // Do not let user artificially set dynamicOwned_Prod2ConsBuffer_isFresh false. B/c only dT has the say on that. It // could be that kT has a new produce ready, but dT idled for long and do not want to use it and want a new produce. @@ -2583,47 +3738,92 @@ size_t DEMDynamicThread::estimateHostMemUsage() const { void DEMDynamicThread::jitifyKernels(const std::unordered_map& Subs, const std::vector& JitifyOptions) { - // First one is force array preparation kernels + // Force calculation kernels { - prep_force_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMPrepForceKernels", JitHelper::KERNEL_DIR / "DEMPrepForceKernels.cu", Subs, JitifyOptions))); + cal_force_kernels = std::make_shared(std::move( + JitHelper::buildProgram("DEMCalcForceKernels_Primitive", + JitHelper::KERNEL_DIR / "DEMCalcForceKernels_Primitive.cu", Subs, JitifyOptions))); } - // Then force calculation kernels + // Then patch-based force calculation kernels { - cal_force_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMCalcForceKernels", JitHelper::KERNEL_DIR / "DEMCalcForceKernels.cu", Subs, JitifyOptions))); + cal_patch_force_kernels = std::make_shared(std::move( + JitHelper::buildProgram("DEMCalcForceKernels_PatchBased", + JitHelper::KERNEL_DIR / "DEMCalcForceKernels_PatchBased.cu", Subs, JitifyOptions))); } // Then force accumulation kernels - if (solverFlags.useCubForceCollect) { - collect_force_kernels = std::make_shared(std::move(JitHelper::buildProgram( + { + collect_force_kernels = std::make_shared(std::move(JitHelper::buildProgram( "DEMCollectForceKernels", JitHelper::KERNEL_DIR / "DEMCollectForceKernels.cu", Subs, JitifyOptions))); - } else { - collect_force_kernels = std::make_shared(std::move( - JitHelper::buildProgram("DEMCollectForceKernels_Compact", - JitHelper::KERNEL_DIR / "DEMCollectForceKernels_Compact.cu", Subs, JitifyOptions))); } // Then integration kernels { - integrator_kernels = std::make_shared(std::move(JitHelper::buildProgram( + integrator_kernels = std::make_shared(std::move(JitHelper::buildProgram( "DEMIntegrationKernels", JitHelper::KERNEL_DIR / "DEMIntegrationKernels.cu", Subs, JitifyOptions))); } - // Then kernels that are... wildcards, which make on-the-fly changes to solver data - if (solverFlags.canFamilyChangeOnDevice) { - mod_kernels = std::make_shared(std::move(JitHelper::buildProgram( + // Then kernels that make on-the-fly changes to solver data + { + mod_kernels = std::make_shared(std::move(JitHelper::buildProgram( "DEMModeratorKernels", JitHelper::KERNEL_DIR / "DEMModeratorKernels.cu", Subs, JitifyOptions))); } - // Then misc kernels - { - misc_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMMiscKernels", JitHelper::KERNEL_DIR / "DEMMiscKernels.cu", Subs, JitifyOptions))); + + // For now, the contact type to kernel map is known and hard-coded after jitification + contactTypePrimitiveKernelMap[SPHERE_SPHERE_CONTACT] = { + {cal_force_kernels, "calculatePrimitiveContactForces_SphSph"}}; + contactTypePrimitiveKernelMap[SPHERE_TRIANGLE_CONTACT] = { + {cal_force_kernels, "calculatePrimitiveContactForces_SphTri"}}; + contactTypePrimitiveKernelMap[SPHERE_ANALYTICAL_CONTACT] = { + {cal_force_kernels, "calculatePrimitiveContactForces_SphAnal"}}; + contactTypePrimitiveKernelMap[TRIANGLE_TRIANGLE_CONTACT] = { + {cal_force_kernels, "calculatePrimitiveContactForces_TriTri"}}; + contactTypePrimitiveKernelMap[TRIANGLE_ANALYTICAL_CONTACT] = { + {cal_force_kernels, "calculatePrimitiveContactForces_TriAnal"}}; + + // Patch-based force kernel map for mesh-related contacts + contactTypePatchKernelMap[SPHERE_TRIANGLE_CONTACT] = { + {cal_patch_force_kernels, "calculatePatchContactForces_SphTri"}}; + contactTypePatchKernelMap[TRIANGLE_TRIANGLE_CONTACT] = { + {cal_patch_force_kernels, "calculatePatchContactForces_TriTri"}}; + contactTypePatchKernelMap[TRIANGLE_ANALYTICAL_CONTACT] = { + {cal_patch_force_kernels, "calculatePatchContactForces_TriAnal"}}; + prewarmKernels(); +} + +void DEMDynamicThread::prewarmKernels() { + // Prewarm force, integration, and moderation kernels so cached JIT artifacts are loaded before the first dynamics + // step. This is especially noticeable for mesh contacts, whose primitive and patch kernels may otherwise compile at + // first contact. + if (cal_force_kernels) { + cal_force_kernels->kernel("calculatePrimitiveContactForces_SphSph").instantiate(); + cal_force_kernels->kernel("calculatePrimitiveContactForces_SphTri").instantiate(); + cal_force_kernels->kernel("calculatePrimitiveContactForces_SphAnal").instantiate(); + cal_force_kernels->kernel("calculatePrimitiveContactForces_TriTri").instantiate(); + cal_force_kernels->kernel("calculatePrimitiveContactForces_TriAnal").instantiate(); + } + if (cal_patch_force_kernels) { + cal_patch_force_kernels->kernel("calculatePatchContactForces_SphTri").instantiate(); + cal_patch_force_kernels->kernel("calculatePatchContactForces_TriTri").instantiate(); + cal_patch_force_kernels->kernel("calculatePatchContactForces_TriAnal").instantiate(); + } + if (collect_force_kernels) { + collect_force_kernels->kernel("forceToAcc").instantiate(); + collect_force_kernels->kernel("aggregateCombinedOwnersAcc").instantiate(); + collect_force_kernels->kernel("reimposeCombinedOwners").instantiate(); + } + if (integrator_kernels) { + integrator_kernels->kernel("integrateOwners").instantiate(); + } + if (mod_kernels && solverFlags.canFamilyChangeOnDevice) { + mod_kernels->kernel("applyFamilyChanges").instantiate(); } } -float* DEMDynamicThread::inspectCall(const std::shared_ptr& inspection_kernel, +float* DEMDynamicThread::inspectCall(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes, bool return_device_ptr) { size_t n; ownerType_t owner_type = 0; @@ -2647,8 +3847,8 @@ float* DEMDynamicThread::inspectCall(const std::shared_ptr& // We can use temp vectors as we please size_t quarryTempSize = n * sizeof(float); - DEME_DUAL_ARRAY_RESIZE_NOVAL(m_reduceResArr, quarryTempSize); - float* resArr = (float*)m_reduceResArr.device(); + DEME_DUAL_ARRAY_RESIZE_NOVAL(reduceResArr, quarryTempSize); + float* resArr = (float*)reduceResArr.device(); size_t regionTempSize = n * sizeof(notStupidBool_t); // If this boolArrExclude is 1 at an element, that means this element is exluded in the reduction notStupidBool_t* boolArrExclude = @@ -2657,14 +3857,14 @@ float* DEMDynamicThread::inspectCall(const std::shared_ptr& // We may actually have 2 reduced returns: in regional reduction, key 0 and 1 give one return each. size_t returnSize = sizeof(float) * 2; - DEME_DUAL_ARRAY_RESIZE_NOVAL(m_reduceRes, returnSize); - float* res = (float*)m_reduceRes.device(); + DEME_DUAL_ARRAY_RESIZE_NOVAL(reduceRes, returnSize); + float* res = (float*)reduceRes.device(); size_t blocks_needed = (n + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; inspection_kernel->kernel(kernel_name) .instantiate() .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) .launch(&granData, &simParams, resArr, boolArrExclude, n, owner_type); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + DEME_GPU_DEBUG_SYNC(streamInfo.stream); if (all_domain) { switch (reduce_flavor) { @@ -2680,10 +3880,10 @@ float* DEMDynamicThread::inspectCall(const std::shared_ptr& case (CUB_REDUCE_FLAVOR::NONE): solverScratchSpace.finishUsingTempVector("boolArrExclude"); if (return_device_ptr) { - return (float*)m_reduceResArr.device(); + return (float*)reduceResArr.device(); } else { - m_reduceResArr.toHost(); - return (float*)m_reduceResArr.host(); + reduceResArr.toHost(); + return (float*)reduceResArr.host(); } } // If this inspection is comfined in a region, then boolArrExclude and resArr need to be sorted and reduce by @@ -2722,10 +3922,10 @@ float* DEMDynamicThread::inspectCall(const std::shared_ptr& solverScratchSpace.finishUsingTempVector("resArr_sorted"); solverScratchSpace.finishUsingTempVector("num_unique_out"); if (return_device_ptr) { - return (float*)m_reduceResArr.device(); + return (float*)reduceResArr.device(); } else { - m_reduceResArr.toHost(); - return (float*)m_reduceResArr.host(); + reduceResArr.toHost(); + return (float*)reduceResArr.host(); } } } @@ -2735,13 +3935,24 @@ float* DEMDynamicThread::inspectCall(const std::shared_ptr& solverScratchSpace.finishUsingTempVector("resArr_sorted"); solverScratchSpace.finishUsingTempVector("num_unique_out"); if (return_device_ptr) { - return (float*)m_reduceRes.device(); + return (float*)reduceRes.device(); } else { - m_reduceRes.toHost(); - return (float*)m_reduceRes.host(); + reduceRes.toHost(); + return (float*)reduceRes.host(); } } +float* DEMDynamicThread::inspectCallDeviceNoReduce(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes) { + return inspectCall(inspection_kernel, kernel_name, thing_to_insp, reduce_flavor, all_domain, reduceResArr, + reduceRes, true); +} + void DEMDynamicThread::initAllocation() { DEME_DUAL_ARRAY_RESIZE(familyExtraMarginSize, NUM_AVAL_FAMILIES, 0); } @@ -2769,13 +3980,13 @@ size_t DEMDynamicThread::getNumContacts() const { } double DEMDynamicThread::getSimTime() const { - return simParams->timeElapsed; + return simParams->dyn.timeElapsed; } void DEMDynamicThread::setSimTime(double time) { - simParams->timeElapsed = time; - // simParams.syncMemberToDevice(offsetof(DEMSimParams, timeElapsed)); - simParams.toDevice(); + simParams->dyn.timeElapsed = time; + simParams.syncMemberToDeviceAsync(offsetof(DEMSimParams, dyn) + offsetof(DEMSimParamsDynamic, timeElapsed), + streamInfo.stream); } float DEMDynamicThread::getUpdateFreq() const { @@ -2794,13 +4005,13 @@ void DEMDynamicThread::setFamilyClumpMaterial(unsigned int N, unsigned int mat_i } void DEMDynamicThread::setFamilyMeshMaterial(unsigned int N, unsigned int mat_id) { migrateFamilyToHost(); - for (size_t i = 0; i < simParams->nTriGM; i++) { - bodyID_t owner_id = ownerMesh[i]; // No device-side change + for (size_t i = 0; i < simParams->nMeshPatches; i++) { + bodyID_t owner_id = ownerPatchMesh[i]; // No device-side change if (+(familyID[owner_id]) == N) { - triMaterialOffset[i] = (materialsOffset_t)mat_id; + patchMaterialOffset[i] = (materialsOffset_t)mat_id; } } - triMaterialOffset.toDevice(); + patchMaterialOffset.toDevice(); } size_t DEMDynamicThread::getOwnerContactForces(const std::vector& ownerIDs, @@ -2918,6 +4129,101 @@ size_t DEMDynamicThread::getOwnerContactForces(const std::vector& owne return numUsefulCnt; } +size_t DEMDynamicThread::getOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bool need_torque, + bool torque_in_local) { + ScopedCudaDevice device_scope(streamInfo.device); + if (ownerIDs.empty()) { + DEME_ERROR("Contact-force device retrieval requires at least one owner ID."); + } + for (bodyID_t owner : ownerIDs) { + if (owner >= simParams->nOwnerBodies) { + DEME_ERROR("Contact-force device retrieval owner ID %zu exceeds the %zu owners in the simulation.", + (size_t)owner, (size_t)simParams->nOwnerBodies); + } + } + const size_t num_contacts = *solverScratchSpace.numContacts; + if (capacity < num_contacts) { + DEME_ERROR( + "Contact-force device retrieval needs capacity for at least %zu entries (the current contact count), but " + "the destination capacity is %zu.", + num_contacts, capacity); + } + + const size_t full_bytes = num_contacts * sizeof(float3); + DEME_GPU_CALL(device_data::ValidateOutputPointer(points, full_bytes, destination_device)); + DEME_GPU_CALL(device_data::ValidateOutputPointer(forces, full_bytes, destination_device)); + if (need_torque) { + DEME_GPU_CALL(device_data::ValidateOutputPointer(torques, full_bytes, destination_device)); + } + if (num_contacts == 0) { + return 0; + } + + solverScratchSpace.allocateDualArray("device_contact_owner_ids", ownerIDs.size() * sizeof(bodyID_t)); + solverScratchSpace.allocateDualStruct("device_contact_count"); + const std::vector sorted_owner_ids = hostSort(ownerIDs); + bodyID_t* host_owner_ids = + reinterpret_cast(solverScratchSpace.getDualArrayHost("device_contact_owner_ids")); + std::copy(sorted_owner_ids.begin(), sorted_owner_ids.end(), host_owner_ids); + solverScratchSpace.syncDualArrayHostToDevice("device_contact_owner_ids"); + + size_t* host_count = solverScratchSpace.getDualStructHost("device_contact_count"); + *host_count = 0; + solverScratchSpace.syncDualStructHostToDevice("device_contact_count"); + + const bool same_device = destination_device == streamInfo.device; + float3* packed_points = points; + float3* packed_forces = forces; + float3* packed_torques = torques; + if (!same_device) { + packed_points = + reinterpret_cast(solverScratchSpace.allocateTempVector("device_contact_points", full_bytes)); + packed_forces = + reinterpret_cast(solverScratchSpace.allocateTempVector("device_contact_forces", full_bytes)); + if (need_torque) { + packed_torques = + reinterpret_cast(solverScratchSpace.allocateTempVector("device_contact_torques", full_bytes)); + } + } + + getContactForcesConcerningOwners( + packed_points, packed_forces, packed_torques, solverScratchSpace.getDualStructDevice("device_contact_count"), + reinterpret_cast(solverScratchSpace.getDualArrayDevice("device_contact_owner_ids")), + sorted_owner_ids.size(), &simParams, &granData, num_contacts, need_torque, torque_in_local, streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + solverScratchSpace.syncDualStructDeviceToHost("device_contact_count"); + const size_t useful_count = *host_count; + + if (!same_device && useful_count > 0) { + const size_t useful_bytes = useful_count * sizeof(float3); + DEME_GPU_CALL( + ownerDataTransferBuffer.Copy(points, destination_device, packed_points, streamInfo.device, useful_bytes)); + DEME_GPU_CALL( + ownerDataTransferBuffer.Copy(forces, destination_device, packed_forces, streamInfo.device, useful_bytes)); + if (need_torque) { + DEME_GPU_CALL(ownerDataTransferBuffer.Copy(torques, destination_device, packed_torques, streamInfo.device, + useful_bytes)); + } + } + + if (!same_device) { + solverScratchSpace.finishUsingTempVector("device_contact_points"); + solverScratchSpace.finishUsingTempVector("device_contact_forces"); + if (need_torque) { + solverScratchSpace.finishUsingTempVector("device_contact_torques"); + } + } + solverScratchSpace.finishUsingDualArray("device_contact_owner_ids"); + solverScratchSpace.finishUsingDualStruct("device_contact_count"); + return useful_count; +} + void DEMDynamicThread::setFamilyContactWildcardValue_impl( unsigned int N1, unsigned int N2, @@ -2927,17 +4233,17 @@ void DEMDynamicThread::setFamilyContactWildcardValue_impl( // Get host updated then send all to device migrateFamilyToHost(); contactWildcards[wc_num]->toHost(); - idGeometryA.toHost(); - idGeometryB.toHost(); - contactType.toHost(); + idPatchA.toHost(); + idPatchB.toHost(); + contactTypePatch.toHost(); size_t numCnt = *solverScratchSpace.numContacts; for (size_t i = 0; i < numCnt; i++) { - bodyID_t geoA = idGeometryA[i]; - bodyID_t ownerA = ownerClumpBody[geoA]; - bodyID_t geoB = idGeometryB[i]; - contact_t typeB = contactType[i]; - bodyID_t ownerB = getGeoOwnerID(geoB, typeB); + contact_t typeContact = contactTypePatch[i]; + bodyID_t geoA = idPatchA[i]; + bodyID_t ownerA = getPatchOwnerID(geoA, decodeTypeA(typeContact)); + bodyID_t geoB = idPatchB[i]; + bodyID_t ownerB = getPatchOwnerID(geoB, decodeTypeB(typeContact)); unsigned int famA = +(familyID[ownerA]); unsigned int famB = +(familyID[ownerB]); @@ -3151,6 +4457,180 @@ std::vector DEMDynamicThread::getOwnerFamily(bodyID_t ownerID, bod return fam; } +void DEMDynamicThread::getOwnerDataToDevice(void* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + OwnerDataField field, + unsigned int wildcard_index, + bool validate) { + if (validate && (ownerID > simParams->nOwnerBodies || n > simParams->nOwnerBodies - ownerID)) { + DEME_ERROR("Owner device retrieval range [%zu, %zu) exceeds the %zu owners in the simulation.", (size_t)ownerID, + (size_t)(ownerID + n), (size_t)simParams->nOwnerBodies); + } + if (validate && capacity < n) { + DEME_ERROR("Owner device retrieval needs capacity for %zu elements, but the destination has %zu.", (size_t)n, + capacity); + } + + const size_t bytes = static_cast(n) * OwnerDataElementSize(field); + if (validate) { + DEME_GPU_CALL(device_data::ValidateOutputPointer(destination, bytes, destination_device)); + } + if (n == 0) { + return; + } + + ScopedCudaDevice device_scope(streamInfo.device); + if (destination_device == streamInfo.device) { + PackOwnerData(destination, field, ownerID, n, &simParams, &granData, solverFlags.useMassJitify, wildcard_index, + streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + return; + } + + // Cross-device output is packed once on dT's GPU, then CUDA selects the available inter-device transfer route. + const std::string scratch_name = "owner_device_retrieval"; + void* packed = solverScratchSpace.allocateTempVector(scratch_name, bytes); + PackOwnerData(packed, field, ownerID, n, &simParams, &granData, solverFlags.useMassJitify, wildcard_index, + streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + DEME_GPU_CALL(ownerDataTransferBuffer.Copy(destination, destination_device, packed, streamInfo.device, bytes)); + solverScratchSpace.finishUsingTempVector(scratch_name); +} + +void DEMDynamicThread::setOwnerDataFromDevice(bodyID_t ownerID, + const void* source, + size_t count, + int source_device, + OwnerStateField field, + bool validate) { + if (validate && (ownerID > simParams->nOwnerBodies || count > simParams->nOwnerBodies - ownerID)) { + DEME_ERROR("Owner device update range [%zu, %zu) exceeds the %zu owners in the simulation.", (size_t)ownerID, + (size_t)(ownerID + count), (size_t)simParams->nOwnerBodies); + } + if (count == 0) { + return; + } + const size_t element_size = field == OwnerStateField::ORIENTATION ? sizeof(float4) : sizeof(float3); + const size_t bytes = count * element_size; + if (validate) { + DEME_GPU_CALL(device_data::ValidateOutputPointer(source, bytes, source_device)); + } + ScopedCudaDevice device_scope(streamInfo.device); + const bool same_device = source_device == streamInfo.device; + constexpr const char* scratch_name = "owner_device_input"; + const void* unpack_source = source; + if (!same_device) { + void* local_source = solverScratchSpace.allocateTempVector(scratch_name, bytes); + DEME_GPU_CALL(ownerDataTransferBuffer.Copy(local_source, streamInfo.device, source, source_device, bytes)); + unpack_source = local_source; + } + if (validate && field == OwnerStateField::ORIENTATION) { + const std::string validation_name = "owner_orientation_validation"; + solverScratchSpace.allocateDualStruct(validation_name); + size_t* host_invalid = solverScratchSpace.getDualStructHost(validation_name); + *host_invalid = 0; + solverScratchSpace.syncDualStructHostToDevice(validation_name); + ValidateOwnerOrientations( + static_cast(unpack_source), count, + reinterpret_cast(solverScratchSpace.getDualStructDevice(validation_name)), + streamInfo.stream); + solverScratchSpace.syncDualStructDeviceToHost(validation_name); + const bool invalid = *host_invalid != 0; + solverScratchSpace.finishUsingDualStruct(validation_name); + if (invalid) { + if (!same_device) { + solverScratchSpace.finishUsingTempVector(scratch_name); + } + DEME_ERROR("Owner orientation updates require finite, nonzero-length quaternions."); + } + } + UnpackOwnerState(unpack_source, field, ownerID, count, &simParams, &granData, streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (!same_device) { + solverScratchSpace.finishUsingTempVector(scratch_name); + } +} + +void DEMDynamicThread::getOwnerContactWrenchToDevice(float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t count) { + if (ownerID > simParams->nOwnerBodies || count > simParams->nOwnerBodies - ownerID) { + DEME_ERROR("Owner contact-wrench range [%zu, %zu) exceeds the %zu owners in the simulation.", (size_t)ownerID, + (size_t)(ownerID + count), (size_t)simParams->nOwnerBodies); + } + if (capacity < count) { + DEME_ERROR("Owner contact-wrench retrieval needs capacity for %zu owners, but the destination has %zu.", + (size_t)count, capacity); + } + + const size_t bytes = static_cast(count) * sizeof(float3); + DEME_GPU_CALL(device_data::ValidateOutputPointer(forces, bytes, destination_device)); + DEME_GPU_CALL(device_data::ValidateOutputPointer(torques, bytes, destination_device)); + if (count == 0) { + return; + } + if (solverFlags.useNoContactRecord) { + DEME_ERROR("Owner contact-wrench retrieval requires contact recording; do not enable SetNoForceRecord()."); + } + + ScopedCudaDevice device_scope(streamInfo.device); + const bool same_device = destination_device == streamInfo.device; + float3* reduced_forces = forces; + float3* reduced_torques = torques; + if (!same_device) { + reduced_forces = reinterpret_cast(solverScratchSpace.allocateTempVector("owner_wrench_forces", bytes)); + reduced_torques = + reinterpret_cast(solverScratchSpace.allocateTempVector("owner_wrench_torques", bytes)); + } + + ReduceOwnerContactWrenches(reduced_forces, reduced_torques, ownerID, count, &granData, + *solverScratchSpace.numContacts, streamInfo.stream); + DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (!same_device) { + DEME_GPU_CALL( + ownerDataTransferBuffer.Copy(forces, destination_device, reduced_forces, streamInfo.device, bytes)); + DEME_GPU_CALL( + ownerDataTransferBuffer.Copy(torques, destination_device, reduced_torques, streamInfo.device, bytes)); + solverScratchSpace.finishUsingTempVector("owner_wrench_forces"); + solverScratchSpace.finishUsingTempVector("owner_wrench_torques"); + } +} + +void DEMDynamicThread::getOwnerContactWrench(std::vector& forces, + std::vector& torques, + bodyID_t ownerID, + bodyID_t count) { + forces.resize(count); + torques.resize(count); + if (count == 0) { + return; + } + + const std::string force_name = "owner_wrench_host_forces"; + const std::string torque_name = "owner_wrench_host_torques"; + solverScratchSpace.allocateDualArray(force_name, static_cast(count) * sizeof(float3)); + solverScratchSpace.allocateDualArray(torque_name, static_cast(count) * sizeof(float3)); + auto* device_forces = reinterpret_cast(solverScratchSpace.getDualArrayDevice(force_name)); + auto* device_torques = reinterpret_cast(solverScratchSpace.getDualArrayDevice(torque_name)); + + // Reuse the device reduction so host and device APIs have identical filtering, frame, and accumulation semantics. + getOwnerContactWrenchToDevice(device_forces, device_torques, count, streamInfo.device, ownerID, count); + solverScratchSpace.syncDualArrayDeviceToHost(force_name); + solverScratchSpace.syncDualArrayDeviceToHost(torque_name); + const auto* host_forces = reinterpret_cast(solverScratchSpace.getDualArrayHost(force_name)); + const auto* host_torques = reinterpret_cast(solverScratchSpace.getDualArrayHost(torque_name)); + std::copy(host_forces, host_forces + count, forces.begin()); + std::copy(host_torques, host_torques + count, torques.begin()); + solverScratchSpace.finishUsingDualArray(force_name); + solverScratchSpace.finishUsingDualArray(torque_name); +} + void DEMDynamicThread::setOwnerAngVel(bodyID_t ownerID, const std::vector& angVel) { omgBarX.setVal(streamInfo.stream, RealTupleVectorToXComponentVector(angVel), ownerID); omgBarY.setVal(streamInfo.stream, RealTupleVectorToYComponentVector(angVel), ownerID); @@ -3180,10 +4660,21 @@ void DEMDynamicThread::setOwnerPos(bodyID_t ownerID, const std::vector& } void DEMDynamicThread::setOwnerOriQ(bodyID_t ownerID, const std::vector& oriQ) { - oriQw.setVal(streamInfo.stream, RealTupleVectorToWComponentVector(oriQ), ownerID); - oriQx.setVal(streamInfo.stream, RealTupleVectorToXComponentVector(oriQ), ownerID); - oriQy.setVal(streamInfo.stream, RealTupleVectorToYComponentVector(oriQ), ownerID); - oriQz.setVal(streamInfo.stream, RealTupleVectorToZComponentVector(oriQ), ownerID); + std::vector normalized(oriQ.size()); + for (size_t i = 0; i < oriQ.size(); i++) { + const double norm_squared = + static_cast(oriQ[i].x) * oriQ[i].x + static_cast(oriQ[i].y) * oriQ[i].y + + static_cast(oriQ[i].z) * oriQ[i].z + static_cast(oriQ[i].w) * oriQ[i].w; + if (!std::isfinite(norm_squared) || norm_squared == 0.0) { + DEME_ERROR("Owner orientation updates require finite, nonzero-length quaternions."); + } + const float inverse_norm = static_cast(1.0 / std::sqrt(norm_squared)); + normalized[i] = oriQ[i] * inverse_norm; + } + oriQw.setVal(streamInfo.stream, RealTupleVectorToWComponentVector(normalized), ownerID); + oriQx.setVal(streamInfo.stream, RealTupleVectorToXComponentVector(normalized), ownerID); + oriQy.setVal(streamInfo.stream, RealTupleVectorToYComponentVector(normalized), ownerID); + oriQz.setVal(streamInfo.stream, RealTupleVectorToZComponentVector(normalized), ownerID); syncMemoryTransfer(); } @@ -3199,6 +4690,7 @@ void DEMDynamicThread::setOwnerFamily(bodyID_t ownerID, family_t fam, bodyID_t n } void DEMDynamicThread::setTriNodeRelPos(size_t start, const std::vector& triangles) { + ++visualizationRevision; for (size_t i = 0; i < triangles.size(); i++) { relPosNode1[start + i] = triangles[i].p1; relPosNode2[start + i] = triangles[i].p2; @@ -3212,6 +4704,7 @@ void DEMDynamicThread::setTriNodeRelPos(size_t start, const std::vector& updates) { + ++visualizationRevision; for (size_t i = 0; i < updates.size(); i++) { relPosNode1[start + i] += updates[i].p1; relPosNode2[start + i] += updates[i].p2; @@ -3223,6 +4716,179 @@ void DEMDynamicThread::updateTriNodeRelPos(size_t start, const std::vector& mesh_owner_ids) { + if (mesh_owner_ids.empty()) { + disableTrianglePVTracking(); + return; + } + + ownerTriMesh.toHost(); + std::vector owner_order; + std::vector offsets; + std::vector counts; + std::unordered_map owner_to_slot; + owner_order.reserve(mesh_owner_ids.size()); + offsets.reserve(mesh_owner_ids.size()); + counts.reserve(mesh_owner_ids.size()); + + for (bodyID_t owner : mesh_owner_ids) { + if (owner_to_slot.find(owner) != owner_to_slot.end()) { + continue; + } + size_t count = 0; + for (size_t tri = 0; tri < ownerTriMesh.size(); tri++) { + if (ownerTriMesh[tri] == owner) { + count++; + } + } + if (count == 0) { + DEME_ERROR("Triangle PV tracking owner %zu is not a mesh owner or has no triangles.", (size_t)owner); + } + const size_t slot = owner_order.size(); + owner_to_slot.emplace(owner, slot); + owner_order.push_back(owner); + offsets.push_back(0); + counts.push_back(count); + } + + size_t n_tracked_triangles = 0; + for (size_t i = 0; i < owner_order.size(); i++) { + offsets[i] = n_tracked_triangles; + n_tracked_triangles += counts[i]; + } + if (n_tracked_triangles == 0) { + disableTrianglePVTracking(); + return; + } + + DEME_DUAL_ARRAY_RESIZE(triPVGlobalTriToLocal, simParams->nTriGM, -1); + for (size_t tri = 0; tri < ownerTriMesh.size(); tri++) { + const bodyID_t owner = ownerTriMesh[tri]; + auto it = owner_to_slot.find(owner); + if (it == owner_to_slot.end()) { + triPVGlobalTriToLocal[tri] = -1; + continue; + } + const size_t slot = it->second; + size_t local_idx = offsets[slot]; + for (size_t prev = 0; prev < tri; prev++) { + if (ownerTriMesh[prev] == owner) { + local_idx++; + } + } + triPVGlobalTriToLocal[tri] = static_cast(local_idx); + } + + const size_t alloc_size = DEME_MAX((size_t)1, n_tracked_triangles); + DEME_DUAL_ARRAY_RESIZE(triPVStepP, alloc_size, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVStepPV, alloc_size, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVAccumP, alloc_size, 0.f); + DEME_DUAL_ARRAY_RESIZE(triPVAccumPV, alloc_size, 0.f); + + triPVGlobalTriToLocal.toDeviceAsync(streamInfo.stream); + triPVStepP.toDeviceAsync(streamInfo.stream); + triPVStepPV.toDeviceAsync(streamInfo.stream); + triPVAccumP.toDeviceAsync(streamInfo.stream); + triPVAccumPV.toDeviceAsync(streamInfo.stream); + syncMemoryTransfer(); + + triPVOwnerOrder = std::move(owner_order); + triPVOwnerOffsets = std::move(offsets); + triPVOwnerCounts = std::move(counts); + triPVOwnerToSlot = std::move(owner_to_slot); + triPVTrackingEnabled = true; + triPVNumTrackedTriangles = n_tracked_triangles; + triPVWindowSteps = 0; +} + +void DEMDynamicThread::disableTrianglePVTracking() { + triPVTrackingEnabled = false; + triPVNumTrackedTriangles = 0; + triPVWindowSteps = 0; + triPVOwnerOrder.clear(); + triPVOwnerOffsets.clear(); + triPVOwnerCounts.clear(); + triPVOwnerToSlot.clear(); + + if (triPVGlobalTriToLocal.size() > 0) { + for (size_t i = 0; i < triPVGlobalTriToLocal.size(); i++) { + triPVGlobalTriToLocal[i] = -1; + } + triPVGlobalTriToLocal.toDeviceAsync(streamInfo.stream); + } + if (triPVStepP.size() > 0) { + DEME_GPU_CALL(cudaMemsetAsync(triPVStepP.device(), 0, triPVStepP.size() * sizeof(float), streamInfo.stream)); + } + if (triPVStepPV.size() > 0) { + DEME_GPU_CALL(cudaMemsetAsync(triPVStepPV.device(), 0, triPVStepPV.size() * sizeof(float), streamInfo.stream)); + } + if (triPVAccumP.size() > 0) { + DEME_GPU_CALL(cudaMemsetAsync(triPVAccumP.device(), 0, triPVAccumP.size() * sizeof(float), streamInfo.stream)); + } + if (triPVAccumPV.size() > 0) { + DEME_GPU_CALL( + cudaMemsetAsync(triPVAccumPV.device(), 0, triPVAccumPV.size() * sizeof(float), streamInfo.stream)); + } + syncMemoryTransfer(); +} + +bool DEMDynamicThread::getTrackedOwnerTrianglePV(bodyID_t ownerID, + std::vector& avgP, + std::vector& avgV, + std::vector& avgPV, + bool reset_window) { + if (!triPVTrackingEnabled) { + return false; + } + auto it = triPVOwnerToSlot.find(ownerID); + if (it == triPVOwnerToSlot.end()) { + return false; + } + const size_t slot = it->second; + const size_t offset = triPVOwnerOffsets[slot]; + const size_t count = triPVOwnerCounts[slot]; + avgP.assign(count, 0.f); + avgV.assign(count, 0.f); + avgPV.assign(count, 0.f); + + if (count > 0 && triPVWindowSteps > 0) { + triPVAccumP.toHost(); + triPVAccumPV.toHost(); + const float inv_steps = 1.f / static_cast(triPVWindowSteps); + for (size_t i = 0; i < count; i++) { + avgP[i] = triPVAccumP[offset + i] * inv_steps; + avgPV[i] = triPVAccumPV[offset + i] * inv_steps; + if (!std::isfinite(avgP[i]) || avgP[i] < 0.f) { + avgP[i] = 0.f; + } + if (!std::isfinite(avgPV[i]) || avgPV[i] < 0.f) { + avgPV[i] = 0.f; + } + avgV[i] = (avgP[i] > DEME_TINY_FLOAT) ? (avgPV[i] / avgP[i]) : 0.f; + if (!std::isfinite(avgV[i]) || avgV[i] < 0.f) { + avgV[i] = 0.f; + } + } + } + + if (reset_window) { + resetTrackedTrianglePVWindow(); + } + return true; +} + +void DEMDynamicThread::resetTrackedTrianglePVWindow() { + triPVWindowSteps = 0; + if (triPVAccumP.size() > 0) { + DEME_GPU_CALL(cudaMemsetAsync(triPVAccumP.device(), 0, triPVAccumP.size() * sizeof(float), streamInfo.stream)); + } + if (triPVAccumPV.size() > 0) { + DEME_GPU_CALL( + cudaMemsetAsync(triPVAccumPV.device(), 0, triPVAccumPV.size() * sizeof(float), streamInfo.stream)); + } + syncMemoryTransfer(); +} + void DEMDynamicThread::addOwnerNextStepAcc(bodyID_t ownerID, const std::vector& acc) { accSpecified.setVal(streamInfo.stream, std::vector(acc.size(), 1), ownerID); aX.setVal(streamInfo.stream, RealTupleVectorToXComponentVector(acc), ownerID); diff --git a/src/DEM/dT.h b/src/DEM/dT.h index 04e58243..a8030546 100644 --- a/src/DEM/dT.h +++ b/src/DEM/dT.h @@ -6,12 +6,11 @@ #ifndef DEME_DT #define DEME_DT -#include #include +#include #include #include #include -#include #include #include @@ -19,18 +18,13 @@ #include "../core/utils/ThreadManager.h" #include "../core/utils/GpuManager.h" #include "../core/utils/DataMigrationHelper.hpp" -#include "../core/utils/GpuError.h" +#include "../core/utils/DeviceDataTransfer.hpp" +#include "../algorithms/DEMOwnerDataRetrieval.h" #include "BdrsAndObjs.h" #include "Defines.h" #include "Structs.h" #include "AuxClasses.h" - -// Forward declare deme::jit::Program -namespace deme { -namespace jit { -class Program; -} -} // namespace deme +#include "utils/DynamicThreadHelpers.hpp" namespace deme { @@ -47,7 +41,7 @@ class DEMDynamicThread { // GpuManager* pGpuDistributor; // dT verbosity - VERBOSITY verbosity = INFO; + verbosity_t verbosity = VERBOSITY_INFO; // Some behavior-related flags SolverFlags solverFlags; @@ -58,13 +52,16 @@ class DEMDynamicThread { // Friend system DEMKinematicThread DEMKinematicThread* kT; - // Number of items in the buffer array (which is not a dual vector, due to our need to explicitly control where - // it is allocated) - size_t buffer_size = 0; + // Number of items in the buffer array for primitive contact info + size_t primitiveBufferSize = 0; + // Number of items in the buffer array for patch-enabled contact info + size_t patchBufferSize = 0; - // dT's one-element buffer of kT-supplied nContacts (as buffer, it's device-only, but I used DualStruct just for - // convenience...) - DualStruct nContactPairs_buffer = DualStruct(0); + // dT's one-element buffer of kT-supplied nPrimitiveContactPairs (as buffer, it's device-only, but I used DualStruct + // just for convenience...) + DualStruct nPrimitiveContactPairs_buffer = DualStruct(0); + // Similarly, the patch contact-related buffer + DualStruct nPatchContactPairs_buffer = DualStruct(0); // Array-used memory size in bytes size_t m_approxDeviceBytesUsed = 0; @@ -75,35 +72,58 @@ class DEMDynamicThread { // A class that contains scratch pad and system status data (constructed with the number of temp arrays we need) DEMSolverScratchData solverScratchSpace = DEMSolverScratchData(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Reuses pinned staging storage only when a requested destination GPU cannot access dT's GPU directly. + device_data::TransferBuffer ownerDataTransferBuffer; // The number of for iterations dT does for a specific user "run simulation" call double cycleDuration; // dT believes this amount of future drift is ideal DualStruct perhapsIdealFutureDrift = DualStruct(0); - // Finiteness probes for the system state, one per array checked. SUM reductions are used - // rather than MAX because a max reduction cannot propagate NaN: IEEE comparisons against - // NaN are false, so cub::DeviceReduce::Max silently skips NaN operands. - DualStruct stateFiniteProbe = DualStruct(0.f); + FutureDriftRegulator futureDriftRegulator; // Buffer arrays for storing info from the dT side. // kT modifies these arrays; dT uses them only. // dT gets contact pair/location/history map info from kT - DeviceArray idGeometryA_buffer = DeviceArray(&m_approxDeviceBytesUsed); - DeviceArray idGeometryB_buffer = DeviceArray(&m_approxDeviceBytesUsed); - DeviceArray contactType_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray idPrimitiveA_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray idPrimitiveB_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray contactTypePrimitive_buffer = DeviceArray(&m_approxDeviceBytesUsed); + + // NEW: Buffer arrays for separate patch IDs and their mapping to geometry arrays + DeviceArray idPatchA_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray idPatchB_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray contactTypePatch_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray contactPatchIsland_buffer = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray geomToPatchMap_buffer = DeviceArray(&m_approxDeviceBytesUsed); DeviceArray contactMapping_buffer = DeviceArray(&m_approxDeviceBytesUsed); + // Permanent array for patch contact penetrations produced by patch-level force correction + DeviceArray finalPenetrations = DeviceArray(&m_approxDeviceBytesUsed); + + // Per-triangle max tri-tri penetration values to be sent to kT + DeviceArray maxTriTriPenetration = DeviceArray(&m_approxDeviceBytesUsed); + + // Optional per-triangle diagnostics (P, V, P*V) for selected mesh owners. + bool triPVTrackingEnabled = false; + size_t triPVNumTrackedTriangles = 0; + unsigned int triPVWindowSteps = 0; + DualArray triPVGlobalTriToLocal = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triPVStepP = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triPVStepPV = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triPVAccumP = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triPVAccumPV = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + std::vector triPVOwnerOrder; + std::vector triPVOwnerOffsets; + std::vector triPVOwnerCounts; + std::unordered_map triPVOwnerToSlot; + // Simulation params-related variables DualStruct simParams = DualStruct(); // Pointers to those data arrays defined below, stored in a struct DualStruct granData = DualStruct(); - // Log for anomalies in the simulation - WorkerAnomalies anomalies = WorkerAnomalies(); - // Body-related arrays, for dT's personal use (not transfer buffer) // Those are the smaller ones, the unique, template ones @@ -132,6 +152,9 @@ class DEMDynamicThread { DualArray relPosNode2 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); DualArray relPosNode3 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Relative position (to mesh CoM) of each mesh patch + DualArray relPosPatch = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // External object's components may need the following arrays to store some extra defining features of them. We // assume there are usually not too many of them in a simulation. // Relative position w.r.t. the owner. For example, the following 3 arrays may hold center points for plates, or tip @@ -207,14 +230,24 @@ class DEMDynamicThread { DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // Contact pair/location, for dT's personal use!! - DualArray idGeometryA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray idGeometryB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray contactType = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPrimitiveA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPrimitiveB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray contactTypePrimitive = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // DualArray contactMapping; + // NEW: Separate patch IDs and mapping arrays (work arrays for dT) + DualArray idPatchA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPatchB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray contactTypePatch = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray contactPatchIsland = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray geomToPatchMap = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Some of dT's own work arrays // Force of each contact event. It is the force that bodyA feels. They are in global. DualArray contactForces = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Optional contact normals. + DualArray contactNormals = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // An imaginary `force' in each contact event that produces torque only, and does not affect the linear motion. It // will rise in our default rolling resistance model, which is just a torque model; yet, our contact registration is // contact pair-based, meaning we do not know the specs of each contact body, so we can register force only, not @@ -264,8 +297,26 @@ class DEMDynamicThread { // Template-related arrays // Belonged-body ID DualArray ownerClumpBody = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray ownerMesh = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerTriMesh = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); DualArray ownerAnalBody = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Mesh owner metadata used by shell, watertight, and contact-island checks. + DualArray ownerMeshConvex = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerMeshNeverWinner = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerMeshWatertight = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerMeshShellHalfThickness = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Mesh patch information: each facet belongs to a patch, and each patch has material properties + // Patch ID for each triangle facet (maps facet to patch) + DualArray triPatchID = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Triangle edge neighbors (compact; index via triNeighborIndex) + DualArray triNeighborIndex = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor1 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor2 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor3 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Mesh patch owner IDs (one per patch, flattened across all meshes) + DualArray ownerPatchMesh = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // The ID that maps this sphere component's geometry-defining parameters, when this component is jitified DualArray clumpComponentOffset = @@ -280,7 +331,8 @@ class DEMDynamicThread { // The ID that maps this entity's material DualArray sphereMaterialOffset = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray triMaterialOffset = + // Material offset for each mesh patch (indexed by patch, can be looked up via triPatchID) + DualArray patchMaterialOffset = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // dT's copy of family map @@ -295,9 +347,32 @@ class DEMDynamicThread { // that means geometries should be considered in contact when they are physically in contact. DualArray familyExtraMarginSize = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Combined-owner mapping (owner -> master owner, NULL_BODYID if not participating in a combined owner). + DualArray ownerCombinedMaster = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Fixed member transforms in the master frame. + DualArray ownerCombinedRelPos = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerCombinedRelOriQ = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Equivalent combined-group mass/MOI, stored on master owner slots. + DualArray ownerCombinedMasterMass = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerCombinedMasterMOI = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // dT's copy of "clump template and their names" map std::unordered_map templateNumNameMap; + // dT's storage of how many contact pairs of each contact type are currently present + DualArray existingContactTypes; + DualArray typeStartOffsetsPrimitive; + DualArray typeStartOffsetsPatch; + size_t m_numExistingTypes = 0; + // A map that records the contact for each contact type currently existing + ContactTypeMap> typeStartCountPrimitiveMap; + ContactTypeMap> typeStartCountPatchMap; + // A map that records the corresponding jitify program bundle and kernel name for each contact type + ContactTypeMap, std::string>>> + contactTypePrimitiveKernelMap; + ContactTypeMap, std::string>>> + contactTypePatchKernelMap; + // dT's timers std::vector timer_names = {"Clear force array", "Calculate contact forces", "Optional force reduction", "Integration", "Unpack updates from kT", "Send to kT buffer", @@ -305,6 +380,8 @@ class DEMDynamicThread { SolverTimers timers = SolverTimers(timer_names); public: + // Invalidates renderer geometry after initialization, insertion, or mesh deformation (not rigid motion). + std::uint64_t visualizationRevision = 0; friend class DEMSolver; friend class DEMKinematicThread; @@ -322,17 +399,8 @@ class DEMDynamicThread { // spawning the child thread. DEME_GPU_CALL(cudaStreamCreate(&streamInfo.stream)); - // Launch a worker thread bound to this instance. An exception escaping a std::thread calls - // std::terminate; on some platforms (notably Windows) that prints nothing, so report it here - // before dying, otherwise runtime errors like JIT compilation failures are silently swallowed. - th = std::move(std::thread([this]() { - try { - this->workerThread(); - } catch (const std::exception& e) { - std::cerr << "Fatal error in dT worker thread: " << e.what() << std::endl; - throw; - } - })); + // Launch a worker thread bound to this instance + th = std::move(std::thread([this]() { this->workerThread(); })); } ~DEMDynamicThread() { // std::cout << "Dynamic thread closing..." << std::endl; @@ -366,8 +434,11 @@ class DEMDynamicThread { double ts_size, float expand_factor, float approx_max_vel, + double max_tritri_penetration, + float tritri_contact_rejection_ratio, float expand_safety_param, float expand_safety_adder, + bool use_angvel_margin, const std::set& contact_wildcards, const std::set& owner_wildcards, const std::set& geo_wildcards); @@ -389,6 +460,37 @@ class DEMDynamicThread { std::vector getOwnerAngAcc(bodyID_t ownerID, bodyID_t n = 1); /// Get this owner's family number, for n consecutive items. std::vector getOwnerFamily(bodyID_t ownerID, bodyID_t n = 1); + /// @brief Pack a consecutive owner range and synchronously fill caller-provided CUDA-accessible memory. + /// @param destination Caller-owned CUDA destination. + /// @param capacity Destination capacity in output elements. + /// @param destination_device Logical device owning `destination`. + /// @param ownerID First owner in the consecutive range. + /// @param n Number of owners to pack. + /// @param field Stored field to pack. + /// @param wildcard_index Wildcard slot used only when `field` is WILDCARD. + /// @param validate Whether to validate ranges, capacity, and CUDA pointer metadata. + void getOwnerDataToDevice(void* destination, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t n, + OwnerDataField field, + unsigned int wildcard_index = 0, + bool validate = true); + /// @brief Synchronously consume public-layout owner state from CUDA memory on dT's device. + /// @param ownerID First owner in the consecutive range. + /// @param source Caller-owned CUDA source on dT's device. + /// @param count Number of owner values to consume. + /// @param source_device Logical device owning `source`. Remote input is copied to dT before unpacking. + /// @param field State field to unpack. Next-step acceleration fields also set their one-step preservation flags. + /// @param validate Whether to validate the range, pointer metadata, and orientation validity. Orientation values + /// are normalized during unpacking regardless of this flag. + void setOwnerDataFromDevice(bodyID_t ownerID, + const void* source, + size_t count, + int source_device, + OwnerStateField field, + bool validate = true); // Get the current auto-adjusted update freq. float getUpdateFreq() const; @@ -414,6 +516,18 @@ class DEMDynamicThread { /// Rewrite the relative positions of the flattened triangle soup, starting from `start' by the amount stipulated in /// updates. void updateTriNodeRelPos(size_t start, const std::vector& updates); + /// Configure per-triangle P/V/P*V tracking for selected mesh owners. + void configureTrianglePVTracking(const std::vector& mesh_owner_ids); + /// Disable and clear per-triangle P/V/P*V tracking state. + void disableTrianglePVTracking(); + /// Get frame-window averaged per-triangle P, V, and P*V for one tracked owner. + bool getTrackedOwnerTrianglePV(bodyID_t ownerID, + std::vector& avgP, + std::vector& avgV, + std::vector& avgPV, + bool reset_window); + /// Reset the current per-triangle PV accumulation window. + void resetTrackedTrianglePVWindow(); /// @brief Globally modify a owner wildcard's value. void setOwnerWildcardValue(bodyID_t ownerID, unsigned int wc_num, const std::vector& vals); @@ -425,7 +539,7 @@ class DEMDynamicThread { /// @brief Set all meshes in this family to have this material. void setFamilyMeshMaterial(unsigned int N, unsigned int mat_id); - /// @brief Set the geometry wildcards of triangles, starting from geoID, for the length of vals. + /// @brief Set the geometry wildcards of mesh triangles, starting from geoID, for the length of vals. void setTriWildcardValue(bodyID_t geoID, unsigned int wc_num, const std::vector& vals); /// @brief Set the geometry wildcards of spheres, starting from geoID, for the length of vals. void setSphWildcardValue(bodyID_t geoID, unsigned int wc_num, const std::vector& vals); @@ -441,7 +555,7 @@ class DEMDynamicThread { /// @brief Fill res with the `wc_num' wildcard values, for n spheres starting from ID. void getSphereWildcardValue(std::vector& res, bodyID_t ID, unsigned int wc_num, size_t n); - /// @brief Fill res with the `wc_num' wildcard values, for n triangles starting from ID. + /// @brief Fill res with the `wc_num' wildcard values, for n mesh triangles starting from ID. void getTriWildcardValue(std::vector& res, bodyID_t ID, unsigned int wc_num, size_t n); /// @brief Fill res with the `wc_num' wildcard values, for n analytical entities starting from ID. void getAnalWildcardValue(std::vector& res, bodyID_t ID, unsigned int wc_num, size_t n); @@ -467,9 +581,43 @@ class DEMDynamicThread { std::vector& forces, std::vector& torques, bool torque_in_local = false); - - /// Get owner of contact geo B. - bodyID_t getGeoOwnerID(const bodyID_t& geoB, const contact_t& type) const; + /// Compact contact-pair data concerning the selected owners directly into caller-provided CUDA memory. + size_t getOwnerContactForcesToDevice(const std::vector& ownerIDs, + float3* points, + float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bool need_torque, + bool torque_in_local); + /// @brief Reduce patch contacts into device-resident global resultant wrenches for consecutive owners. + /// @param forces Writable CUDA destination for one resultant force per owner. + /// @param torques Writable CUDA destination for one owner-position resultant torque per owner. + /// @param capacity Element capacity shared by both destination buffers. + /// @param destination_device Logical CUDA device owning both destinations. + /// @param ownerID First owner in the consecutive range. + /// @param count Number of owners to reduce. + void getOwnerContactWrenchToDevice(float3* forces, + float3* torques, + size_t capacity, + int destination_device, + bodyID_t ownerID, + bodyID_t count); + /// @brief Reduce patch contacts into host vectors containing one global resultant wrench per requested owner. + /// @param forces Output vector resized to `count` global resultant forces. + /// @param torques Output vector resized to `count` global owner-position resultant torques. + /// @param ownerID First owner in the consecutive range. + /// @param count Number of owners to reduce. + void getOwnerContactWrench(std::vector& forces, + std::vector& torques, + bodyID_t ownerID, + bodyID_t count); + + /// Get owner of contact geometry (sphere, triangle, analytical entity). + bodyID_t getGeoOwnerID(const bodyID_t& geo, const geoType_t& type) const; + + /// Get the owner of a contact patch (triangle patch, sphere, analytical entity). + bodyID_t getPatchOwnerID(const bodyID_t& patchID, const geoType_t& type) const; /// Let dT know that it needs a kT update, as something important may have changed, and old contact pair info is no /// longer valid. @@ -485,6 +633,8 @@ class DEMDynamicThread { size_t nTriMeshes, size_t nSpheresGM, size_t nTriGM, + size_t nTriNeighbors, + size_t nMeshPatches, unsigned int nAnalGM, size_t nExtraContacts, unsigned int nMassProperties, @@ -496,7 +646,7 @@ class DEMDynamicThread { // Components of initGPUArrays void buildTrackedObjs(const std::vector>& input_clump_batches, const std::vector& ext_obj_comp_num, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, std::vector>& tracked_objs, size_t nExistOwners, size_t nExistSpheres, @@ -506,22 +656,32 @@ class DEMDynamicThread { const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, const std::vector& ext_obj_moi_types, const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_offsets, size_t nExistOwners, size_t nExistSpheres, - size_t nExistingFacets); + size_t nExistingFacets, + size_t nExistingPatches, + size_t nExistingTriNeighbors); void registerPolicies(const std::unordered_map& template_number_name_map, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, @@ -537,13 +697,20 @@ class DEMDynamicThread { const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const std::unordered_map& template_number_name_map, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, @@ -551,6 +718,9 @@ class DEMDynamicThread { const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_jit_types, + const std::vector& mesh_obj_moi_jit_types, + const std::vector& mesh_obj_mass_offsets, const std::vector>& loaded_materials, const std::vector& family_mask_matrix, const std::set& no_output_families, @@ -562,19 +732,29 @@ class DEMDynamicThread { const std::vector& input_ext_obj_xyz, const std::vector& input_ext_obj_rot, const std::vector& input_ext_obj_family, - const std::vector>& input_mesh_objs, + const std::vector>& input_mesh_objs, const std::vector& input_mesh_obj_xyz, const std::vector& input_mesh_obj_rot, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& mesh_facet_owner, - const std::vector& mesh_facet_materials, + const std::vector& mesh_facet_patch, + const std::vector& mesh_facet_neighbor1, + const std::vector& mesh_facet_neighbor2, + const std::vector& mesh_facet_neighbor3, const std::vector& mesh_facets, + const std::vector& mesh_patch_owner, + const std::vector& mesh_patch_materials, const ClumpTemplateFlatten& clump_templates, const std::vector& ext_obj_mass_types, const std::vector& ext_obj_moi_types, const std::vector& ext_obj_comp_num, const std::vector& mesh_obj_mass_types, const std::vector& mesh_obj_moi_types, + const std::vector& mesh_obj_mass_jit_types, + const std::vector& mesh_obj_moi_jit_types, + const std::vector& mesh_obj_mass_offsets, const std::vector>& loaded_materials, const std::vector& family_mask_matrix, const std::set& no_output_families, @@ -584,12 +764,11 @@ class DEMDynamicThread { size_t nExistingSpheres, size_t nExistingTriMesh, size_t nExistingFacets, + size_t nExistingTriNeighbors, + size_t nExistingPatches, unsigned int nExistingObj, unsigned int nExistingAnalGM); - /// Change radii and relPos info of these owners (if these owners are clumps) - void changeOwnerSizes(const std::vector& IDs, const std::vector& factors); - /// Put sim data array pointers in place void packDataPointers(); void packTransferPointers(DEMKinematicThread* kT); @@ -600,15 +779,22 @@ class DEMDynamicThread { // Generate contact info container based on the current contact array, and return it. std::shared_ptr generateContactInfo(float force_thres); + std::shared_ptr generateContactInfoFromHost(float force_thres); -#ifdef DEME_USE_CHPF - void writeSpheresAsChpf(std::ofstream& ptFile); - void writeClumpsAsChpf(std::ofstream& ptFile, unsigned int accuracy = 10); -#endif void writeSpheresAsCsv(std::ofstream& ptFile); + void writeSpheresAsCsvFromHost(std::ofstream& ptFile); + void writeSpheresAsVtk(std::ofstream& ptFile); + void writeSpheresAsVtkFromHost(std::ofstream& ptFile); void writeClumpsAsCsv(std::ofstream& ptFile, unsigned int accuracy = 10); + void writeClumpsAsCsvFromHost(std::ofstream& ptFile, unsigned int accuracy = 10); void writeContactsAsCsv(std::ofstream& ptFile, float force_thres = DEME_TINY_FLOAT); + void writeContactsAsCsvFromHost(std::ofstream& ptFile, float force_thres = DEME_TINY_FLOAT); void writeMeshesAsVtk(std::ofstream& ptFile); + void writeMeshesAsVtkFromHost(std::ofstream& ptFile); + void writeMeshesAsStl(std::ofstream& ptFile); + void writeMeshesAsStlFromHost(std::ofstream& ptFile); + void writeMeshesAsPly(std::ofstream& ptFile, bool patch_colors = false); + void writeMeshesAsPlyFromHost(std::ofstream& ptFile, bool patch_colors = false); /// Called each time when the user calls DoDynamicsThenSync. void startThread(); @@ -618,9 +804,7 @@ class DEMDynamicThread { void workerThread(); // Sync my stream - void syncMemoryTransfer() { - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } + void syncMemoryTransfer() { DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); } // Reset kT--dT interaction coordinator stats void resetUserCallStat(); @@ -636,6 +820,7 @@ class DEMDynamicThread { for (const auto& name : timer_names) { timers.GetTimer(name).reset(); } + timers.ResetGpuTimers(); } /// Get the simulation time passed since the start of simulation @@ -648,13 +833,24 @@ class DEMDynamicThread { const std::vector& JitifyOptions); // Execute this kernel, then return the reduced value - float* inspectCall(const std::shared_ptr& inspection_kernel, + float* inspectCall(const std::shared_ptr& inspection_kernel, const std::string& kernel_name, INSPECT_ENTITY_TYPE thing_to_insp, CUB_REDUCE_FLAVOR reduce_flavor, bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes, bool return_device_ptr = false); + // Device-only inspection helper for dT worker paths that need per-entity values, such as kT margin inputs. + float* inspectCallDeviceNoReduce(const std::shared_ptr& inspection_kernel, + const std::string& kernel_name, + INSPECT_ENTITY_TYPE thing_to_insp, + CUB_REDUCE_FLAVOR reduce_flavor, + bool all_domain, + DualArray& reduceResArr, + DualArray& reduceRes); + private: // Name for this class const std::string Name = "dT"; @@ -665,7 +861,7 @@ class DEMDynamicThread { bool new_contacts_loaded = false; // Meshes cached on dT side that has corresponding owner number associated. Useful for outputting meshes. - std::vector> m_meshes; + std::vector> m_meshes; // Number of trackers I already processed before (if I see a tracked_obj array longer than this in initialization, I // know I have to process the new-comers) @@ -673,19 +869,35 @@ class DEMDynamicThread { // A pointer that points to the location that holds the current max_vel info, which will soon be transferred to kT float* pCycleVel; + // Pointer that points to the location that holds the current angular velocity magnitude info, which will soon be + // transferred to kT + float* pCycleAngVel; // The inspector for calculating max vel for this cycle - std::shared_ptr approxMaxVelFunc; - - // Some private arrays that can be used to store inspection results, ready to be passed somewhere else - DualArray m_reduceResArr = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray m_reduceRes = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + std::shared_ptr approxVelFunc; + // The inspector for calculating angular velocity magnitude for this cycle + std::shared_ptr approxAngVelFunc; // Migrate contact history to fit the structure of the newly received contact array - inline void migrateEnduringContacts(); - + inline void migrateEnduringContacts(const LostContactDebugSnapshot& oldContactSnapshot); + // DEBUG-verbosity only: print detailed records for old live patch contacts that lost their migration partner. + inline void reportLostContactDebugDetails(const LostContactDebugSnapshot& oldContactSnapshot, + const notStupidBool_t* contactSentry, + float* const* newWildcards, + size_t lostContactCount); + + // Impl of calculateForces + inline void dispatchPrimitiveForceKernels( + const ContactTypeMap>& typeStartCountMap, + const ContactTypeMap, std::string>>>& + typeKernelMap); + inline void dispatchPatchBasedForceCorrections( + const ContactTypeMap>& typeStartCountPrimitiveMap, + const ContactTypeMap>& typeStartCountPatchMap, + const ContactTypeMap, std::string>>>& + typeKernelMap); // Update clump-based acceleration array based on sphere-based force array - inline void calculateForces(); + void calculateForces(); // Update clump pos/oriQ and vel/omega based on acceleration inline void integrateOwnerMotions(); @@ -697,22 +909,24 @@ class DEMDynamicThread { // Change sim params based on dT's experience, if needed inline void calibrateParams(); - // Verifies the integrated state is finite, and reports which component is not. - inline void checkStateIsFinite(); - // Determine the max vel for this cycle, kT needs it - inline float* determineSysVel(); + // Determine the vel info for this cycle, kT needs it + inline void determineSysVel(); // Some per-step checks/modification, done before integration, but after force calculation (thus sort of in the // mid-step stage) inline void routineChecks(); + // Record one completed dT step for per-triangle PV tracking. + void finalizeTrianglePVWindowStep(); // Bring dT buffer array data to its working arrays inline void unpackMyBuffer(); // Send produced data to kT-owned biffers void sendToTheirBuffer(); // Resize some work arrays based on the number of contact pairs provided by kT - void contactEventArraysResize(size_t nContactPairs); + void contactPrimitivesArraysResize(size_t nContactPairs); + // Resize mesh patch pair array based on the number of mesh-involved contact pairs + void contactPatchArrayResize(size_t nMeshInvolvedContactPairs); // Deallocate everything void deallocateEverything(); @@ -728,48 +942,14 @@ class DEMDynamicThread { const std::function& condition); // Just-in-time compiled kernels - std::shared_ptr prep_force_kernels; - std::shared_ptr cal_force_kernels; - std::shared_ptr collect_force_kernels; - std::shared_ptr integrator_kernels; - // std::shared_ptr quarry_stats_kernels; - std::shared_ptr mod_kernels; - std::shared_ptr misc_kernels; - - // Adjuster for update freq - class AccumStepUpdater { - private: - unsigned int num_steps = 0; - unsigned int num_updates = 0; - unsigned int cached_size = 200; - - public: - AccumStepUpdater() {} - ~AccumStepUpdater() {} - inline void AddUpdate() { num_updates++; } - inline void AddStep() { num_steps++; } - inline bool Query(unsigned int& ideal) { - if (num_updates > NUM_STEPS_RESERVED_AFTER_RENEWING_FREQ_TUNER) { - // * 2 because double update freq is an ideal future drift - ideal = (unsigned int)((double)num_steps / num_updates * 2); - if (num_updates >= cached_size) { - Clear(); - } - return true; - } else { - return false; - } - } - - // Return this accumulator to initial state - void Clear() { - num_steps = 0; - num_updates = 0; - } - - void SetCacheSize(unsigned int n) { cached_size = n; } - }; - AccumStepUpdater accumStepUpdater = AccumStepUpdater(); + std::shared_ptr cal_force_kernels; + std::shared_ptr cal_patch_force_kernels; + std::shared_ptr collect_force_kernels; + std::shared_ptr integrator_kernels; + // std::shared_ptr quarry_stats_kernels; + std::shared_ptr mod_kernels; + // Instantiate common dT kernels during initialization so first-use JIT costs are paid before time stepping. + void prewarmKernels(); // A collection of migrate-to-host methods. Bulk migrate-to-host is by nature on-demand only. void migrateFamilyToHost(); diff --git a/src/DEM/kT.cpp b/src/DEM/kT.cpp index 9ea1e98e..80fc0043 100644 --- a/src/DEM/kT.cpp +++ b/src/DEM/kT.cpp @@ -11,33 +11,53 @@ #include #include #include "dT.h" -#include "HostSideHelpers.hpp" +#include "utils/HostSideHelpers.hpp" #include "Defines.h" #include "algorithms/DEMStaticDeviceSubroutines.h" #include "kernel/DEMHelperKernels.cuh" namespace deme { -inline void DEMKinematicThread::transferArraysResize(size_t nContactPairs) { +inline void DEMKinematicThread::transferPrimitivesArraysResize(size_t nContactPairs) { // These buffers are on dT DEME_GPU_CALL(cudaSetDevice(dT->streamInfo.device)); - dT->buffer_size = nContactPairs; - DEME_DEVICE_ARRAY_RESIZE(dT->idGeometryA_buffer, nContactPairs); - DEME_DEVICE_ARRAY_RESIZE(dT->idGeometryB_buffer, nContactPairs); - DEME_DEVICE_ARRAY_RESIZE(dT->contactType_buffer, nContactPairs); - granData->pDTOwnedBuffer_idGeometryA = dT->idGeometryA_buffer.data(); - granData->pDTOwnedBuffer_idGeometryB = dT->idGeometryB_buffer.data(); - granData->pDTOwnedBuffer_contactType = dT->contactType_buffer.data(); + dT->primitiveBufferSize = nContactPairs; + DEME_DEVICE_ARRAY_RESIZE(dT->idPrimitiveA_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->idPrimitiveB_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->contactTypePrimitive_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->geomToPatchMap_buffer, nContactPairs); + granData->pDTOwnedBuffer_idPrimitiveA = dT->idPrimitiveA_buffer.data(); + granData->pDTOwnedBuffer_idPrimitiveB = dT->idPrimitiveB_buffer.data(); + granData->pDTOwnedBuffer_contactType = dT->contactTypePrimitive_buffer.data(); + granData->pDTOwnedBuffer_geomToPatchMap = dT->geomToPatchMap_buffer.data(); + + // Unset the device change we just made + DEME_GPU_CALL(cudaSetDevice(streamInfo.device)); + + // But we don't have to toDevice granData or dT->granData, and this is because all _buffer arrays don't + // particupate kernel computations, so even if their pointers are fresh only on host, it's fine +} + +inline void DEMKinematicThread::transferPatchArrayResize(size_t nContactPairs) { + // These buffers are on dT + DEME_GPU_CALL(cudaSetDevice(dT->streamInfo.device)); + dT->patchBufferSize = nContactPairs; + DEME_DEVICE_ARRAY_RESIZE(dT->idPatchA_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->idPatchB_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->contactTypePatch_buffer, nContactPairs); + DEME_DEVICE_ARRAY_RESIZE(dT->contactPatchIsland_buffer, nContactPairs); + granData->pDTOwnedBuffer_idPatchA = dT->idPatchA_buffer.data(); + granData->pDTOwnedBuffer_idPatchB = dT->idPatchB_buffer.data(); + granData->pDTOwnedBuffer_contactTypePatch = dT->contactTypePatch_buffer.data(); + granData->pDTOwnedBuffer_contactPatchIsland = dT->contactPatchIsland_buffer.data(); if (!solverFlags.isHistoryless) { DEME_DEVICE_ARRAY_RESIZE(dT->contactMapping_buffer, nContactPairs); granData->pDTOwnedBuffer_contactMapping = dT->contactMapping_buffer.data(); } + // Unset the device change we just made DEME_GPU_CALL(cudaSetDevice(streamInfo.device)); - - // But we don't have to toDevice granData or dT->granData, and this is because all _buffer arrays don't - // particupate kernel computations, so even if their values are fresh only on host, it's fine } void DEMKinematicThread::calibrateParams() { @@ -50,7 +70,7 @@ void DEMKinematicThread::calibrateParams() { // Note the speed can be 0, yet we find performance variance. Then this is purely noise. We still wish the // bin size to change in the next iteration, so we assign a direction randomly. if (speed_dir == 0) - speed_dir = (randomZeroOrOne() == 0) ? -1 : 1; + speed_dir = (random_zero_or_one() == 0) ? -1 : 1; float speed_update; if (curr_time < prev_time) { // If there is improvement, then we accelerate the current change direction @@ -79,22 +99,53 @@ void DEMKinematicThread::calibrateParams() { // Change bin size if (stateParams.binCurrentChangeRate > 0) { - simParams->binSize *= (1. + stateParams.binCurrentChangeRate); + simParams->dyn.binSize *= (1. + stateParams.binCurrentChangeRate); } else { - simParams->binSize /= (1. - stateParams.binCurrentChangeRate); + simParams->dyn.binSize /= (1. - stateParams.binCurrentChangeRate); } + simParams->dyn.inv_binSize = 1. / simParams->dyn.binSize; } // Register the new bin size - stateParams.numBins = hostCalcBinNum(simParams->nbX, simParams->nbY, simParams->nbZ, simParams->voxelSize, - simParams->binSize, simParams->nvXp2, simParams->nvYp2, simParams->nvZp2); + stateParams.numBins = + hostCalcBinNum(simParams->nbX, simParams->nbY, simParams->nbZ, simParams->voxelSize, simParams->dyn.binSize, + simParams->nvXp2, simParams->nvYp2, simParams->nvZp2); - DEME_DEBUG_PRINTF("Bin size is now: %.7g", simParams->binSize); + DEME_DEBUG_PRINTF("Bin size is now: %.7g", simParams->dyn.binSize); DEME_DEBUG_PRINTF("Total num of bins is now: %zu", stateParams.numBins); DEME_DEBUG_PRINTF("kT runtime per step: %.7gs", CDAccumTimer.GetPrevTime()); } - // binSize is now calculated, we need to migrate that to device - // simParams.syncMemberToDevice(offsetof(DEMSimParams, binSize)); - simParams.toDevice(); + // binSize is now calculated; queue the device refresh on kT's stream so the next kernel sees it without forcing a + // host-side barrier. + simParams.toDeviceAsync(streamInfo.stream); +} + +inline void DEMKinematicThread::computeMarginFromAbsv(float* absVel_owner, float* absAngVel_owner) { + size_t blocks_needed; + blocks_needed = (simParams->nSpheresGM + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + misc_kernels->kernel("computeMarginFromAbsv_implSph") + .instantiate() + .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, absVel_owner, absAngVel_owner, &(stateParams.ts), &(stateParams.maxDrift), + (size_t)simParams->nSpheresGM); + } + blocks_needed = (simParams->nTriGM + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + misc_kernels->kernel("computeMarginFromAbsv_implTri") + .instantiate() + .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, absVel_owner, absAngVel_owner, &(stateParams.ts), &(stateParams.maxDrift), + maxTriTriPenetration.data(), solverFlags.meshUniversalContact, (size_t)simParams->nTriGM); + } + blocks_needed = (simParams->nAnalGM + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + misc_kernels->kernel("computeMarginFromAbsv_implAnal") + .instantiate() + .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) + .launch(&simParams, &granData, absVel_owner, absAngVel_owner, &(stateParams.ts), &(stateParams.maxDrift), + (size_t)simParams->nAnalGM); + } + DEME_GPU_DEBUG_SYNC(streamInfo.stream); } inline void DEMKinematicThread::unpackMyBuffer() { @@ -114,62 +165,57 @@ inline void DEMKinematicThread::unpackMyBuffer() { cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(granData->oriQz, oriQ3_buffer.data(), simParams->nOwnerBodies * sizeof(oriQ_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->marginSize, absVel_buffer.data(), simParams->nOwnerBodies * sizeof(float), - cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(&(stateParams.ts), &(stateParams.ts_buffer), sizeof(float), cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(&(stateParams.maxDrift), &(stateParams.maxDrift_buffer), sizeof(unsigned int), cudaMemcpyDeviceToDevice)); + if (!simParams->meshParticlesLowPoly && simParams->nTriGM > 0) { + DEME_GPU_CALL(cudaMemcpy(maxTriTriPenetration.data(), maxTriTriPenetration_buffer.data(), + (size_t)simParams->nTriGM * sizeof(float), cudaMemcpyDeviceToDevice)); + } + // Use two temp arrays to store absVel and absAngVel's buffer + float* absVel_owner = + (float*)solverScratchSpace.allocateTempVector("absVel_owner", simParams->nOwnerBodies * sizeof(float)); + float* absAngVel_owner = + (float*)solverScratchSpace.allocateTempVector("absAngVel_owner", simParams->nOwnerBodies * sizeof(float)); + DEME_GPU_CALL(cudaMemcpy(absVel_owner, absVel_buffer.data(), simParams->nOwnerBodies * sizeof(float), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(absAngVel_owner, absAngVel_buffer.data(), simParams->nOwnerBodies * sizeof(float), + cudaMemcpyDeviceToDevice)); - // Whatever drift value dT says, kT listens; unless kinematicMaxFutureDrift is negative in which case the user - // explicitly said not caring the future drift. - stateParams.maxDrift.toHost(); - pSchedSupport->kinematicMaxFutureDrift = (pSchedSupport->kinematicMaxFutureDrift.load() < 0.) - ? pSchedSupport->kinematicMaxFutureDrift.load() - : *(stateParams.maxDrift); - - // Need to reduce to check if max velocity is exceeded (right now, array marginSize is still storing absv...) - cubMaxReduce(granData->marginSize, &(stateParams.maxVel), simParams->nOwnerBodies, streamInfo.stream, + // Make sure we don't have velocity that is too high + cubMaxReduce(absVel_owner, &(stateParams.maxVel), simParams->nOwnerBodies, streamInfo.stream, + solverScratchSpace); + cubMaxReduce(absAngVel_owner, &(stateParams.maxAngVel), simParams->nOwnerBodies, streamInfo.stream, solverScratchSpace); // Get the reduced maxVel value stateParams.maxVel.toHost(); - if (*(stateParams.maxVel) > simParams->errOutVel || (!std::isfinite(*(stateParams.maxVel)))) { + stateParams.maxAngVel.toHost(); + if (*stateParams.maxVel > simParams->errOutVel || !std::isfinite(*stateParams.maxVel) || + *stateParams.maxAngVel > simParams->errOutAngVel || !std::isfinite(*stateParams.maxAngVel)) { DEME_ERROR( - "System max velocity is %.7g, exceeded max allowance (%.7g).\nIf this velocity is not abnormal and you " + "System max velocity/angular velocity is %.7g/%.7g, exceeded max allowance (%.7g/%.7g).\nIf this velocity " + "is not abnormal and you " "want to increase this allowance, use SetErrorOutVelocity before initializing simulation.\nOtherwise, the " "simulation may have diverged and relaxing the physics may help, such as decreasing the step size and " "modifying material properties.\nIf this happens at the start of simulation, check if there are initial " "penetrations, a.k.a. elements initialized inside walls.", - *(stateParams.maxVel), simParams->errOutVel); - } else if (*(stateParams.maxVel) > - simParams->approxMaxVel) { // If maxVel is larger than the user estimation, that is an anomaly - DEME_STEP_ANOMALY("Simulation entity velocity reached %.6g, over the user-estimated %.6g", - *(stateParams.maxVel), simParams->approxMaxVel); - anomalies.over_max_vel = true; + *(stateParams.maxVel), *(stateParams.maxAngVel), simParams->errOutVel, simParams->errOutAngVel); } - - // kT will need to derive the thickness of the CD margin, based on dT's info on system vel. - if (!solverFlags.isExpandFactorFixed) { - // This kernel will turn absv to marginSize, and if a vel is over max, it will clamp it. - // Converting to size_t is SUPER important... CUDA kernel call basically does not have type conversion. - size_t blocks_needed = (simParams->nOwnerBodies + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - misc_kernels->kernel("computeMarginFromAbsv") - .instantiate() - .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData, &(stateParams.ts), &(stateParams.maxDrift), - (size_t)(simParams->nOwnerBodies)); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - } else { // If isExpandFactorFixed, then just fill in that constant array. - size_t blocks_needed = (simParams->nOwnerBodies + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - misc_kernels->kernel("fillMarginValues") - .instantiate() - .configure(dim3(blocks_needed), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&simParams, &granData, (size_t)(simParams->nOwnerBodies)); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); + if (*stateParams.maxVel > + simParams->dyn.approxMaxVel) { // If maxVel is larger than the user estimation, that is an anomaly + // This prints when verbosity higher than METRIC + DEME_STATUS("OVER_MAX_VEL", "Simulation entity velocity reached %.6g, over the user-estimated max (%.6g)", + *stateParams.maxVel, simParams->dyn.approxMaxVel); } + DEME_DEBUG_PRINTF("kT received an update, max vel: %.6g", *stateParams.maxVel); + DEME_DEBUG_PRINTF("kT received an update, max ang vel: %.6g", *stateParams.maxAngVel); - DEME_DEBUG_PRINTF("kT received a velocity update: %.6g", *(stateParams.maxVel)); - // DEME_DEBUG_PRINTF("A margin of thickness %.6g is added", simParams->beta); + // Now update the future drift info. Whatever drift value dT says, kT listens; unless kinematicMaxFutureDrift is + // negative in which case the user explicitly said not caring the future drift. + stateParams.maxDrift.toHost(); + pSchedSupport->kinematicMaxFutureDrift = (pSchedSupport->kinematicMaxFutureDrift.load() < 0.) + ? pSchedSupport->kinematicMaxFutureDrift.load() + : *(stateParams.maxDrift); // Family number is a typical changable quantity on-the-fly. If this flag is on, kT received changes from dT. if (solverFlags.canFamilyChangeOnDevice) { @@ -188,25 +234,61 @@ inline void DEMKinematicThread::unpackMyBuffer() { // dT won't be sending if kT is loading, so it is safe solverFlags.willMeshDeform = false; } + + // kT will need to derive the thickness of the CD margin, based on dT's info on system vel. + if (!solverFlags.isExpandFactorFixed) { + // This kernel will turn absv to marginSize, and if a vel is over max, it will clamp it. + // Converting to size_t is SUPER important... CUDA kernel call basically does not have type conversion. + computeMarginFromAbsv(absVel_owner, absAngVel_owner); + } else { // If isExpandFactorFixed, then just fill in that constant array. + // This one is statically compiled, unlike the other branch + fillMarginValues(&simParams, &granData, (size_t)(simParams->nSpheresGM), (size_t)(simParams->nTriGM), + (size_t)(simParams->nAnalGM), streamInfo.stream); + } + + solverScratchSpace.finishUsingTempVector("absVel_owner"); + solverScratchSpace.finishUsingTempVector("absAngVel_owner"); } +//// TODO: Fix the transfer; is primitive transfer needed at all? inline void DEMKinematicThread::sendToTheirBuffer() { - DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_nContactPairs, &(solverScratchSpace.numContacts), sizeof(size_t), + // Send over the sum of contacts + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_nPrimitiveContacts, &(solverScratchSpace.numPrimitiveContacts), + sizeof(size_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_nPatchContacts, &(solverScratchSpace.numContacts), sizeof(size_t), cudaMemcpyDeviceToDevice)); // Resize dT owned buffers before usage - if (*solverScratchSpace.numContacts > dT->buffer_size) { - transferArraysResize(*solverScratchSpace.numContacts); + if (*solverScratchSpace.numPrimitiveContacts > dT->primitiveBufferSize) { + transferPrimitivesArraysResize(*solverScratchSpace.numPrimitiveContacts); + } + // Resize the patch-contact transfer array too + if (*solverScratchSpace.numContacts > dT->patchBufferSize) { + transferPatchArrayResize(*solverScratchSpace.numContacts); } - DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idGeometryA, granData->idGeometryA, + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idPrimitiveA, granData->idPrimitiveA, + (*solverScratchSpace.numPrimitiveContacts) * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idPrimitiveB, granData->idPrimitiveB, + (*solverScratchSpace.numPrimitiveContacts) * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_contactType, granData->contactTypePrimitive, + (*solverScratchSpace.numPrimitiveContacts) * sizeof(contact_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_geomToPatchMap, granData->geomToPatchMap, + (*solverScratchSpace.numPrimitiveContacts) * sizeof(contactPairs_t), + cudaMemcpyDeviceToDevice)); + + // NEW: Transfer separate patch IDs and mapping array + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idPatchA, granData->idPatchA, (*solverScratchSpace.numContacts) * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idGeometryB, granData->idGeometryB, + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_idPatchB, granData->idPatchB, (*solverScratchSpace.numContacts) * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_contactType, granData->contactType, + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_contactTypePatch, granData->contactTypePatch, (*solverScratchSpace.numContacts) * sizeof(contact_t), cudaMemcpyDeviceToDevice)); - // DEME_MIGRATE_TO_DEVICE(dT->idGeometryA_buffer, dT->streamInfo.device, streamInfo.stream); - // DEME_MIGRATE_TO_DEVICE(dT->idGeometryB_buffer, dT->streamInfo.device, streamInfo.stream); - // DEME_MIGRATE_TO_DEVICE(dT->contactType_buffer, dT->streamInfo.device, streamInfo.stream); + DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_contactPatchIsland, granData->contactPatchIsland, + (*solverScratchSpace.numContacts) * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + + // DEME_MIGRATE_TO_DEVICE(dT->idPrimitiveA_buffer, dT->streamInfo.device, streamInfo.stream); + // DEME_MIGRATE_TO_DEVICE(dT->idPrimitiveB_buffer, dT->streamInfo.device, streamInfo.stream); + // DEME_MIGRATE_TO_DEVICE(dT->contactTypePrimitive_buffer, dT->streamInfo.device, streamInfo.stream); if (!solverFlags.isHistoryless) { DEME_GPU_CALL(cudaMemcpy(granData->pDTOwnedBuffer_contactMapping, granData->contactMapping, (*solverScratchSpace.numContacts) * sizeof(contactPairs_t), cudaMemcpyDeviceToDevice)); @@ -240,14 +322,14 @@ void DEMKinematicThread::workerThread() { // via memcpy while (!pSchedSupport->dynamicDone) { // Before producing something, a new work order should be in place. Wait on it. - if (!pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh) { + if (!pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.load(std::memory_order_acquire)) { timers.GetTimer("Wait for dT update").start(); pSchedSupport->schedulingStats.nTimesKinematicHeldBack++; std::unique_lock lock(pSchedSupport->kinematicCanProceed); // kT never got locked in here indefinitely because, dT will always send a cv_KinematicCanProceed signal // AFTER setting dynamicDone to true, if dT is about to finish - while (!pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh) { + while (!pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.load(std::memory_order_acquire)) { // Loop to avoid spurious wakeups pSchedSupport->cv_KinematicCanProceed.wait(lock); } @@ -272,7 +354,7 @@ void DEMKinematicThread::workerThread() { // Make it clear that the data for most recent work order has been used, in case there is interest in // updating it - pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh = false; + pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.store(false, std::memory_order_release); // figure out the amount of shared mem // cudaDeviceGetAttribute.cudaDevAttrMaxSharedMemoryPerBlock @@ -281,9 +363,12 @@ void DEMKinematicThread::workerThread() { // For auto-adjusting bin size, this part of code is encapsuled in an accumulative timer. CDAccumTimer.Begin(); contactDetection(bin_sphere_kernels, bin_triangle_kernels, sphere_contact_kernels, sphTri_contact_kernels, - history_kernels, granData, simParams, solverFlags, verbosity, idGeometryA, idGeometryB, - contactType, previous_idGeometryA, previous_idGeometryB, previous_contactType, - contactPersistency, contactMapping, streamInfo.stream, solverScratchSpace, timers, + granData, simParams, solverFlags, verbosity, idPrimitiveA, idPrimitiveB, + contactTypePrimitive, previous_idPrimitiveA, previous_idPrimitiveB, + previous_contactTypePrimitive, previous_primitivePatchIsland, contactPersistency, + contactMapping, idPatchA, idPatchB, previous_idPatchA, previous_idPatchB, contactTypePatch, + previous_contactTypePatch, contactPatchIsland, previous_contactPatchIsland, + typeStartCountPatchMap, geomToPatchMap, streamInfo.stream, solverScratchSpace, timers, stateParams); CDAccumTimer.End(); @@ -295,7 +380,7 @@ void DEMKinematicThread::workerThread() { std::lock_guard lock(pSchedSupport->dynamicOwnedBuffer_AccessCoordination); sendToTheirBuffer(); } - pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh = true; + pSchedSupport->dynamicOwned_Prod2ConsBuffer_isFresh.store(true, std::memory_order_release); pSchedSupport->schedulingStats.nDynamicUpdates++; timers.GetTimer("Send to dT buffer").stop(); @@ -337,57 +422,6 @@ void DEMKinematicThread::changeFamily(unsigned int ID_from, unsigned int ID_to) familyID.toDevice(); } -void DEMKinematicThread::changeOwnerSizes(const std::vector& IDs, const std::vector& factors) { - // Set the gpu for this thread - cudaSetDevice(streamInfo.device); - // cudaStream_t new_stream; - // cudaStreamCreate(&new_stream); - - // First get IDs and factors to device side - size_t IDSize = IDs.size() * sizeof(bodyID_t); - bodyID_t* dIDs = (bodyID_t*)solverScratchSpace.allocateTempVector("dIDs", IDSize); - DEME_GPU_CALL(cudaMemcpy(dIDs, IDs.data(), IDSize, cudaMemcpyHostToDevice)); - size_t factorSize = factors.size() * sizeof(float); - float* dFactors = (float*)solverScratchSpace.allocateTempVector("dFactors", factorSize); - DEME_GPU_CALL(cudaMemcpy(dFactors, factors.data(), factorSize, cudaMemcpyHostToDevice)); - - size_t idBoolSize = (size_t)simParams->nOwnerBodies * sizeof(notStupidBool_t); - size_t ownerFactorSize = (size_t)simParams->nOwnerBodies * sizeof(float); - // Bool table for whether this owner should change - notStupidBool_t* idBool = (notStupidBool_t*)solverScratchSpace.allocateTempVector("idBool", idBoolSize); - DEME_GPU_CALL(cudaMemset(idBool, 0, idBoolSize)); - float* ownerFactors = (float*)solverScratchSpace.allocateTempVector("ownerFactors", ownerFactorSize); - size_t blocks_needed_for_marking = (IDs.size() + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - - // Mark on the bool array those owners that need a change - misc_kernels->kernel("markOwnerToChange") - .instantiate() - .configure(dim3(blocks_needed_for_marking), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(idBool, ownerFactors, dIDs, dFactors, (size_t)IDs.size()); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - - // Change the size of the sphere components in question - size_t blocks_needed_for_changing = - (simParams->nSpheresGM + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - misc_kernels->kernel("modifyComponents") - .instantiate("deme::DEMDataKT") - .configure(dim3(blocks_needed_for_changing), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, streamInfo.stream) - .launch(&granData, idBool, ownerFactors, (size_t)simParams->nSpheresGM); - DEME_GPU_CALL(cudaStreamSynchronize(streamInfo.stream)); - - solverScratchSpace.finishUsingTempVector("dIDs"); - solverScratchSpace.finishUsingTempVector("dFactors"); - solverScratchSpace.finishUsingTempVector("idBool"); - solverScratchSpace.finishUsingTempVector("ownerFactors"); - // cudaStreamDestroy(new_stream); - - // Update them back to host - relPosSphereX.toHost(); - relPosSphereY.toHost(); - relPosSphereZ.toHost(); - radiiSphere.toHost(); -} - void DEMKinematicThread::startThread() { std::lock_guard lock(pSchedSupport->kinematicStartLock); pSchedSupport->kinematicStarted = true; @@ -399,7 +433,7 @@ void DEMKinematicThread::breakWaitingStatus() { pSchedSupport->dynamicDone = true; // We distrubed kinematicOwned_Cons2ProdBuffer_isFresh and kTShouldReset here, but it matters not, as when // breakWaitingStatus is called, they will always be reset to default soon - pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh = true; + pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.store(true, std::memory_order_release); kTShouldReset = true; std::lock_guard lock(pSchedSupport->kinematicCanProceed); @@ -408,7 +442,7 @@ void DEMKinematicThread::breakWaitingStatus() { void DEMKinematicThread::resetUserCallStat() { // Reset kT stats variables, making ready for next user call - pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh = false; + pSchedSupport->kinematicOwned_Cons2ProdBuffer_isFresh.store(false, std::memory_order_release); kTShouldReset = false; // My ingredient production date is... unknown now pSchedSupport->kinematicIngredProdDateStamp = -1; @@ -438,28 +472,52 @@ void DEMKinematicThread::packDataPointers() { oriQx.bindDevicePointer(&(granData->oriQx)); oriQy.bindDevicePointer(&(granData->oriQy)); oriQz.bindDevicePointer(&(granData->oriQz)); - marginSize.bindDevicePointer(&(granData->marginSize)); - idGeometryA.bindDevicePointer(&(granData->idGeometryA)); - idGeometryB.bindDevicePointer(&(granData->idGeometryB)); - contactType.bindDevicePointer(&(granData->contactType)); + marginSizeSphere.bindDevicePointer(&(granData->marginSizeSphere)); + marginSizeTriangle.bindDevicePointer(&(granData->marginSizeTriangle)); + marginSizeAnalytical.bindDevicePointer(&(granData->marginSizeAnalytical)); + idPrimitiveA.bindDevicePointer(&(granData->idPrimitiveA)); + idPrimitiveB.bindDevicePointer(&(granData->idPrimitiveB)); + contactTypePrimitive.bindDevicePointer(&(granData->contactTypePrimitive)); contactPersistency.bindDevicePointer(&(granData->contactPersistency)); - previous_idGeometryA.bindDevicePointer(&(granData->previous_idGeometryA)); - previous_idGeometryB.bindDevicePointer(&(granData->previous_idGeometryB)); - previous_contactType.bindDevicePointer(&(granData->previous_contactType)); + previous_idPrimitiveA.bindDevicePointer(&(granData->previous_idPrimitiveA)); + previous_idPrimitiveB.bindDevicePointer(&(granData->previous_idPrimitiveB)); + previous_contactTypePrimitive.bindDevicePointer(&(granData->previous_contactTypePrimitive)); contactMapping.bindDevicePointer(&(granData->contactMapping)); + + // NEW: Bind separate patch ID and mapping array pointers + idPatchA.bindDevicePointer(&(granData->idPatchA)); + idPatchB.bindDevicePointer(&(granData->idPatchB)); + previous_idPatchA.bindDevicePointer(&(granData->previous_idPatchA)); + previous_idPatchB.bindDevicePointer(&(granData->previous_idPatchB)); + contactTypePatch.bindDevicePointer(&(granData->contactTypePatch)); + previous_contactTypePatch.bindDevicePointer(&(granData->previous_contactTypePatch)); + contactPatchIsland.bindDevicePointer(&(granData->contactPatchIsland)); + previous_contactPatchIsland.bindDevicePointer(&(granData->previous_contactPatchIsland)); + geomToPatchMap.bindDevicePointer(&(granData->geomToPatchMap)); + familyMaskMatrix.bindDevicePointer(&(granData->familyMasks)); familyExtraMarginSize.bindDevicePointer(&(granData->familyExtraMarginSize)); + ownerCombinedMaster.bindDevicePointer(&(granData->ownerCombinedMaster)); // The offset info that indexes into the template arrays ownerClumpBody.bindDevicePointer(&(granData->ownerClumpBody)); clumpComponentOffset.bindDevicePointer(&(granData->clumpComponentOffset)); clumpComponentOffsetExt.bindDevicePointer(&(granData->clumpComponentOffsetExt)); + ownerAnalBody.bindDevicePointer(&(granData->ownerAnalBody)); // Mesh-related - ownerMesh.bindDevicePointer(&(granData->ownerMesh)); + ownerTriMesh.bindDevicePointer(&(granData->ownerTriMesh)); + ownerMeshConvex.bindDevicePointer(&(granData->ownerMeshConvex)); + ownerMeshNeverWinner.bindDevicePointer(&(granData->ownerMeshNeverWinner)); + triPatchID.bindDevicePointer(&(granData->triPatchID)); + triNeighborIndex.bindDevicePointer(&(granData->triNeighborIndex)); + triNeighbor1.bindDevicePointer(&(granData->triNeighbor1)); + triNeighbor2.bindDevicePointer(&(granData->triNeighbor2)); + triNeighbor3.bindDevicePointer(&(granData->triNeighbor3)); relPosNode1.bindDevicePointer(&(granData->relPosNode1)); relPosNode2.bindDevicePointer(&(granData->relPosNode2)); relPosNode3.bindDevicePointer(&(granData->relPosNode3)); + maxTriTriPenetration.bindDevicePointer(&(granData->maxTriTriPenetration)); // Template array pointers radiiSphere.bindDevicePointer(&(granData->radiiSphere)); @@ -478,23 +536,39 @@ void DEMKinematicThread::migrateDataToDevice() { oriQx.toDeviceAsync(streamInfo.stream); oriQy.toDeviceAsync(streamInfo.stream); oriQz.toDeviceAsync(streamInfo.stream); - marginSize.toDeviceAsync(streamInfo.stream); - idGeometryA.toDeviceAsync(streamInfo.stream); - idGeometryB.toDeviceAsync(streamInfo.stream); - contactType.toDeviceAsync(streamInfo.stream); + idPrimitiveA.toDeviceAsync(streamInfo.stream); + idPrimitiveB.toDeviceAsync(streamInfo.stream); + contactTypePrimitive.toDeviceAsync(streamInfo.stream); contactPersistency.toDeviceAsync(streamInfo.stream); - previous_idGeometryA.toDeviceAsync(streamInfo.stream); - previous_idGeometryB.toDeviceAsync(streamInfo.stream); - previous_contactType.toDeviceAsync(streamInfo.stream); + previous_idPrimitiveA.toDeviceAsync(streamInfo.stream); + previous_idPrimitiveB.toDeviceAsync(streamInfo.stream); + previous_contactTypePrimitive.toDeviceAsync(streamInfo.stream); contactMapping.toDeviceAsync(streamInfo.stream); + previous_idPatchA.toDeviceAsync(streamInfo.stream); + previous_idPatchB.toDeviceAsync(streamInfo.stream); + contactTypePatch.toDeviceAsync(streamInfo.stream); + previous_contactTypePatch.toDeviceAsync(streamInfo.stream); + contactPatchIsland.toDeviceAsync(streamInfo.stream); + previous_contactPatchIsland.toDeviceAsync(streamInfo.stream); familyMaskMatrix.toDeviceAsync(streamInfo.stream); familyExtraMarginSize.toDeviceAsync(streamInfo.stream); + if (ownerCombinedMaster.size() > 0) { + ownerCombinedMaster.toDeviceAsync(streamInfo.stream); + } ownerClumpBody.toDeviceAsync(streamInfo.stream); clumpComponentOffset.toDeviceAsync(streamInfo.stream); clumpComponentOffsetExt.toDeviceAsync(streamInfo.stream); - - ownerMesh.toDeviceAsync(streamInfo.stream); + ownerAnalBody.toDeviceAsync(streamInfo.stream); + + ownerTriMesh.toDeviceAsync(streamInfo.stream); + ownerMeshConvex.toDeviceAsync(streamInfo.stream); + ownerMeshNeverWinner.toDeviceAsync(streamInfo.stream); + triPatchID.toDeviceAsync(streamInfo.stream); + triNeighborIndex.toDeviceAsync(streamInfo.stream); + triNeighbor1.toDeviceAsync(streamInfo.stream); + triNeighbor2.toDeviceAsync(streamInfo.stream); + triNeighbor3.toDeviceAsync(streamInfo.stream); relPosNode1.toDeviceAsync(streamInfo.stream); relPosNode2.toDeviceAsync(streamInfo.stream); relPosNode3.toDeviceAsync(streamInfo.stream); @@ -520,10 +594,18 @@ void DEMKinematicThread::migrateDeviceModifiableInfoToHost() { void DEMKinematicThread::packTransferPointers(DEMDynamicThread* dT) { // Set the pointers to dT owned buffers - granData->pDTOwnedBuffer_nContactPairs = &(dT->nContactPairs_buffer); - granData->pDTOwnedBuffer_idGeometryA = dT->idGeometryA_buffer.data(); - granData->pDTOwnedBuffer_idGeometryB = dT->idGeometryB_buffer.data(); - granData->pDTOwnedBuffer_contactType = dT->contactType_buffer.data(); + granData->pDTOwnedBuffer_nPrimitiveContacts = &(dT->nPrimitiveContactPairs_buffer); + granData->pDTOwnedBuffer_nPatchContacts = &(dT->nPatchContactPairs_buffer); + granData->pDTOwnedBuffer_idPrimitiveA = dT->idPrimitiveA_buffer.data(); + granData->pDTOwnedBuffer_idPrimitiveB = dT->idPrimitiveB_buffer.data(); + granData->pDTOwnedBuffer_contactType = dT->contactTypePrimitive_buffer.data(); + granData->pDTOwnedBuffer_geomToPatchMap = dT->geomToPatchMap_buffer.data(); + + // NEW: Set pointers for separate patch arrays + granData->pDTOwnedBuffer_idPatchA = dT->idPatchA_buffer.data(); + granData->pDTOwnedBuffer_idPatchB = dT->idPatchB_buffer.data(); + granData->pDTOwnedBuffer_contactTypePatch = dT->contactTypePatch_buffer.data(); + granData->pDTOwnedBuffer_contactPatchIsland = dT->contactPatchIsland_buffer.data(); granData->pDTOwnedBuffer_contactMapping = dT->contactMapping_buffer.data(); } @@ -543,8 +625,11 @@ void DEMKinematicThread::setSimParams(unsigned char nvXp2, double ts_size, float expand_factor, float approx_max_vel, + double max_tritri_penetration, + float tritri_contact_rejection_ratio, float expand_safety_param, float expand_safety_adder, + bool use_angvel_margin, const std::set& contact_wildcards, const std::set& owner_wildcards, const std::set& geo_wildcards) { @@ -553,18 +638,22 @@ void DEMKinematicThread::setSimParams(unsigned char nvXp2, simParams->nvZp2 = nvZp2; simParams->l = l; simParams->voxelSize = voxelSize; - simParams->binSize = binSize; + simParams->dyn.binSize = binSize; + simParams->dyn.inv_binSize = 1. / binSize; simParams->LBFX = LBFPoint.x; simParams->LBFY = LBFPoint.y; simParams->LBFZ = LBFPoint.z; simParams->Gx = G.x; simParams->Gy = G.y; simParams->Gz = G.z; - simParams->h = ts_size; - simParams->beta = expand_factor; // If beta is auto-adapting, this assignment has no effect - simParams->approxMaxVel = approx_max_vel; - simParams->expSafetyMulti = expand_safety_param; - simParams->expSafetyAdder = expand_safety_adder; + simParams->dyn.h = ts_size; + simParams->dyn.beta = expand_factor; // If beta is auto-adapting, this assignment has no effect + simParams->dyn.approxMaxVel = approx_max_vel; + simParams->dyn.expSafetyMulti = expand_safety_param; + simParams->dyn.expSafetyAdder = expand_safety_adder; + simParams->capTriTriPenetration = max_tritri_penetration; + simParams->triTriContactRejectionRatio = tritri_contact_rejection_ratio; + simParams->useAngVelMargin = use_angvel_margin ? 1 : 0; simParams->nbX = nbX; simParams->nbY = nbY; simParams->nbZ = nbZ; @@ -582,6 +671,7 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, size_t nTriMeshes, size_t nSpheresGM, size_t nTriGM, + size_t nTriNeighbors, unsigned int nAnalGM, size_t nExtraContacts, unsigned int nMassProperties, @@ -618,7 +708,12 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DUAL_ARRAY_RESIZE(oriQx, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(oriQy, nOwnerBodies, 0); DEME_DUAL_ARRAY_RESIZE(oriQz, nOwnerBodies, 0); - DEME_DUAL_ARRAY_RESIZE(marginSize, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshConvex, nOwnerBodies, 0); + DEME_DUAL_ARRAY_RESIZE(ownerMeshNeverWinner, nOwnerBodies, 0); + DEME_DEVICE_ARRAY_RESIZE(marginSizeSphere, nSpheresGM); + DEME_DEVICE_ARRAY_RESIZE(marginSizeAnalytical, nAnalGM); + DEME_DEVICE_ARRAY_RESIZE(marginSizeTriangle, nTriGM); + DEME_DEVICE_ARRAY_RESIZE(maxTriTriPenetration, nTriGM); // Transfer buffer arrays // It is cudaMalloc-ed memory, not on host, because we want explicit locality control of buffers @@ -634,6 +729,8 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DEVICE_ARRAY_RESIZE(oriQ2_buffer, nOwnerBodies); DEME_DEVICE_ARRAY_RESIZE(oriQ3_buffer, nOwnerBodies); DEME_DEVICE_ARRAY_RESIZE(absVel_buffer, nOwnerBodies); + DEME_DEVICE_ARRAY_RESIZE(absAngVel_buffer, nOwnerBodies); + DEME_DEVICE_ARRAY_RESIZE(maxTriTriPenetration_buffer, nTriGM); // DEME_ADVISE_DEVICE(voxelID_buffer, dT->streamInfo.device); // DEME_ADVISE_DEVICE(locX_buffer, dT->streamInfo.device); // DEME_ADVISE_DEVICE(locY_buffer, dT->streamInfo.device); @@ -660,11 +757,19 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, DEME_DUAL_ARRAY_RESIZE(ownerClumpBody, nSpheresGM, 0); // Resize to the number of triangle facets - DEME_DUAL_ARRAY_RESIZE(ownerMesh, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(ownerTriMesh, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(triPatchID, nTriGM, 0); + DEME_DUAL_ARRAY_RESIZE(triNeighborIndex, nTriGM, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor1, nTriNeighbors, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor2, nTriNeighbors, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(triNeighbor3, nTriNeighbors, NULL_BODYID); DEME_DUAL_ARRAY_RESIZE(relPosNode1, nTriGM, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(relPosNode2, nTriGM, make_float3(0)); DEME_DUAL_ARRAY_RESIZE(relPosNode3, nTriGM, make_float3(0)); + // And analytical geometry owner array + DEME_DUAL_ARRAY_RESIZE(ownerAnalBody, nAnalGM, 0); + if (solverFlags.useClumpJitify) { DEME_DUAL_ARRAY_RESIZE(clumpComponentOffset, nSpheresGM, 0); // This extended component offset array can hold offset numbers even for big clumps (whereas @@ -684,20 +789,33 @@ void DEMKinematicThread::allocateGPUArrays(size_t nOwnerBodies, } // Arrays for kT produced contact info - // The following several arrays will have variable sizes, so here we only used an estimate. My estimate of total - // contact pairs is 2n, and I think the max is 6n (although I can't prove it). Note the estimate should be large - // enough to decrease the number of reallocations in the simulation, but not too large that eats too much memory. + // The following several arrays will have variable sizes, so here we only used a good initial value. My estimate of + // total contact pairs is ~n, and I think the max is 6n. { - size_t cnt_arr_size = DEME_MAX(*solverScratchSpace.numPrevContacts, nSpheresGM * DEME_INIT_CNT_MULTIPLIER); - DEME_DUAL_ARRAY_RESIZE(idGeometryA, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(idGeometryB, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(contactType, cnt_arr_size, NOT_A_CONTACT); + size_t cnt_arr_size = DEME_MAX(*solverScratchSpace.numPrevContacts, INITIAL_CONTACT_ARRAY_SIZE); + DEME_DUAL_ARRAY_RESIZE(idPrimitiveA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(idPrimitiveB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePrimitive, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(contactPersistency, cnt_arr_size, CONTACT_NOT_PERSISTENT); + + DEME_DUAL_ARRAY_RESIZE(idPatchA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(idPatchB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(contactTypePatch, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(contactPatchIsland, cnt_arr_size, NULL_BODYID); + DEME_DUAL_ARRAY_RESIZE(geomToPatchMap, cnt_arr_size, 0); + if (!solverFlags.isHistoryless) { - DEME_DUAL_ARRAY_RESIZE(contactPersistency, cnt_arr_size, CONTACT_NOT_PERSISTENT); - DEME_DUAL_ARRAY_RESIZE(previous_idGeometryA, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(previous_idGeometryB, cnt_arr_size, 0); - DEME_DUAL_ARRAY_RESIZE(previous_contactType, cnt_arr_size, NOT_A_CONTACT); + if (solverFlags.useStablePatchIslandIDs) { + DEME_DUAL_ARRAY_RESIZE(previous_idPrimitiveA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(previous_idPrimitiveB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(previous_contactTypePrimitive, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(previous_primitivePatchIsland, cnt_arr_size, NULL_BODYID); + } DEME_DUAL_ARRAY_RESIZE(contactMapping, cnt_arr_size, NULL_MAPPING_PARTNER); + DEME_DUAL_ARRAY_RESIZE(previous_idPatchA, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(previous_idPatchB, cnt_arr_size, 0); + DEME_DUAL_ARRAY_RESIZE(previous_contactTypePatch, cnt_arr_size, NOT_A_CONTACT); + DEME_DUAL_ARRAY_RESIZE(previous_contactPatchIsland, cnt_arr_size, NULL_BODYID); } } } @@ -711,12 +829,21 @@ void DEMKinematicThread::registerPolicies(const std::vector& fa void DEMKinematicThread::populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, const ClumpTemplateFlatten& clump_templates, + const std::vector& ext_obj_comp_num, size_t nExistOwners, size_t nExistSpheres, - size_t nExistingFacets) { + size_t nExistingFacets, + size_t nExistingMeshPatches, + size_t nExistingTriNeighbors) { // All the input vectors should have the same length, nClumpTopo size_t k = 0; std::vector prescans_comp; @@ -801,8 +928,15 @@ void DEMKinematicThread::populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, + const std::vector& ext_obj_comp_num, const std::vector& family_mask_matrix, const ClumpTemplateFlatten& clump_templates) { // Get the info into the GPU memory from the host side. Can this process be more efficient? Maybe, but it's @@ -844,15 +1002,24 @@ void DEMKinematicThread::initGPUArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, + const std::vector& ext_obj_comp_num, const std::vector& family_mask_matrix, const ClumpTemplateFlatten& clump_templates, size_t nExistingOwners, @@ -860,18 +1027,24 @@ void DEMKinematicThread::updateClumpMeshArrays(const std::vector& dT_data, size_t nContacts) { // Store the incoming info in kT's arrays - // Note kT never had the responsibility to migrate contact info to host, even at UpdateClumps, as even in this case + // Note kT never had the responsibility to migrate contact info to host, even at Update, as even in this case // its host-side update comes from dT - overwritePrevContactArrays(granData, dT_data, previous_idGeometryA, previous_idGeometryB, previous_contactType, - simParams, contactPersistency, solverScratchSpace, streamInfo.stream, nContacts); + overwritePrevContactArrays(granData, dT_data, previous_idPatchA, previous_idPatchB, previous_contactTypePatch, + previous_contactPatchIsland, typeStartCountPatchMap, simParams, solverScratchSpace, + streamInfo.stream, nContacts); DEME_DEBUG_PRINTF("Number of contacts after a user-manual contact load: %zu", nContacts); DEME_DEBUG_PRINTF("Number of spheres after a user-manual contact load: %zu", (size_t)simParams->nSpheresGM); } @@ -880,37 +1053,32 @@ void DEMKinematicThread::jitifyKernels(const std::unordered_map& JitifyOptions) { // First one is bin_sphere_kernels kernels, which figure out the bin--sphere touch pairs { - bin_sphere_kernels = std::make_shared(std::move(JitHelper::buildProgram( + bin_sphere_kernels = std::make_shared(std::move(JitHelper::buildProgram( "DEMBinSphereKernels", JitHelper::KERNEL_DIR / "DEMBinSphereKernels.cu", Subs, JitifyOptions))); } // Then CD kernels { - sphere_contact_kernels = std::make_shared(std::move( + sphere_contact_kernels = std::make_shared(std::move( JitHelper::buildProgram("DEMContactKernels_SphereSphere", JitHelper::KERNEL_DIR / "DEMContactKernels_SphereSphere.cu", Subs, JitifyOptions))); } - // Mesh kernels are only needed when triangle geometry exists. Analytical - // walls/planes use the non-mesh contact path. - if (solverFlags.hasMeshes) { - bin_triangle_kernels = std::make_shared(std::move(JitHelper::buildProgram( + // Then triangle--bin intersection-related kernels + { + bin_triangle_kernels = std::make_shared(std::move(JitHelper::buildProgram( "DEMBinTriangleKernels", JitHelper::KERNEL_DIR / "DEMBinTriangleKernels.cu", Subs, JitifyOptions))); - sphTri_contact_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMContactKernels_SphereTriangle", JitHelper::KERNEL_DIR / "DEMContactKernels_SphereTriangle.cu", Subs, - JitifyOptions))); - } else { - bin_triangle_kernels.reset(); - sphTri_contact_kernels.reset(); } - // Then contact history mapping kernels + // Then sphere--triangle contact detection-related kernels { - history_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMHistoryMappingKernels", JitHelper::KERNEL_DIR / "DEMHistoryMappingKernels.cu", Subs, JitifyOptions))); + sphTri_contact_kernels = std::make_shared(std::move(JitHelper::buildProgram( + "DEMContactKernels_SphTri_TriTri", JitHelper::KERNEL_DIR / "DEMContactKernels_SphTri_TriTri.cu", Subs, + JitifyOptions))); } - // Then misc kernels + // Then misc. { - misc_kernels = std::make_shared(std::move(JitHelper::buildProgram( - "DEMMiscKernels", JitHelper::KERNEL_DIR / "DEMMiscKernels.cu", Subs, JitifyOptions))); + misc_kernels = std::make_shared(std::move(JitHelper::buildProgram( + "DEMKinematicMisc", JitHelper::KERNEL_DIR / "DEMKinematicMisc.cu", Subs, JitifyOptions))); } + prewarmKernels(); } void DEMKinematicThread::initAllocation() { @@ -950,4 +1118,33 @@ void DEMKinematicThread::updateTriNodeRelPos(size_t start, const std::vectorkernel("getNumberOfBinsEachSphereTouches").instantiate(); + bin_sphere_kernels->kernel("populateBinSphereTouchingPairs").instantiate(); + } + if (sphere_contact_kernels) { + sphere_contact_kernels->kernel("getNumberOfSphereContactsEachBin").instantiate(); + sphere_contact_kernels->kernel("populateSphereContactPairsEachBin").instantiate(); + } + if (bin_triangle_kernels) { + bin_triangle_kernels->kernel("makeTriangleSandwich").instantiate(); + bin_triangle_kernels->kernel("precomputeMeshOwnerPose").instantiate(); + bin_triangle_kernels->kernel("precomputeTriangleSandwichData").instantiate(); + bin_triangle_kernels->kernel("getNumberOfBinsEachTriangleTouches").instantiate(); + bin_triangle_kernels->kernel("populateBinTriangleTouchingPairs").instantiate(); + } + if (sphTri_contact_kernels) { + sphTri_contact_kernels->kernel("getNumberOfTriangleContactsEachBin").instantiate(); + sphTri_contact_kernels->kernel("populateTriangleContactsEachBin").instantiate(); + } + if (misc_kernels) { + misc_kernels->kernel("computeMarginFromAbsv_implSph").instantiate(); + misc_kernels->kernel("computeMarginFromAbsv_implTri").instantiate(); + misc_kernels->kernel("computeMarginFromAbsv_implAnal").instantiate(); + } +} + } // namespace deme diff --git a/src/DEM/kT.h b/src/DEM/kT.h index 295c1d67..454b2164 100644 --- a/src/DEM/kT.h +++ b/src/DEM/kT.h @@ -6,7 +6,6 @@ #ifndef DEME_KT #define DEME_KT -#include #include #include #include @@ -17,20 +16,13 @@ #include "../core/utils/ThreadManager.h" #include "../core/utils/GpuManager.h" #include "../core/utils/DataMigrationHelper.hpp" -#include "../core/utils/GpuError.h" +#include "../core/utils/JitHelper.h" #include "../core/utils/Timer.hpp" #include "BdrsAndObjs.h" #include "Defines.h" #include "Structs.h" -// Forward declare deme::jit::Program -namespace deme { -namespace jit { -class Program; -} -} // namespace deme - namespace deme { // Implementation-level classes @@ -44,8 +36,8 @@ class DEMKinematicThread { ThreadManager* pSchedSupport; // GpuManager* pGpuDistributor; - // kT verbosity - VERBOSITY verbosity = INFO; + // dT verbosity + verbosity_t verbosity = VERBOSITY_INFO; // Some behavior-related flags SolverFlags solverFlags; @@ -75,9 +67,6 @@ class DEMKinematicThread { // Pointers to those data arrays defined below, stored in a struct DualStruct granData = DualStruct(); - // Log for anomalies in the simulation - WorkerAnomalies anomalies = WorkerAnomalies(); - // Buffer arrays for storing info from the dT side. // dT modifies these arrays; kT uses them only. @@ -98,8 +87,15 @@ class DEMKinematicThread { DeviceArray relPosNode1_buffer = DeviceArray(&m_approxDeviceBytesUsed); DeviceArray relPosNode2_buffer = DeviceArray(&m_approxDeviceBytesUsed); DeviceArray relPosNode3_buffer = DeviceArray(&m_approxDeviceBytesUsed); - // Max vel of entities + // Buffer for per-triangle max tri-tri penetration received from dT + DeviceArray maxTriTriPenetration_buffer = DeviceArray(&m_approxDeviceBytesUsed); + // Vel of entities DeviceArray absVel_buffer = DeviceArray(&m_approxDeviceBytesUsed); + // Angular velocity magnitude of entities + DeviceArray absAngVel_buffer = DeviceArray(&m_approxDeviceBytesUsed); + + // kT's working copy of per-triangle max tri-tri penetration values + DeviceArray maxTriTriPenetration = DeviceArray(&m_approxDeviceBytesUsed); // kT's copy of family map // std::unordered_map familyUserImplMap; @@ -156,8 +152,10 @@ class DEMKinematicThread { DualArray oriQy = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); DualArray oriQz = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - // dT-supplied system velocity - DualArray marginSize = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // marginSizes include both the velocity-induced margin and family-prescribed margin. + DeviceArray marginSizeSphere = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray marginSizeAnalytical = DeviceArray(&m_approxDeviceBytesUsed); + DeviceArray marginSizeTriangle = DeviceArray(&m_approxDeviceBytesUsed); // Clump's family identification code. Used in determining whether they can be contacts between two families, and // whether a family has prescribed motions. @@ -171,23 +169,53 @@ class DEMKinematicThread { // that means geometries should be considered in contact when they are physically in contact. DualArray familyExtraMarginSize = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Combined-owner mapping (owner -> master owner, NULL_BODYID if not participating in a combined owner). + DualArray ownerCombinedMaster = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // kT computed contact pair info - DualArray idGeometryA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray idGeometryB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray contactType = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPrimitiveA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPrimitiveB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray contactTypePrimitive = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // Contact pair info at the previous time step. This is needed by dT so enduring contacts are identified in // history-based models. - DualArray previous_idGeometryA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray previous_idGeometryB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray previous_contactType = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray contactMapping = - DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_idPrimitiveA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_idPrimitiveB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_contactTypePrimitive = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Stable flooded-island ID per previous primitive contact. Used by the next contact detection step to preserve + // patch-contact identity when the raw flooded representative triangle changes. + DualArray previous_primitivePatchIsland = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Records if this primitive contact is persistent and serves as kT's work array on treating their persistency. + DualArray contactPersistency = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Records the contact for each patch contact type in the previous step. + // Used for patch-based persistent contact mapping. + ContactTypeMap> typeStartCountPatchMap = + ContactTypeMap>({0, 0}); // Sphere-related arrays // Owner body ID of this component DualArray ownerClumpBody = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); - DualArray ownerMesh = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerTriMesh = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerAnalBody = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Mesh owner flags (indexed by owner body ID) + DualArray ownerMeshConvex = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray ownerMeshNeverWinner = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Mesh patch information: each facet belongs to a patch + // Patch ID for each triangle facet (maps facet to patch) + DualArray triPatchID = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // Triangle edge neighbors (compact; index via triNeighborIndex) + DualArray triNeighborIndex = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor1 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor2 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray triNeighbor3 = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // The ID that maps this sphere component's geometry-defining parameters, when this component is jitified DualArray clumpComponentOffset = @@ -199,9 +227,33 @@ class DEMKinematicThread { // The ID that maps this analytical entity component's geometry-defining parameters, when this component is jitified // DualArray analComponentOffset; - // Records if this contact is persistent and serves as kT's work array on treating their persistency. - DualArray contactPersistency = - DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + // NEW: Separate patch IDs for contact pairs (instead of combined patchIDPair_t) + // These arrays store patch IDs for contacts involving patches (mesh-based contacts) + // They are potentially smaller in size than idPrimitiveA/B arrays + DualArray idPatchA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray idPatchB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Patch IDs at the previous time step. Needed for history-based models with mesh contacts. + DualArray previous_idPatchA = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_idPatchB = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Contact types at patch level (corresponding to idPatchA/B arrays) + DualArray contactTypePatch = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_contactTypePatch = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray contactPatchIsland = DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + DualArray previous_contactPatchIsland = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // Mapping array: maps from primitive-based pair index to patch-based pair index + // Same length as primitive pair arrays (idPrimitiveA/B). For each primitive pair, + // records the index where the corresponding patch pair appears in idPatchA/B (the shorter array) + DualArray geomToPatchMap = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); + + // The mapping between time steps, of patch-based contact pairs + DualArray contactMapping = + DualArray(&m_approxHostBytesUsed, &m_approxDeviceBytesUsed); // kT's timers std::vector timer_names = {"Discretize domain", "Find contact pairs", "Build history map", @@ -226,17 +278,8 @@ class DEMKinematicThread { // spawning the child thread. DEME_GPU_CALL(cudaStreamCreate(&streamInfo.stream)); - // Launch a worker thread bound to this instance. An exception escaping a std::thread calls - // std::terminate; on some platforms (notably Windows) that prints nothing, so report it here - // before dying, otherwise runtime errors like JIT compilation failures are silently swallowed. - th = std::move(std::thread([this]() { - try { - this->workerThread(); - } catch (const std::exception& e) { - std::cerr << "Fatal error in kT worker thread: " << e.what() << std::endl; - throw; - } - })); + // Launch a worker thread bound to this instance + th = std::move(std::thread([this]() { this->workerThread(); })); } ~DEMKinematicThread() { // std::cout << "Kinematic thread closing..." << std::endl; @@ -283,6 +326,7 @@ class DEMKinematicThread { size_t nTriMeshes, size_t nSpheresGM, size_t nTriGM, + size_t nTriNeighbors, unsigned int nAnalGM, size_t nExtraContacts, unsigned int nMassProperties, @@ -296,19 +340,35 @@ class DEMKinematicThread { void populateEntityArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, const ClumpTemplateFlatten& clump_templates, + const std::vector& ext_obj_comp_num, size_t nExistOwners, size_t nExistSpheres, - size_t nExistingFacets); + size_t nExistingFacets, + size_t nExistingMeshPatches, + size_t nExistingTriNeighbors); /// Initialize arrays void initGPUArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, + const std::vector& ext_obj_comp_num, const std::vector& family_mask_matrix, const ClumpTemplateFlatten& clump_templates); @@ -317,8 +377,15 @@ class DEMKinematicThread { void updateClumpMeshArrays(const std::vector>& input_clump_batches, const std::vector& input_ext_obj_family, const std::vector& input_mesh_obj_family, + const std::vector& input_mesh_obj_convex, + const std::vector& input_mesh_obj_never_winner, const std::vector& input_mesh_facet_owner, + const std::vector& input_mesh_facet_patch, + const std::vector& input_mesh_facet_neighbor1, + const std::vector& input_mesh_facet_neighbor2, + const std::vector& input_mesh_facet_neighbor3, const std::vector& input_mesh_facets, + const std::vector& ext_obj_comp_num, const std::vector& family_mask_matrix, const ClumpTemplateFlatten& clump_templates, size_t nExistingOwners, @@ -326,6 +393,8 @@ class DEMKinematicThread { size_t nExistingSpheres, size_t nExistingTriMesh, size_t nExistingFacets, + size_t nExistingTriNeighbors, + size_t nExistingPatches, unsigned int nExistingObj, unsigned int nExistingAnalGM); @@ -346,8 +415,11 @@ class DEMKinematicThread { double ts_size, float expand_factor, float approx_max_vel, + double max_tritri_penetration, + float tritri_contact_rejection_ratio, float expand_safety_param, float expand_safety_adder, + bool use_angvel_margin, const std::set& contact_wildcards, const std::set& owner_wildcards, const std::set& geo_wildcards); @@ -371,14 +443,12 @@ class DEMKinematicThread { for (const auto& name : timer_names) { timers.GetTimer(name).reset(); } + timers.ResetGpuTimers(); } /// Change all entities with (user-level) family number ID_from to have a new number ID_to void changeFamily(unsigned int ID_from, unsigned int ID_to); - /// Change radii and relPos info of these owners (if these owners are clumps) - void changeOwnerSizes(const std::vector& IDs, const std::vector& factors); - // Jitify kT kernels (at initialization) based on existing knowledge of this run void jitifyKernels(const std::unordered_map& Subs, const std::vector& JitifyOptions); @@ -407,10 +477,14 @@ class DEMKinematicThread { // Bring kT buffer array data to its working arrays inline void unpackMyBuffer(); + // Compute margin sizes from absolute velocities + inline void computeMarginFromAbsv(float* absVel_owner, float* absAngVel_owner); // Send produced data to dT-owned biffers void sendToTheirBuffer(); // Resize dT's buffer arrays based on the number of contact pairs - inline void transferArraysResize(size_t nContactPairs); + inline void transferPrimitivesArraysResize(size_t nContactPairs); + // Resize mesh patch pair array + inline void transferPatchArrayResize(size_t nContactPairs); // Automatic adjustments to sim params void calibrateParams(); // The kT-side allocations that can be done at initialization time @@ -419,13 +493,13 @@ class DEMKinematicThread { void deallocateEverything(); // Just-in-time compiled kernels - // jitify::Program bin_sphere_kernels = JitHelper::buildProgram("bin_sphere_kernels", " "); - std::shared_ptr bin_sphere_kernels; - std::shared_ptr bin_triangle_kernels; - std::shared_ptr sphTri_contact_kernels; - std::shared_ptr sphere_contact_kernels; - std::shared_ptr history_kernels; - std::shared_ptr misc_kernels; + std::shared_ptr bin_sphere_kernels; + std::shared_ptr bin_triangle_kernels; + std::shared_ptr sphTri_contact_kernels; + std::shared_ptr sphere_contact_kernels; + std::shared_ptr misc_kernels; + // Instantiate commonly used kT kernels up front so JIT cache misses do not interrupt the first contact step. + void prewarmKernels(); // Adjuster for bin size class AccumTimer { diff --git a/src/DEM/python/DEME.py b/src/DEM/python/DEME.py index d05711dc..83545819 100644 --- a/src/DEM/python/DEME.py +++ b/src/DEM/python/DEME.py @@ -1,4 +1,7 @@ -"""Compatibility import for the historical capitalized DEME module.""" +"""Compatibility import for the historical capitalized DEME module. + +New code should use ``import deme``. +""" from deme import * # noqa: F401,F403 from deme import __version__ diff --git a/src/DEM/python/PythonCudaIncludes.hpp b/src/DEM/python/PythonCudaIncludes.hpp new file mode 100644 index 00000000..098f99d5 --- /dev/null +++ b/src/DEM/python/PythonCudaIncludes.hpp @@ -0,0 +1,23 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_PYTHON_CUDA_INCLUDES_HPP +#define DEME_PYTHON_CUDA_INCLUDES_HPP + +#include +#include + +namespace deme { +namespace python { + +// Import configures this extension-local state before any solver workers start. +// Only the Python core variant includes this helper; native C++ uses its original toolkit discovery. +inline std::vector& CudaIncludePaths() { + static std::vector paths; + return paths; +} + +} // namespace python +} // namespace deme + +#endif diff --git a/src/DEM/python/bindings.cpp b/src/DEM/python/bindings.cpp new file mode 100644 index 00000000..03e703b0 --- /dev/null +++ b/src/DEM/python/bindings.cpp @@ -0,0 +1,2481 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/utils/RuntimeData.h" +#include "PythonCudaIncludes.hpp" +#include "core/utils/DataMigrationHelper.hpp" +#include "core/utils/DEMEPaths.h" +#include "core/ApiVersion.h" +#include "DEM/utils/HostSideHelpers.hpp" +#include "DEM/utils/Samplers.hpp" +#include "DEM/Defines.h" +#include "DEM/Structs.h" +#include "DEM/API.h" +#include "DEM/AuxClasses.h" +#include "DEM/VariableTypes.h" +#ifdef DEME_HAS_VISUALIZER + #include "DEM/utils/DEMVisualizer.h" +#endif + +namespace py = pybind11; + +// Custom type caster for float3 and float4 +namespace pybind11 { +namespace detail { +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(float3, _("float3")); + + // Python -> C++ + bool load(handle src, bool) { + if (!isinstance(src)) + return false; + auto seq = reinterpret_borrow(src); + if (seq.size() != 3) + return false; + value.x = seq[0].cast(); + value.y = seq[1].cast(); + value.z = seq[2].cast(); + return true; + } + + // C++ -> Python + static handle cast(const float3& src, return_value_policy, handle) { + return py::make_tuple(src.x, src.y, src.z).release(); + } +}; + +template <> +struct type_caster { + public: + PYBIND11_TYPE_CASTER(float4, _("float4")); + + // Python -> C++ + bool load(handle src, bool) { + if (!isinstance(src)) + return false; + auto seq = reinterpret_borrow(src); + if (seq.size() != 4) + return false; + value.x = seq[0].cast(); + value.y = seq[1].cast(); + value.z = seq[2].cast(); + value.w = seq[3].cast(); + return true; + } + + // C++ -> Python + static handle cast(const float4& src, return_value_policy, handle) { + return py::make_tuple(src.x, src.y, src.z, src.w).release(); + } +}; +} // namespace detail +} // namespace pybind11 + +PYBIND11_MODULE(_deme, obj) { + // Called once by the package bootstrap, before user code can create a solver. Keep this state + // private to the extension so importing Python cannot change a separately launched C++ program. + obj.def("_set_cuda_include_paths", [](const std::vector& paths) { + auto& configured = deme::python::CudaIncludePaths(); + static bool initialized = false; + const std::vector requested(paths.begin(), paths.end()); + if (initialized && configured != requested) { + throw py::value_error("Python CUDA include paths have already been configured"); + } + if (!initialized) { + configured = requested; + initialized = true; + } + }); + + // Report the same version as the CMake project so Python and C++ callers + // cannot observe conflicting release versions. + obj.attr("__version__") = std::to_string(DEME_VERSION_MAJOR) + "." + std::to_string(DEME_VERSION_MINOR) + "." + + std::to_string(DEME_VERSION_PATCH); + // Obtaining the location of python's site-packages dynamically and setting it + // as path prefix + try { + py::module _site = py::module_::import("site"); + std::string loc; + + // Try getsitepackages() first (works in most environments) + if (py::hasattr(_site, "getsitepackages")) { + py::object site_packages = _site.attr("getsitepackages")(); + if (py::isinstance(site_packages) && py::len(site_packages) > 0) { + loc = site_packages.cast()[0].cast(); + } + } + + // Fallback to USER_SITE if getsitepackages fails + if (loc.empty() && py::hasattr(_site, "USER_SITE")) { + loc = _site.attr("USER_SITE").cast(); + } + + // Last resort: use sys.prefix + if (loc.empty()) { + py::module sys = py::module_::import("sys"); + std::string prefix = sys.attr("prefix").cast(); + loc = prefix + "/lib/python" + sys.attr("version_info").attr("major").cast() + "." + + sys.attr("version_info").attr("minor").cast() + "/site-packages"; + } + + // Setting path prefix + std::filesystem::path path = loc; + DEMERuntimeDataHelper::SetPathPrefix(path); + } catch (const py::error_already_set& e) { + // If all else fails, don't set the prefix and let the system use default paths + py::print( + "Warning: Could not determine site-packages location for data files, using default paths. " + "Data file loading may fail. Set DEME_DATA_PATH environment variable if you encounter issues."); + } + + deme::SetDEMEDataPath(); + deme::SetDEMEKernelPath(); + deme::SetDEMEIncludePath(); + + // To define methods independent of a class, use obj.def() syntax to wrap them! + obj.def("FrameTransformGlobalToLocal", &deme::FrameTransformGlobalToLocal, + "Translating the inverse of the provided vec then applying a local inverse rotation of the provided rot_Q, " + "then return the result."); + obj.def("FrameTransformLocalToGlobal", &deme::FrameTransformLocalToGlobal, + "Apply a local rotation then a translation, then return the result."); + obj.def("GetDEMEDataFile", &deme::GetDEMEDataFile, "Resolve a path relative to DEME's installed data directory.", + py::arg("relative_path")); + obj.def( + "DEMBoxGridSampler", + static_cast (*)(const std::vector&, const std::vector&, float, float, float)>( + &deme::DEMBoxGridSampler), + py::arg("BoxCenter"), py::arg("HalfDims"), py::arg("GridSizeX"), py::arg("GridSizeY") = -1.0, + py::arg("GridSizeZ") = -1.0, + "Return regularly spaced points inside a box. Negative Y or Z spacing reuses the X spacing."); + obj.def("DEMBoxHCPSampler", + static_cast (*)(const std::vector&, const std::vector&, float)>( + &deme::DEMBoxHCPSampler), + "Return hexagonally close-packed points inside a box.", py::arg("BoxCenter"), py::arg("HalfDims"), + py::arg("separation")); + obj.def("DEMCylSurfSampler", + static_cast> (*)(const std::vector&, const std::vector&, float, + float, float, float)>(&deme::DEMCylSurfSampler), + py::arg("CylCenter"), py::arg("CylAxis"), py::arg("CylRad"), py::arg("CylHeight"), py::arg("ParticleRad"), + py::arg("spacing") = 1.2, + "Return points on a cylindrical surface. ``spacing`` scales the nominal particle-diameter separation."); + + obj.attr("PI") = py::float_(M_PI); + // Export both the precise name and its legacy mesh-oriented spelling. They deliberately carry the same value. + obj.attr("SPHERE_TRIANGLE_CONTACT") = py::int_(static_cast(deme::SPHERE_TRIANGLE_CONTACT)); + obj.attr("SPHERE_MESH_CONTACT") = py::int_(static_cast(deme::SPHERE_MESH_CONTACT)); + + py::class_(obj, "DEMERuntimeDataHelper") + .def(py::init<>()) + .def_static("SetPathPrefix", &DEMERuntimeDataHelper::SetPathPrefix); + + py::class_(obj, "PDSampler", + "Poisson-disk volumetric sampler enforcing a minimum point separation.") + .def(py::init(), py::arg("separation"), "Create a sampler with the requested minimum separation.") + .def("SetSeparation", &deme::PDSampler::SetSeparation, "Set the minimum separation used by subsequent samples.", + py::arg("separation")) + .def("SetRandomEngineSeed", &deme::PDSampler::SetRandomEngineSeed, + "Seed the random engine so Poisson-disk samples can be reproduced.", py::arg("seed")) + .def("SampleBox", + static_cast> (deme::PDSampler::*)( + const std::vector& center, const std::vector& halfDim)>(&deme::PDSampler::SampleBox), + "Return points sampled from the specified box volume.", py::arg("center"), py::arg("halfDim")) + + .def("SampleSphere", + static_cast> (deme::PDSampler::*)(const std::vector&, float)>( + &deme::PDSampler::SampleSphere), + "Return points sampled from the specified spherical volume.", py::arg("center"), py::arg("radius")) + + .def("SampleCylinderX", + static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( + &deme::PDSampler::SampleCylinderX), + "Return points sampled from an X-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("SampleCylinderY", + static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( + &deme::PDSampler::SampleCylinderY), + "Return points sampled from a Y-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("SampleCylinderZ", + static_cast> (deme::PDSampler::*)(const std::vector&, float, float)>( + &deme::PDSampler::SampleCylinderZ), + "Return points sampled from a Z-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("GetSeparation", &deme::Sampler::GetSeparation, "Return the current minimum separation."); + + py::class_(obj, "GridSampler", "Regular-grid volumetric point sampler.") + .def(py::init(), py::arg("separation"), "Create a regular-grid sampler with uniform point spacing.") + .def("SetSeparation", &deme::GridSampler::SetSeparation, "Set the point spacing used by subsequent samples.", + py::arg("separation")) + .def("SampleBox", + static_cast> (deme::GridSampler::*)( + const std::vector& center, const std::vector& halfDim)>(&deme::GridSampler::SampleBox), + "Return points sampled from the specified box volume.", py::arg("center"), py::arg("halfDim")) + + .def("SampleSphere", + static_cast> (deme::GridSampler::*)(const std::vector&, float)>( + &deme::GridSampler::SampleSphere), + "Return points sampled from the specified spherical volume.", py::arg("center"), py::arg("radius")) + + .def("SampleCylinderX", + static_cast> (deme::GridSampler::*)( + const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderX), + "Return points sampled from an X-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("SampleCylinderY", + static_cast> (deme::GridSampler::*)( + const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderY), + "Return points sampled from a Y-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("SampleCylinderZ", + static_cast> (deme::GridSampler::*)( + const std::vector&, float, float)>(&deme::GridSampler::SampleCylinderZ), + "Return points sampled from a Z-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("GetSeparation", &deme::Sampler::GetSeparation, "Return the current grid spacing."); + + py::class_(obj, "HCPSampler", "Hexagonally close-packed volumetric point sampler.") + .def(py::init(), py::arg("separation"), "Create an HCP sampler with the requested point separation.") + .def("SampleBox", + static_cast> (deme::HCPSampler::*)( + const std::vector& center, const std::vector& halfDim)>(&deme::HCPSampler::SampleBox), + "Return points sampled from the specified box volume.", py::arg("center"), py::arg("halfDim")) + + .def("SetSeparation", &deme::HCPSampler::SetSeparation, "Set the point separation used by subsequent samples.", + py::arg("separation")) + .def("SampleSphere", + static_cast> (deme::HCPSampler::*)(const std::vector&, float)>( + &deme::HCPSampler::SampleSphere), + "Return points sampled from the specified spherical volume.", py::arg("center"), py::arg("radius")) + + .def( + "SampleCylinderX", + static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( + &deme::HCPSampler::SampleCylinderX), + "Return points sampled from an X-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def( + "SampleCylinderY", + static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( + &deme::HCPSampler::SampleCylinderY), + "Return points sampled from a Y-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def( + "SampleCylinderZ", + static_cast> (deme::HCPSampler::*)(const std::vector&, float, float)>( + &deme::HCPSampler::SampleCylinderZ), + "Return points sampled from a Z-aligned cylindrical volume.", py::arg("center"), py::arg("radius"), + py::arg("halfHeight")) + + .def("GetSeparation", &deme::HCPSampler::GetSeparation, "Return the current point separation."); + + // Have to forward declare this templated method so pybind can see it + std::vector& (deme::DataContainer::*get_float_dc)(const std::string&) = &deme::DataContainer::Get; + py::class_>( + obj, "DataContainer", "String-keyed container used to return named arrays from DEME.") + .def(py::init<>(), "Create an empty data container.") + .def("Get", get_float_dc, py::return_value_policy::reference_internal, + "Return the stored float array named ``key`` by reference. This is primarily used for wildcard output.", + py::arg("key")); + + py::class_>( + obj, "ContactInfoContainer", + "Named arrays describing contacts selected by ``DEMSolver.SetContactOutputContent``.") + .def(py::init>&>()) + .def("GetContactType", &deme::ContactInfoContainer::GetContactType, + "Get contact type as strings from the container.") + .def("GetPoint", &deme::ContactInfoContainer::GetPoint, "Get contact points as vectors from the container.") + .def("GetAOwner", &deme::ContactInfoContainer::GetAOwner, "Get AOwner number from the container.") + .def("GetBOwner", &deme::ContactInfoContainer::GetBOwner, "Get BOwner number from the container.") + .def("GetAGeo", &deme::ContactInfoContainer::GetAGeo, "Get AGeo number from the container.") + .def("GetBGeo", &deme::ContactInfoContainer::GetBGeo, "Get BGeo number from the container.") + .def("GetAOwnerFamily", &deme::ContactInfoContainer::GetAOwnerFamily, + "Get AOwnerFamily number from the container.") + .def("GetBOwnerFamily", &deme::ContactInfoContainer::GetBOwnerFamily, + "Get BOwnerFamily number from the container.") + .def("GetForce", &deme::ContactInfoContainer::GetForce, "Get force as vectors from the container.") + .def("GetTorque", &deme::ContactInfoContainer::GetTorque, "Get torque as vectors from the container.") + .def("GetNormal", &deme::ContactInfoContainer::GetNormal, "Get contact normal as vectors from the container."); + + py::class_>( + obj, "DEMInspector", "Evaluate a configured aggregate or spatial quantity from initialized simulation state.") + .def(py::init()) + .def("GetValue", &deme::DEMInspector::GetValue, + "Synchronize as needed and return the inspector's current scalar value."); + + py::class_>(obj, "DEMInitializer").def(py::init<>()); + + py::class_>(obj, "DEMTrackedObj") + .def(py::init()); + + py::class_>(obj, "Tracker", + R"doc( +Access and modify owners associated with a tracked batch or object. + +Create trackers with ``DEMSolver.Track`` during setup. Most state queries and +updates require the solver to be initialized and synchronized. ``offset`` +selects an owner within the tracked collection, starting at zero. Keep the +parent solver alive while using a tracker. + +Fixed-size CUDA exchange methods validate ranges, capacities, and pointer metadata by default. Pass +``validate=False`` only when those preconditions are guaranteed and avoiding validation overhead matters. Required +device routing and documented transformations, including quaternion normalization, still apply. +)doc") + .def(py::init(), py::arg("solver"), + "Create a tracker associated with ``solver``. Prefer ``DEMSolver.Track`` so the tracked owner range is " + "configured correctly.") + .def("GetOwnerID", &deme::DEMTracker::GetOwnerID, + "Return the simulation-wide owner ID at ``offset`` within this tracker.", py::arg("offset") = 0) + .def("GetOwnerIDs", &deme::DEMTracker::GetOwnerIDs, + "Return simulation-wide owner IDs for every owner covered by this tracker.") + + .def("Pos", &deme::DEMTracker::GetPos, + "Return the global-frame center-of-mass position of the tracked owner at ``offset``.", + py::arg("offset") = 0) + .def("Positions", &deme::DEMTracker::GetPositions, + "Return global-frame center-of-mass positions for all owners covered by this tracker.") + + .def("AngVelLocal", &deme::DEMTracker::GetAngVelLocal, + "Return the angular velocity of the owner at ``offset`` in its local principal-axis frame. Rotate it by " + "``OriQ(offset)`` to obtain the global-frame value.", + py::arg("offset") = 0) + .def("AngularVelocitiesLocal", &deme::DEMTracker::GetAngularVelocitiesLocal, + "Return local principal-axis-frame angular velocities for all tracked owners.") + + .def("AngVelGlobal", &deme::DEMTracker::GetAngVelGlobal, + "Return the global-frame angular velocity of the owner at ``offset``.", py::arg("offset") = 0) + .def("AngularVelocitiesGlobal", &deme::DEMTracker::GetAngularVelocitiesGlobal, + "Return global-frame angular velocities for all tracked owners.") + + .def("Vel", &deme::DEMTracker::GetVel, "Return the global-frame linear velocity of the owner at ``offset``.", + py::arg("offset") = 0) + .def("Velocities", &deme::DEMTracker::GetVelocities, + "Return global-frame linear velocities for all tracked owners.") + + .def("OriQ", &deme::DEMTracker::GetOriQ, + "Return the quaternion rotating the owner-local frame to the global frame. Python ordering is " + "``(x, y, z, w)`` (Chrono ``(e1, e2, e3, e0)``).", + py::arg("offset") = 0) + .def("OrientationQuaternions", &deme::DEMTracker::GetOrientationQuaternions, + "Return local-to-global orientation quaternions for all tracked owners. Each quaternion uses Python " + "ordering ``(x, y, z, w)``.") + + .def("GetFamily", &deme::DEMTracker::GetFamily, "Get the family number of the tracked object.", + py::arg("offset") = 0) + .def("GetFamilies", &deme::DEMTracker::GetFamilies, "Get the family numbers of all the tracked object.") + + .def("MOI", &deme::DEMTracker::GetMOI, + "Get the moment of inertia (in principal axis frame) of the tracked object.", py::arg("offset") = 0) + .def("MOIs", &deme::DEMTracker::GetMOIs, + "Get the moment of inertia (in principal axis frame) of all the tracked objects.") + + .def("Mass", &deme::DEMTracker::Mass, "Get the mass of the tracked object.", py::arg("offset") = 0) + .def("Masses", &deme::DEMTracker::Masses, "Get the masses of all the tracked objects.") + + .def("GetContactClumps", &deme::DEMTracker::GetContactClumps, + "Get the clumps that are in contact with this tracked owner as a vector.", py::arg("offset") = 0) + + .def("ContactAcc", &deme::DEMTracker::GetContactAcc, + "Get the a portion of the acceleration of this tracked object, that is the result of its contact with " + "other simulation entities. In most cases, this means excluding the gravitational acceleration. The " + "acceleration is in global frame.", + py::arg("offset") = 0) + .def("ContactAccelerations", &deme::DEMTracker::GetContactAccelerations, + "Get the acceleration experienced by all objects tracked by this tracker, that is the result of their " + "contact with other simulation entities. The acceleration is in global frame. In most cases, this means " + "excluding the gravitational acceleration. The acceleration is in global frame.") + + .def("ContactAngAccLocal", &deme::DEMTracker::GetContactAngAccLocal, + "Get the a portion of the angular acceleration of this tracked object, that is the result of its contact " + "with other simulation entities. The acceleration is in this object's local frame.", + py::arg("offset") = 0) + .def("ContactAngularAccelerationsLocal", &deme::DEMTracker::GetContactAngularAccelerationsLocal, + "Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of " + "their contact with other simulation entities. The acceleration is in this object's local frame.") + + .def("ContactAngAccGlobal", &deme::DEMTracker::GetContactAngAccGlobal, + "Get the a portion of the angular acceleration of this tracked object, that is the result of its contact " + "with other simulation entities. The acceleration is in this object's global frame.", + py::arg("offset") = 0) + .def("ContactAngularAccelerationsGlobal", &deme::DEMTracker::GetContactAngularAccelerationsGlobal, + "Get the angular acceleration experienced by all objects tracked by this tracker, that is the result of " + "their contact with other simulation entities. The acceleration is in this object's global frame.") + + .def("GetOwnerWildcardValue", + static_cast( + &deme::DEMTracker::GetOwnerWildcardValue), + "Get the owner's wildcard value.", py::arg("name"), py::arg("offset") = 0) + .def("GetOwnerWildcardValues", + static_cast (deme::DEMTracker::*)(const std::string&)>( + &deme::DEMTracker::GetOwnerWildcardValues), + "Get the owner wildcard values for all the owners entities tracked by this tracker.", py::arg("name")) + .def( + "PositionsToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.PositionsToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + R"doc( +Synchronously copy all tracked global positions to caller-owned CUDA memory. + +``pointer`` is an integer address to writable, CUDA-accessible ``float3`` +storage on logical CUDA ``device``. ``capacity`` is measured in ``float3`` +elements and must cover every owner in this tracker. The caller owns the +allocation and must keep it alive through the call. +)doc", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "SetPositionsFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, int device, bool validate) { + tracker.SetPositionsFromDevice(reinterpret_cast(pointer), device, validate); + }, + "Synchronously set every tracked global position from CUDA ``float3`` storage. The buffer must contain " + "one element per tracked owner; remote input is copied to the dynamic-worker device before unpacking.", + py::arg("pointer"), py::arg("device"), py::arg("validate") = true) + .def( + "VelocitiesToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.VelocitiesToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked global linear velocities to caller-owned CUDA ``float3`` storage. " + "``capacity`` is in elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "SetVelocitiesFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, int device, bool validate) { + tracker.SetVelocitiesFromDevice(reinterpret_cast(pointer), device, validate); + }, + "Synchronously set every tracked global linear velocity from CUDA ``float3`` storage. The buffer must " + "contain one element per tracked owner; remote input is copied to the dynamic-worker device before " + "unpacking.", + py::arg("pointer"), py::arg("device"), py::arg("validate") = true) + .def( + "AngularVelocitiesLocalToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.AngularVelocitiesLocalToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked local-frame angular velocities to caller-owned CUDA ``float3`` storage. " + "``capacity`` is in elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "SetAngularVelocitiesFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, int device, bool validate) { + tracker.SetAngularVelocitiesFromDevice(reinterpret_cast(pointer), device, validate); + }, + "Synchronously set every tracked local-frame angular velocity from CUDA ``float3`` storage. The buffer " + "must contain one element per tracked owner; remote input is copied to the dynamic-worker device before " + "unpacking.", + py::arg("pointer"), py::arg("device"), py::arg("validate") = true) + .def( + "AngularVelocitiesGlobalToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.AngularVelocitiesGlobalToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked global-frame angular velocities to caller-owned CUDA ``float3`` storage. " + "``capacity`` is in elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "SetAngularVelocitiesGlobalFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, int device, bool validate) { + tracker.SetAngularVelocitiesGlobalFromDevice(reinterpret_cast(pointer), device, + validate); + }, + "Synchronously set every tracked global angular velocity from CUDA ``float3`` storage. Each value is " + "converted to the owner's local principal-axis frame using its orientation at call time. The buffer must " + "contain one element per tracked owner; remote input is copied to the dynamic-worker device before " + "unpacking.", + py::arg("pointer"), py::arg("device"), py::arg("validate") = true) + .def( + "OrientationQuaternionsToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.OrientationQuaternionsToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked local-to-global quaternions to caller-owned CUDA ``float4`` storage in " + "``(x, y, z, w)`` order. ``capacity`` is in elements.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "SetOrientationQuaternionsFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, int device, bool validate) { + tracker.SetOrientationQuaternionsFromDevice(reinterpret_cast(pointer), device, validate); + }, + "Synchronously set every tracked local-to-global quaternion from CUDA ``float4`` storage in ``(x, y, z, " + "w)`` order. With validation enabled, non-finite and zero-length inputs are rejected; finite, nonzero " + "inputs are always normalized. The buffer must contain one element per tracked owner; remote input is " + "copied to the dynamic-worker device before validation and unpacking.", + py::arg("pointer"), py::arg("device"), py::arg("validate") = true) + .def( + "FamiliesToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.FamiliesToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked family numbers to caller-owned CUDA ``uint32`` storage. ``capacity`` is " + "in elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "MassesToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.MassesToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked masses to caller-owned CUDA ``float32`` storage. ``capacity`` is in " + "elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "MOIsToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.MOIsToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked principal moments of inertia to caller-owned CUDA ``float3`` storage. " + "``capacity`` is in elements; ``device`` is the allocation's logical CUDA device.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "ContactAccelerationsToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.ContactAccelerationsToDevice(reinterpret_cast(pointer), capacity, device, validate); + }, + "Synchronously copy all tracked global-frame contact accelerations to caller-owned CUDA ``float3`` " + "storage. Gravity and other non-contact acceleration are excluded.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "ContactAngularAccelerationsLocalToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.ContactAngularAccelerationsLocalToDevice(reinterpret_cast(pointer), capacity, device, + validate); + }, + "Synchronously copy all tracked local-frame contact angular accelerations to caller-owned CUDA " + "``float3`` storage.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "ContactAngularAccelerationsGlobalToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t pointer, size_t capacity, int device, bool validate) { + tracker.ContactAngularAccelerationsGlobalToDevice(reinterpret_cast(pointer), capacity, device, + validate); + }, + "Synchronously copy all tracked global-frame contact angular accelerations to caller-owned CUDA " + "``float3`` storage.", + py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + .def( + "ContactWrenches", + [](deme::DEMTracker& tracker) { + std::vector forces; + std::vector torques; + tracker.ContactWrenches(forces, torques); + return py::make_tuple(forces, torques); + }, + R"doc(Return ``(forces, torques)`` with one reduced contact wrench per tracked owner. + +Both tuple elements preserve tracker owner order and contain ``float3`` values in the global frame. Torque is measured +about each owner's current DEME position and includes both force-generated moments and force-model-only torque. Owners +without recorded contact receive zero. This reads current dT force records without triggering contact detection or +force evaluation, so callers must advance/synchronize the simulation as needed. Contact recording must remain enabled.)doc") + .def( + "ContactWrenchesToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t forces, std::uintptr_t torques, size_t capacity, int device) { + tracker.ContactWrenchesToDevice(reinterpret_cast(forces), reinterpret_cast(torques), + capacity, device); + }, + R"doc(Synchronously write one reduced contact wrench per tracked owner to CUDA ``float3`` buffers. + +Outputs preserve tracker owner order. Force and torque are global-frame resultants; torque is measured about each +owner's current DEME position and includes force-generated moments and force-model-only torque. Owners without contact +receive zero. ``force_destination`` and ``torque_destination`` are integer addresses of writable CUDA ``float3`` +storage. ``capacity`` is the element capacity of each buffer and must cover every tracked owner. Both buffers must +belong to logical CUDA ``device``; CUDA selects the available inter-device transfer route for remote output. The call +is synchronous, so both buffers may be consumed when it returns. This reads current recorded dT forces and does not +trigger contact detection or force evaluation. Contact recording must remain enabled.)doc", + py::arg("force_destination"), py::arg("torque_destination"), py::arg("capacity"), py::arg("device")) + .def( + "OwnerWildcardValuesToDevice", + [](deme::DEMTracker& tracker, const std::string& name, std::uintptr_t pointer, size_t capacity, int device, + bool validate) { + tracker.OwnerWildcardValuesToDevice(name, reinterpret_cast(pointer), capacity, device, + validate); + }, + "Synchronously copy the named ``float32`` owner wildcard for every tracked owner to caller-owned CUDA " + "storage. ``capacity`` is in owner elements.", + py::arg("name"), py::arg("pointer"), py::arg("capacity"), py::arg("device"), py::arg("validate") = true) + + .def("SetPos", static_cast(&deme::DEMTracker::SetPos), + "Set the position of this tracked object.", py::arg("pos"), py::arg("offset") = 0) + .def("SetPos", static_cast&)>(&deme::DEMTracker::SetPos), + "Set the positions of consecutive tracked objects.", py::arg("pos")) + + .def("SetAngVel", static_cast(&deme::DEMTracker::SetAngVel), + "Set the angular velocity of this tracked object in its own local coordinate system.", py::arg("angVel"), + py::arg("offset") = 0) + .def("SetAngVel", + static_cast&)>(&deme::DEMTracker::SetAngVel), + "Set the angular velocity of consecutive tracked objects in their own local coordinate systems.", + py::arg("angVel")) + + .def("SetVel", static_cast(&deme::DEMTracker::SetVel), + "Set the velocity of this tracked object in global frame.", py::arg("vel"), py::arg("offset") = 0) + .def("SetVel", static_cast&)>(&deme::DEMTracker::SetVel), + "Set the velocity of consecutive tracked objects in global frame.", py::arg("vel")) + + .def("SetOriQ", static_cast(&deme::DEMTracker::SetOriQ), + "Set the quaternion which represents the orientation of this tracked object's coordinate system.", + py::arg("oriQ"), py::arg("offset") = 0) + .def("SetOriQ", static_cast&)>(&deme::DEMTracker::SetOriQ), + "Set the quaternion which represents the orientation of consecutive tracked objects' coordinate systems.", + py::arg("oriQ")) + + .def("AddAcc", static_cast(&deme::DEMTracker::AddAcc), + "Add an extra acc to the tracked body, for the next time step. Note if the user intends to add a " + "persistent external force, then using family prescription is the better method.", + py::arg("acc"), py::arg("offset") = 0) + .def("AddAcc", static_cast&)>(&deme::DEMTracker::AddAcc), + "Add an extra acc to consecutive tracked objects, (only) for the next time step. Note if the user intends " + "to add a persistent external force, then using family prescription is the better method.", + py::arg("acc")) + .def( + "AddAccFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t source, int device, bool validate) { + tracker.AddAccFromDevice(reinterpret_cast(source), device, validate); + }, + R"doc(Synchronously queue one global-frame linear acceleration per tracked owner from CUDA memory. + +``source`` is an integer address containing one CUDA ``float3`` per tracked owner in tracker order. Remote input is +copied from ``source_device`` to the dynamic-worker device before unpacking. Values replace any previously queued +next-step linear-acceleration contribution. Contact acceleration is accumulated on top during the next +force/integration step, gravity is applied separately, and the queued contribution is consumed after one step. Pass +``validate=False`` only when the range and pointer metadata are known to be valid. The source buffer may be reused when +this call returns.)doc", + py::arg("source"), py::arg("source_device"), py::arg("validate") = true) + + .def("AddAngAcc", static_cast(&deme::DEMTracker::AddAngAcc), + "Add an extra angular acceleration to the tracked body, for the next time step. Note if the user intends " + "to add a persistent external torque, then using family prescription is the better method.", + py::arg("angAcc"), py::arg("offset") = 0) + .def("AddAngAcc", + static_cast&)>(&deme::DEMTracker::AddAngAcc), + "Add an extra angular acceleration to consecutive tracked objects, (only) for the next time step. Note if " + "the user intends to add a persistent external torque, then using family prescription is the better " + "method.", + py::arg("angAcc")) + .def( + "AddAngAccFromDevice", + [](deme::DEMTracker& tracker, std::uintptr_t source, int device, bool validate) { + tracker.AddAngAccFromDevice(reinterpret_cast(source), device, validate); + }, + R"doc(Synchronously queue one local-frame angular acceleration per tracked owner from CUDA memory. + +``source`` is an integer address containing one CUDA ``float3`` per tracked owner in tracker order. Remote input is +copied from ``source_device`` to the dynamic-worker device before unpacking. Values use each owner's local principal- +axis frame and replace any previously queued next-step angular-acceleration contribution. Contact angular acceleration +is accumulated on top during the next force/integration step, and the queued contribution is consumed after one step. +Pass ``validate=False`` only when the range and pointer metadata are known to be valid. The source buffer may be reused +when this call returns.)doc", + py::arg("source"), py::arg("source_device"), py::arg("validate") = true) + + .def("SetFamily", static_cast(&deme::DEMTracker::SetFamily), + "Change the family numbers of all the entities tracked by this tracker.", py::arg("fam_num")) + .def("SetFamily", static_cast(&deme::DEMTracker::SetFamily), + "Change the family number of one entities tracked by this tracker.", py::arg("fam_num"), py::arg("offset")) + + .def("UpdateMesh", + static_cast&)>(&deme::DEMTracker::UpdateMesh), + "Apply the new mesh node positions such that the tracked mesh is replaced by the new_nodes.", + py::arg("new_nodes")) + .def("UpdateMeshByIncrement", + static_cast&)>( + &deme::DEMTracker::UpdateMeshByIncrement), + "Change the coordinates of each mesh node by the given amount.", py::arg("deformation")) + + .def("GetMeshNodesGlobal", + static_cast (deme::DEMTracker::*)()>(&deme::DEMTracker::GetMeshNodesGlobal), + "Get the current locations of all the nodes in the mesh being tracked.") + + .def("GetMesh", &deme::DEMTracker::GetMesh, "Get a handle for the mesh this tracker is tracking.") + + .def("SetOwnerWildcardValue", &deme::DEMTracker::SetOwnerWildcardValue, + "Set a wildcard value of the owner this tracker is tracking.", py::arg("name"), py::arg("wc"), + py::arg("offset") = 0) + .def("SetOwnerWildcardValues", &deme::DEMTracker::SetOwnerWildcardValues, + "Set a wildcard value of the owner this tracker is tracking.", py::arg("name"), py::arg("wc")) + + .def( + "GetContactForcesAndLocalTorque", + static_cast&, std::vector&, std::vector&, + size_t)>(&deme::DEMTracker::GetContactForcesAndLocalTorque), + "Get all contact forces and local torques that concern this tracked object. Returns number of force pairs.", + py::arg("points"), py::arg("forces"), py::arg("torques"), py::arg("offset") = 0) + .def( + "GetContactForcesAndLocalTorqueForAll", + static_cast&, std::vector&, std::vector&)>( + &deme::DEMTracker::GetContactForcesAndLocalTorqueForAll), + "Get all contact forces and local torques that concern all objects tracked by this tracker. Returns number " + "of force pairs.", + py::arg("points"), py::arg("forces"), py::arg("torques")) + + .def("GetContactForcesAndGlobalTorque", + static_cast&, std::vector&, std::vector&, + size_t)>(&deme::DEMTracker::GetContactForcesAndGlobalTorque), + "Get all contact forces and global torques that concern this tracked object. Returns number of force " + "pairs.", + py::arg("points"), py::arg("forces"), py::arg("torques"), py::arg("offset") = 0) + .def( + "GetContactForcesAndGlobalTorqueForAll", + static_cast&, std::vector&, std::vector&)>( + &deme::DEMTracker::GetContactForcesAndGlobalTorqueForAll), + "Get all contact forces and global torques that concern all objects tracked by this tracker. Returns " + "number of force pairs.", + py::arg("points"), py::arg("forces"), py::arg("torques")) + + .def("GetContactForces", + static_cast&, std::vector&, size_t)>( + &deme::DEMTracker::GetContactForces), + "Get all contact forces that concern this tracked object. Returns number of force pairs.", + py::arg("points"), py::arg("forces"), py::arg("offset") = 0) + .def("GetContactForcesForAll", + static_cast&, std::vector&)>( + &deme::DEMTracker::GetContactForcesForAll), + "Get all contact forces that concern all objects tracked by this tracker. Returns number of force pairs.", + py::arg("points"), py::arg("forces")) + .def( + "GetContactForcesForAllToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t points, std::uintptr_t forces, size_t capacity, int device) { + return tracker.GetContactForcesForAllToDevice(reinterpret_cast(points), + reinterpret_cast(forces), capacity, device); + }, + R"doc( +Synchronously compact contact points and forces for all tracked owners into CUDA ``float3`` arrays. + +Each pointer must address ``capacity`` elements on logical CUDA ``device``. +Capacity must cover the simulation's total recorded-contact count. The return +value is the number of valid compacted rows. Contact force recording must be +enabled. +)doc", + py::arg("points"), py::arg("forces"), py::arg("capacity"), py::arg("device")) + .def( + "GetContactForcesAndLocalTorqueForAllToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t points, std::uintptr_t forces, std::uintptr_t torques, + size_t capacity, int device) { + return tracker.GetContactForcesAndLocalTorqueForAllToDevice( + reinterpret_cast(points), reinterpret_cast(forces), + reinterpret_cast(torques), capacity, device); + }, + "Synchronously compact contact points, forces, and owner-local extra torques into caller-owned CUDA " + "``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number " + "of valid rows.", + py::arg("points"), py::arg("forces"), py::arg("torques"), py::arg("capacity"), py::arg("device")) + .def( + "GetContactForcesAndGlobalTorqueForAllToDevice", + [](deme::DEMTracker& tracker, std::uintptr_t points, std::uintptr_t forces, std::uintptr_t torques, + size_t capacity, int device) { + return tracker.GetContactForcesAndGlobalTorqueForAllToDevice( + reinterpret_cast(points), reinterpret_cast(forces), + reinterpret_cast(torques), capacity, device); + }, + "Synchronously compact contact points, forces, and global-frame extra torques into caller-owned CUDA " + "``float3`` arrays. Capacity must cover the total recorded-contact count; the return value is the number " + "of valid rows.", + py::arg("points"), py::arg("forces"), py::arg("torques"), py::arg("capacity"), py::arg("device")); + + py::class_>(obj, "DEMForceModel") + .def(py::init(), py::arg("model"), + "Create a force-model definition of the selected built-in type.") + .def("SetForceModelType", &deme::DEMForceModel::SetForceModelType, "Set the contact force model type") + + .def("DefineCustomModel", &deme::DEMForceModel::DefineCustomModel, + "Define user-custom force model with a string which is your force calculation code.") + .def("ReadCustomModelFile", &deme::DEMForceModel::ReadCustomModelFile, + "Read user-custom force model from a file (which by default should reside in kernel/DEMUserScripts), " + "which contains your force calculation code. Returns 0 if read successfully, otherwise 1.") + .def("DefineCustomModelPrerequisites", &deme::DEMForceModel::DefineCustomModelPrerequisites, + "Define user-custom force model's utility __device__ functions with a string.") + .def("ReadCustomModelPrerequisitesFile", &deme::DEMForceModel::ReadCustomModelPrerequisitesFile, + "Read user-custom force model's utility __device__ functions from a file (which by default should reside " + "in kernel/DEMUserScripts). Returns 0 if read successfully, otherwise 1.") + + .def("SetMustHaveMatProp", &deme::DEMForceModel::SetMustHaveMatProp, + "Specifiy the material properties that this force model will use") + .def("SetMustPairwiseMatProp", &deme::DEMForceModel::SetMustPairwiseMatProp, + "Specifiy the material properties that are pair-wise (instead of being associated with each individual " + "material).") + .def("SetPerContactWildcards", &deme::DEMForceModel::SetPerContactWildcards, + "Set the names for the extra quantities that will be associated with each contact pair. For example, " + "history-based models should have 3 float arrays to store contact history. Only float is supported. Note " + "the initial value of all contact wildcard arrays is automatically 0") + .def("SetPerOwnerWildcards", &deme::DEMForceModel::SetPerOwnerWildcards, + " Set the names for the extra quantities that will be associated with each owner. For example, you can " + "use this to associate a cohesion parameter to each particle. Only float is supported."); + + // The scene/frame interface also supports external renderers without the optional native viewer library. + py::class_(obj, "VisualizationSphere") + .def_readonly("offset", &deme::DEMVisualizationScene::Sphere::offset) + .def_readonly("radius", &deme::DEMVisualizationScene::Sphere::radius) + .def_readonly("owner", &deme::DEMVisualizationScene::Sphere::owner); + py::class_(obj, "VisualizationTriangle") + .def_readonly("a", &deme::DEMVisualizationScene::Triangle::a) + .def_readonly("b", &deme::DEMVisualizationScene::Triangle::b) + .def_readonly("c", &deme::DEMVisualizationScene::Triangle::c) + .def_readonly("owner", &deme::DEMVisualizationScene::Triangle::owner); + py::class_(obj, "VisualizationScene") + .def_readonly("revision", &deme::DEMVisualizationScene::revision) + .def_readonly("spheres", &deme::DEMVisualizationScene::spheres) + .def_readonly("triangles", &deme::DEMVisualizationScene::triangles); + py::class_(obj, "VisualizationFrame") + .def(py::init<>()) + .def_readonly("simulation_time", &deme::DEMVisualizationFrame::simulation_time) + .def_readonly("revision", &deme::DEMVisualizationFrame::revision) + .def_readonly("positions", &deme::DEMVisualizationFrame::positions) + .def_readonly("orientations", &deme::DEMVisualizationFrame::orientations) + .def_readonly("families", &deme::DEMVisualizationFrame::families) + .def_readonly("velocities", &deme::DEMVisualizationFrame::velocities); + + py::class_( + obj, "DEMSolver", + R"doc( +Own a DEM simulation and its dynamic and kinematic CUDA workers. + +Configure materials, geometry, particles, solver options, and trackers before +calling ``Initialize``. Device placement is fixed at construction. Keep this +object alive while using any solver-owned material, template, tracker, or +inspector handle. + +Fixed-size owner CUDA exchange methods validate ranges, capacities, and pointer metadata by default. Pass +``validate=False`` only when those preconditions are guaranteed and avoiding validation overhead matters. Required +device routing and documented transformations still apply. +)doc") + .def("GetVisualizationScene", &deme::DEMSolver::GetVisualizationScene) + .def("GetVisualizationFrame", &deme::DEMSolver::GetVisualizationFrame, + py::arg("frame"), py::arg("include_velocities") = false) + .def(py::init(), py::arg("nGPUs") = 2, + R"doc( +Construct a solver using one or two logical CUDA devices. + +With one GPU, both workers use device 0. With two requested GPUs and at least +two visible devices, the dynamic worker uses device 0 and the kinematic worker +uses device 1. If only one device is visible, both workers use device 0 and a +warning is emitted. Only 1 and 2 are valid values. +)doc") + .def(py::init&>(), py::arg("device_ids"), + R"doc( +Construct a solver on explicit logical CUDA device IDs. + +One ID places both workers on that device. For two IDs, the first selects the +dynamic worker and the second selects the kinematic worker. Repeated IDs are +valid. IDs use the numbering visible to this process, including the effects of +``CUDA_VISIBLE_DEVICES``. +)doc") + .def("GetGPUDeviceIDs", &deme::DEMSolver::GetGPUDeviceIDs, + "Return ``[dynamic_worker_device, kinematic_worker_device]`` using process-visible logical CUDA IDs.") + .def("UpdateStepSize", &deme::DEMSolver::UpdateStepSize, + "Legacy name for SetTimeStepSize, kept for backward compatibility.", py::arg("ts") = -1.0) + .def("SetNoForceRecord", &deme::DEMSolver::SetNoForceRecord, + "Instruct the solver that there is no need to record the contact force (and contact point location etc.) " + "in an array. After initialization, call UpdateSimParams() for this change to take effect in dT.", + py::arg("flag") = true) + .def("LoadSphereType", + static_cast (deme::DEMSolver::*)( + float, float, const std::shared_ptr&)>(&deme::DEMSolver::LoadSphereType), + "Load a reusable one-sphere clump template, deriving its spherical moment of inertia.", py::arg("mass"), + py::arg("radius"), py::arg("material")) + .def("LoadSphereType", + static_cast (deme::DEMSolver::*)( + float, float, float, const std::shared_ptr&)>(&deme::DEMSolver::LoadSphereType), + "Load a reusable one-sphere clump template with an explicitly supplied scalar moment of inertia.", + py::arg("mass"), py::arg("moi"), py::arg("radius"), py::arg("material")) + .def("LoadCombinedClumpType", + static_cast (deme::DEMSolver::*)( + const std::vector>&, const std::vector&, + const std::vector&, size_t)>(&deme::DEMSolver::LoadCombinedClumpType), + "Load a rigid combined-clump template with fixed member-relative transforms.", + py::arg("component_templates"), py::arg("component_rel_pos"), + py::arg("component_rel_oriQ") = std::vector(), py::arg("master_component") = 0) + .def("LoadCombinedMeshType", + static_cast (deme::DEMSolver::*)( + const std::vector>&, const std::vector&, + const std::vector&, size_t)>(&deme::DEMSolver::LoadCombinedMeshType), + "Load a rigid combined-mesh template with fixed member-relative transforms.", + py::arg("component_templates"), py::arg("component_rel_pos"), + py::arg("component_rel_oriQ") = std::vector(), py::arg("master_component") = 0) + .def("AddCombinedFromTemplate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&, const std::vector&, + const std::vector&)>(&deme::DEMSolver::AddCombinedFromTemplate), + "Instantiate a combined template at one or more user-specified global poses (batch).", + py::arg("combined_template"), py::arg("init_pos"), py::arg("init_oriQ") = std::vector()) + .def("AddCombinedFromTemplate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&, const float3&, const float4&)>( + &deme::DEMSolver::AddCombinedFromTemplate), + "Instantiate a combined template at a single user-specified global pose.", py::arg("combined_template"), + py::arg("init_pos") = make_float3(0), py::arg("init_oriQ") = make_float4(0, 0, 0, 1)) + .def("GetNumCombinedInstances", &deme::DEMSolver::GetNumCombinedInstances, + "Return the number of combined instances currently cached.") + .def( + "GetCombinedInstanceInfo", + [](deme::DEMSolver& self, size_t combined_instance_id) { + deme::bodyID_t master_owner_id = deme::NULL_BODYID; + std::vector member_owner_ids; + std::vector member_rel_pos; + std::vector member_rel_oriQ; + const bool ok = self.GetCombinedInstanceInfo(combined_instance_id, master_owner_id, member_owner_ids, + member_rel_pos, member_rel_oriQ); + return py::make_tuple(ok, master_owner_id, member_owner_ids, member_rel_pos, member_rel_oriQ); + }, + "Query combined-instance metadata; returns (ok, master_owner_id, member_owner_ids, member_rel_pos, " + "member_rel_oriQ).", + py::arg("combined_instance_id")) + .def("EnsureKernelErrMsgLineNum", &deme::DEMSolver::EnsureKernelErrMsgLineNum, + "If true, each jitification string substitution will do a one-liner to one-liner replacement, so that if " + "the kernel compilation fails, the error meessage line number will reflex the actual spot where that " + "happens (instead of some random number).", + py::arg("flag") = true) + .def("SetCollectAccRightAfterForceCalc", &deme::DEMSolver::SetCollectAccRightAfterForceCalc, + "Reduce contact forces to accelerations right after calculating them, in the same kernel. This may give " + "some performance boost if you have only polydisperse spheres, no clumps. After initialization, call " + "UpdateSimParams() for this change to take effect in dT.", + py::arg("flag") = true) + + .def("SetInitBinSize", &deme::DEMSolver::SetInitBinSize, + " Explicitly instruct the bin size (for contact detection) that the solver should use.") + .def("SetOutputFormat", + static_cast(&deme::DEMSolver::SetOutputFormat), + "Choose sphere and clump output file format by name.", py::arg("format")) + .def("SetOutputFormat", + static_cast(&deme::DEMSolver::SetOutputFormat), + "Choose sphere and clump output file format. VTK is supported by WriteSphereFile.", py::arg("format")) + .def("GetNumContacts", &deme::DEMSolver::GetNumContacts, + "Get the number of kT-reported potential contact pairs.") + .def( + "SetOwnerPositionFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.SetOwnerPositionFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously set consecutive owner positions from ``count`` CUDA ``float3`` elements. + +Positions are global-frame values. ``source_device`` identifies the CUDA-accessible source allocation; remote input is +copied to the dynamic-worker device before unpacking. When this call returns the source buffer may be reused.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "SetOwnerOriQFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.SetOwnerOriQFromDevice(owner_id, reinterpret_cast(source), device, count, validate); + }, + R"doc(Synchronously set consecutive owner orientations from ``count`` CUDA ``float4`` elements. + +Quaternion elements use public DEME ordering ``(x, y, z, w)``. ``source_device`` identifies the CUDA-accessible source +allocation; remote input is copied to the dynamic-worker device before validation and unpacking. With +``validate=True``, non-finite and zero-length quaternions are rejected. Every finite, nonzero quaternion is normalized +before storage even when validation is disabled. When this call returns the source buffer may be reused.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "SetOwnerVelocityFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.SetOwnerVelocityFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously set consecutive global linear velocities from ``count`` CUDA ``float3`` elements. + +``source_device`` identifies the CUDA-accessible source allocation; remote input is copied to the dynamic-worker +device before unpacking. When this call returns the source buffer may be reused.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "SetOwnerAngVelFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.SetOwnerAngVelFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously set consecutive local-frame angular velocities from CUDA ``float3`` elements. + +This has the same frame semantics as ``SetOwnerAngVel``. ``source_device`` identifies the CUDA-accessible source +allocation; remote input is copied to the dynamic-worker device before unpacking. When this call returns the source +buffer may be reused.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "SetOwnerAngVelGlobalFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.SetOwnerAngVelGlobalFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously set consecutive global angular velocities from ``count`` CUDA ``float3`` elements. + +Each value is converted to the owner's local principal-axis frame using that owner's orientation at call time. +``source_device`` identifies the CUDA-accessible source allocation; remote input is copied to the dynamic-worker +device before unpacking. When this call returns the source buffer may be reused.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "AddOwnerNextStepAccFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.AddOwnerNextStepAccFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously queue global-frame linear accelerations for consecutive owners from CUDA memory. + +``source`` is an integer address containing ``count`` CUDA ``float3`` values. Remote input is copied from +``source_device`` to the dynamic-worker device before unpacking. Values replace any previously queued next-step +linear-acceleration contribution. Contact acceleration is accumulated on top during the next force/integration step, +gravity is applied separately, and the queued contribution is consumed after one step. Pass ``validate=False`` only +when the owner range and pointer metadata are known to be valid. A zero count is a no-op; otherwise the source buffer +may be reused when this call returns.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "AddOwnerNextStepAngAccFromDevice", + [](deme::DEMSolver& self, deme::bodyID_t owner_id, std::uintptr_t source, int device, size_t count, + bool validate) { + self.AddOwnerNextStepAngAccFromDevice(owner_id, reinterpret_cast(source), device, count, + validate); + }, + R"doc(Synchronously queue local-frame angular accelerations for consecutive owners from CUDA memory. + +``source`` is an integer address containing ``count`` CUDA ``float3`` values. Remote input is copied from +``source_device`` to the dynamic-worker device before unpacking. Values use each owner's local principal-axis frame +and replace any previously queued next-step angular-acceleration contribution. Contact angular acceleration is +accumulated on top during the next force/integration step, and the queued contribution is consumed after one step. Pass +``validate=False`` only when the owner range and pointer metadata are known to be valid. A zero count is a no-op; +otherwise the source buffer may be reused when this call returns.)doc", + py::arg("owner_id"), py::arg("source"), py::arg("source_device"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "GetOwnerPositionToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerPositionToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive global owner positions as CUDA float3 elements.", py::arg("destination"), + py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "GetOwnerOriQToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerOriQToDevice(reinterpret_cast(destination), capacity, device, owner_id, count, + validate); + }, + "Synchronously write consecutive owner quaternions as CUDA float4 elements in (x, y, z, w) order.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerAngVelLocalToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerAngVelLocalToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive local principal-axis-frame owner angular velocities as CUDA float3 " + "elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerVelocityToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerVelocityToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive global owner velocities as CUDA float3 elements.", py::arg("destination"), + py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "GetOwnerAngVelGlobalToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerAngVelGlobalToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive global owner angular velocities as CUDA float3 elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerAccToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerAccToDevice(reinterpret_cast(destination), capacity, device, owner_id, count, + validate); + }, + "Synchronously write consecutive global contact accelerations as CUDA float3 elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerAngAccGlobalToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerAngAccGlobalToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive global contact angular accelerations as CUDA float3 elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerAngAccLocalToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerAngAccLocalToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive local principal-axis-frame contact angular accelerations as CUDA " + "float3 elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerFamilyToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerFamilyToDevice(reinterpret_cast(destination), capacity, device, owner_id, + count, validate); + }, + "Synchronously write consecutive owner family numbers as CUDA uint32 elements.", py::arg("destination"), + py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "GetOwnerMassToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerMassToDevice(reinterpret_cast(destination), capacity, device, owner_id, count, + validate); + }, + "Synchronously write consecutive owner masses as CUDA float elements.", py::arg("destination"), + py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), py::arg("count") = 1, + py::arg("validate") = true) + .def( + "GetOwnerMOIToDevice", + [](const deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count, bool validate) { + self.GetOwnerMOIToDevice(reinterpret_cast(destination), capacity, device, owner_id, count, + validate); + }, + "Synchronously write consecutive owner principal moments of inertia as CUDA float3 elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerWildcardValueToDevice", + [](deme::DEMSolver& self, std::uintptr_t destination, size_t capacity, int device, + deme::bodyID_t owner_id, const std::string& name, deme::bodyID_t count, bool validate) { + self.GetOwnerWildcardValueToDevice(reinterpret_cast(destination), capacity, device, owner_id, + name, count, validate); + }, + "Synchronously write one named owner wildcard for consecutive owners as CUDA float elements.", + py::arg("destination"), py::arg("capacity"), py::arg("destination_device"), py::arg("first_owner_id"), + py::arg("name"), py::arg("count") = 1, py::arg("validate") = true) + .def( + "GetOwnerContactWrench", + [](const deme::DEMSolver& self, deme::bodyID_t owner_id, deme::bodyID_t count) { + std::vector forces; + std::vector torques; + self.GetOwnerContactWrench(forces, torques, owner_id, count); + return py::make_tuple(forces, torques); + }, + R"doc(Return one reduced contact wrench per consecutive owner. + +The returned tuple is ``(forces, torques)``; each vector is resized to ``count`` and follows consecutive owner order. +Force and torque are global-frame resultants, and torque is measured about DEME's current owner position. Torque +includes both force-generated moments and force-model-only torque such as rolling resistance. Owners without recorded +contact receive zero force and torque. This reads current dT force records and does not trigger contact detection or +force evaluation. ``first_owner_id`` selects the first owner in the range. Contact recording must remain enabled. A +zero count returns two empty vectors.)doc", + py::arg("first_owner_id"), py::arg("count") = 1) + .def( + "GetOwnerContactWrenchToDevice", + [](const deme::DEMSolver& self, std::uintptr_t forces, std::uintptr_t torques, size_t capacity, int device, + deme::bodyID_t owner_id, deme::bodyID_t count) { + self.GetOwnerContactWrenchToDevice(reinterpret_cast(forces), + reinterpret_cast(torques), capacity, device, owner_id, + count); + }, + R"doc(Synchronously write one reduced contact wrench per consecutive owner to CUDA ``float3`` buffers. + +Force and torque are global-frame resultants, and torque is measured about DEME's current owner position. Torque +includes both force-generated moments and force-model-only torque such as rolling resistance. ``force_destination`` +and ``torque_destination`` are integer addresses of writable CUDA ``float3`` storage. ``capacity`` and ``count`` are +measured in owners, and ``first_owner_id`` selects the start of the range; owners without contact receive zero force +and torque. This reads current dT force records and does not trigger contact detection or force evaluation. Both +buffers belong to logical CUDA ``destination_device``; CUDA selects the available inter-device transfer route for +remote output. The call is synchronous, so both buffers may be consumed when it returns. Contact recording must remain +enabled. A zero count is a no-op.)doc", + py::arg("force_destination"), py::arg("torque_destination"), py::arg("capacity"), + py::arg("destination_device"), py::arg("first_owner_id"), py::arg("count") = 1) + .def("GetTimeStepSize", &deme::DEMSolver::GetTimeStepSize, "Get the current time step size in simulation.") + .def("SetCDUpdateFreq", &deme::DEMSolver::SetCDUpdateFreq, + "Set the number of dT steps before it waits for a contact-pair update. After initialization, call " + "UpdateSimParams() for this change to take effect in the workers.") + .def("GetSimTime", &deme::DEMSolver::GetSimTime, + "Get the simulation time passed since the start of simulation.") + .def("SetSimTime", &deme::DEMSolver::SetSimTime, + "Get the simulation time passed since the start of simulation.") + .def("UpdateClumps", &deme::DEMSolver::UpdateClumps, + "Transfer newly loaded clumps to the GPU-side in mid-simulation.") + .def("SetAdaptiveTimeStepType", &deme::DEMSolver::SetAdaptiveTimeStepType, + "Set the strategy for auto-adapting time step size. Currently, hertz_const computes one fixed setup-time " + "timestep; max_vel and int_diff are accepted but not implemented.") + .def("UseHertzConstTimeStep", &deme::DEMSolver::UseHertzConstTimeStep, + "Use the setup-time Hertzian constant timestep estimate.") + .def("SetIntegrator", + static_cast(&deme::DEMSolver::SetIntegrator), + "Set the time integrator. After initialization, call UpdateSimParams() for this change to take effect.") + .def("SetIntegrator", + static_cast(&deme::DEMSolver::SetIntegrator), + "Set the time integrator. After initialization, call UpdateSimParams() for this change to take effect.") + .def("GetInitStatus", &deme::DEMSolver::GetInitStatus, "Return whether this simulation system is initialized.") + .def("GetJitStringSubs", &deme::DEMSolver::GetJitStringSubs, + "Get the jitification string substitution laundary list. It is needed by some of this simulation system's " + "friend classes.") + .def("GetJitifyOptions", &deme::DEMSolver::GetJitifyOptions, + "Get current jitification options. It is needed by some of this simulation system's friend classes.") + .def("SetJitifyOptions", &deme::DEMSolver::SetJitifyOptions, + "Set the jitification options. It is only needed by advanced users.") + .def("SetCudaDebugSync", &deme::DEMSolver::SetCudaDebugSync, + "Set the process-wide switch that synchronizes after each CUDA operation that enqueues stream work. " + "Disable for normal asynchronous performance.", + py::arg("enable") = true) + .def("GetCudaDebugSync", &deme::DEMSolver::GetCudaDebugSync, + "Return whether process-wide CUDA debug synchronization is enabled.") + + .def("SetInitBinSizeAsMultipleOfSmallestSphere", &deme::DEMSolver::SetInitBinSizeAsMultipleOfSmallestSphere, + "Explicitly instruct the bin size (for contact detection) that the solver should use, as a multiple of " + "the radius of the smallest sphere in simulation.") + .def( + "SetInitBinNumTarget", &deme::DEMSolver::SetInitBinNumTarget, + "Set the target number of bins (for contact detection) at the start of the simulation upon initialization.") + .def("InstructNumOwners", &deme::DEMSolver::InstructNumOwners, + "Explicitly instruct the sizes for the arrays at initialization time. This is useful when the number of " + "owners tends to change (especially gradually increase) frequently in the simulation.") + .def("UseFrictionalHertzianModel", &deme::DEMSolver::UseFrictionalHertzianModel, + py::return_value_policy::reference_internal, + "Instruct the solver to use frictonal (history-based) Hertzian contact force model.") + .def("UseFrictionlessHertzianModel", &deme::DEMSolver::UseFrictionlessHertzianModel, + py::return_value_policy::reference_internal, + "Instruct the solver to use frictonless Hertzian contact force model") + .def("DefineContactForceModel", &deme::DEMSolver::DefineContactForceModel, + py::return_value_policy::reference_internal, + "Define a custom contact force model by a string. Returns a pointer to the force model in use.") + .def("ReadContactForceModel", &deme::DEMSolver::ReadContactForceModel, + py::return_value_policy::reference_internal, + "Read user custom contact force model from a file (which by default should reside in " + "kernel/DEMUserScripts). Returns a pointer to the force model in use.") + + .def("GetContactForceModel", &deme::DEMSolver::GetContactForceModel, "Get the current force model", + py::return_value_policy::reference_internal) + .def("SetSortContactPairs", &deme::DEMSolver::SetSortContactPairs, + "Instruct the solver if contact pair arrays should be sorted (based on the types of contacts) before " + "usage.") + .def( + "SetJitifyClumpTemplates", &deme::DEMSolver::SetJitifyClumpTemplates, + "Instruct the solver to rearrange and consolidate clump templates information, then jitify it into GPU " + "kernels (if set to true), rather than using flattened sphere component configuration arrays whose entries " + "are associated with individual spheres.", + py::arg("use") = true) + .def("DisableJitifyClumpTemplates", &deme::DEMSolver::DisableJitifyClumpTemplates, + "Disable jitification of clump templates (use flattened arrays instead).") + .def("SetJitifyMassProperties", &deme::DEMSolver::SetJitifyMassProperties, + "Instruct the solver to rearrange and consolidate mass property information (for all owner types), then " + "jitify it into GPU kernels (if set to true), rather than using flattened mass property arrays whose " + "entries are associated with individual owners.", + py::arg("use") = true) + .def("DisableJitifyMassProperties", &deme::DEMSolver::DisableJitifyMassProperties, + "Disable jitification of mass properties (use flattened arrays instead).") + .def("SetExpandFactor", &deme::DEMSolver::SetExpandFactor, + "(Explicitly) set the amount by which the radii of the spheres (and the thickness of the boundaries) are " + "expanded for the purpose of contact detection (safe, and creates false positives). If fix is set to " + "true, then this expand factor does not change even if the user uses variable time step size. After " + "initialization, call UpdateSimParams() for this change to take effect in the workers.", + py::arg("beta"), py::arg("fix") = true) + .def("SetMeshUniversalContact", &deme::DEMSolver::SetMeshUniversalContact, + "Set whether mesh-mesh contacts should be universally detected. Set to false to speedup simulation if " + "meshes are not expected to have contacts. Default is false.", + py::arg("use") = true) + .def("SetDEME2MeshBehavior", &deme::DEMSolver::SetDEME2MeshBehavior, + "When enabled, assign every triangle of subsequently loaded meshes to its own patch. Disabling restores " + "the normal behavior of leaving new meshes' patch assignments unchanged.", + py::arg("use") = true) + .def("SetPersistentContact", &deme::DEMSolver::SetPersistentContact, + "Set whether the solver should expect the user to mark certain contacts as persistent across kT updates. " + "Set this to true if you later will call MarkPersistentContact series of methods.", + py::arg("use") = true) + .def("SetSimplePatchCombination", &deme::DEMSolver::SetSimplePatchCombination, + "Use simple patch-ID based triangle contact combination. Disable to opt into connected-component flooding.", + py::arg("use") = true) + .def("SetStablePatchIslandIDs", &deme::DEMSolver::SetStablePatchIslandIDs, + "Stabilize flooded patch-island IDs across contact-detection steps.", py::arg("use") = true) + .def("SetMeshParticlesLowPoly", &deme::DEMSolver::SetMeshParticlesLowPoly, + "Declare that all meshed particles have a low polygon count (e.g. box, tetrahedron). When enabled, the " + "per-triangle maxTriTriPenetration array is neither computed, transferred to kT, nor used to inflate " + "contact-detection margins, saving compute time. Toggle this on only when you are confident that no " + "triangle from one mesh will ever be completely submerged inside another mesh.", + py::arg("use") = true) + + .def("SetExpandSafetyType", &deme::DEMSolver::SetExpandSafetyType, + "Select the contact-margin velocity strategy. After initialization, call UpdateSimParams() for this " + "change to take effect in the workers.") + .def("SetExpandSafetyAdder", &deme::DEMSolver::SetExpandSafetyAdder, + "Set a `base' velocity, which we will always add to our estimated maximum system velocity, when deriving " + "the thickness of the contact `safety' margin. After initialization, call UpdateSimParams() for this " + "change to take effect in the workers.") + .def("SetUseAngularVelocityMargin", &deme::DEMSolver::SetUseAngularVelocityMargin, + "Control whether angular velocity contributes to the contact detection margin.") + .def("SetMaxSphereInBin", &deme::DEMSolver::SetMaxSphereInBin, + "Used to force the solver to error out when there are too many spheres in a bin. A huge number can be " + "used to discourage this error type") + .def("SetMaxTriangleInBin", &deme::DEMSolver::SetMaxTriangleInBin, + "Used to force the solver to error out when there are too many spheres in a bin. A huge number can be " + "used to discourage this error type") + + .def( + "SetErrorOutVelocity", &deme::DEMSolver::SetErrorOutVelocity, + "Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage this " + "error type. Defaulted to 1e3.") + .def("SetErrorOutAngularVelocity", &deme::DEMSolver::SetErrorOutAngularVelocity, + "Set the angular velocity which when exceeded, the solver errors out. A huge number can be used to " + "discourage this error type. Defaulted to 1e4.") + .def("SetErrorOutAvgContacts", &deme::DEMSolver::SetErrorOutAvgContacts, + "Set the average number of contacts a sphere has, before the solver errors out. A huge number can be used " + "to discourage this error type. Defaulted to 100") + .def("SetTriTriPenetration", &deme::DEMSolver::SetTriTriPenetration, + "Manually set the current triangle-triangle penetration value in dT. This allows the user to directly " + "control the maxTriTriPenetration value which will ONLY be used in the NEXT contact detection run in kT.") + .def("SetMaxTriTriPenetration", &deme::DEMSolver::SetMaxTriTriPenetration, + "Set the maximum allowed triangle-triangle penetration used as the margin added in kT contact detection. " + "This value caps the penetration margin added to prevent excessively large values.") + .def("SetTriTriContactRejectionRatio", &deme::DEMSolver::SetTriTriContactRejectionRatio, + "Set the ratio threshold for rejecting suspicious tri-tri contacts. A contact is rejected when the " + "penetration depth exceeds this fraction of the contact-point-to-mesh-center distance for either mesh " + "involved. A negative value disables the guard entirely.") + .def("GetAvgSphContacts", &deme::DEMSolver::GetAvgSphContacts, + "Get the current number of contacts each sphere has") + .def("UseAdaptiveBinSize", &deme::DEMSolver::UseAdaptiveBinSize, + "Enable or disable adaptive bin size. After initialization, call UpdateSimParams() for this change to " + "take effect in the workers.", py::arg("use") = true) + .def("DisableAdaptiveBinSize", &deme::DEMSolver::DisableAdaptiveBinSize, + "Disable adaptive bin size. After initialization, call UpdateSimParams() for this change to take effect " + "in the workers.") + .def("UseAdaptiveUpdateFreq", &deme::DEMSolver::UseAdaptiveUpdateFreq, + "Enable or disable adaptive contact-update frequency. After initialization, call UpdateSimParams() for " + "this change to take effect in the workers.", py::arg("use") = true) + .def("DisableAdaptiveUpdateFreq", &deme::DEMSolver::DisableAdaptiveUpdateFreq, + "Disable adaptive contact-update frequency. After initialization, call UpdateSimParams() for this " + "change to take effect in the workers.") + .def("SetAdaptiveBinSizeDelaySteps", &deme::DEMSolver::SetAdaptiveBinSizeDelaySteps, + "Adjust how frequently kT updates the bin size. After initialization, call UpdateSimParams().") + .def("SetAdaptiveBinSizeMaxRate", &deme::DEMSolver::SetAdaptiveBinSizeMaxRate, + "Set the maximum bin-size change rate. After initialization, call UpdateSimParams().") + .def("SetAdaptiveBinSizeAcc", &deme::DEMSolver::SetAdaptiveBinSizeAcc, + "Set the bin-size adjustment acceleration. After initialization, call UpdateSimParams().") + .def("SetAdaptiveBinSizeUpperProactivity", &deme::DEMSolver::SetAdaptiveBinSizeUpperProactivity, + "Set how proactive the solver is in avoiding the bin being too big (leading to too many geometries in a " + "bin). After initialization, call UpdateSimParams().") + .def( + "SetAdaptiveBinSizeLowerProactivity", &deme::DEMSolver::SetAdaptiveBinSizeLowerProactivity, + "Set how proactive the solver is in avoiding undersized bins. After initialization, call UpdateSimParams().") + .def("GetBinSize", &deme::DEMSolver::GetBinSize, + "Get the current bin (for contact detection) size. Must be called from synchronized stance.") + .def("GetBinNum", &deme::DEMSolver::GetBinNum, + "Get the current number of bins (for contact detection). Must be called from synchronized stance.") + .def("SetCDMaxUpdateFreq", &deme::DEMSolver::SetCDMaxUpdateFreq, + "Set the upper bound of kT update frequency. After initialization, call UpdateSimParams().") + .def("SetCDNumStepsMaxDriftAheadOfAvg", &deme::DEMSolver::SetCDNumStepsMaxDriftAheadOfAvg, + "Set dT's drift allowance above average. After initialization, call UpdateSimParams().") + .def("SetCDNumStepsMaxDriftMultipleOfAvg", &deme::DEMSolver::SetCDNumStepsMaxDriftMultipleOfAvg, + "Set dT's maximum-drift multiplier. After initialization, call UpdateSimParams().") + .def("SetCDNumStepsMaxDriftHistorySize", &deme::DEMSolver::SetCDNumStepsMaxDriftHistorySize, + "Set how many past kinematic-worker updates calibrate the maximum future drift limit. The default is " + "recommended for normal use. After initialization, call UpdateSimParams().", + py::arg("n")) + .def("GetUpdateFreq", &deme::DEMSolver::GetUpdateFreq, "Get the current update frequency used by the solver.") + .def("SetForceCalcThreadsPerBlock", &deme::DEMSolver::SetForceCalcThreadsPerBlock, + "Set the number of threads per block in force calculation (default 256).") + .def("Duplicate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), + "Duplicate a material that is loaded into the system.") + .def("Duplicate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), + "Duplicate a clump template that is loaded into the system.") + .def("Duplicate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&)>(&deme::DEMSolver::Duplicate), + "Duplicate a batch of clumps that is loaded into the system.") + .def("AddExternalObject", &deme::DEMSolver::AddExternalObject, + "Add an analytical object to the simulation system.") + .def( + "SetOutputContent", + static_cast&)>(&deme::DEMSolver::SetOutputContent), + "Specify the information that needs to go into the clump or sphere output files.") + .def("SetMeshOutputFormat", + static_cast(&deme::DEMSolver::SetMeshOutputFormat), + "Specify the output file format of meshes.") + .def("SetMeshOutputContent", + static_cast&)>( + &deme::DEMSolver::SetMeshOutputContent), + "Specify per-triangle fields to include in mesh VTK output.", py::arg("content")) + .def("SetMeshOutputContent", + static_cast( + &deme::DEMSolver::SetMeshOutputContent), + "Specify one per-triangle field to include in mesh VTK output.", py::arg("content")) + .def("EnableMeshPatchColorOutput", &deme::DEMSolver::EnableMeshPatchColorOutput, + "Enable or disable patch color metadata in PLY mesh output.", py::arg("enable") = true) + .def("SetContactOutputContent", + static_cast&)>( + &deme::DEMSolver::SetContactOutputContent), + "Specify the information that needs to go into the contact pair output files.") + .def("SetContactOutputFormat", + static_cast(&deme::DEMSolver::SetContactOutputFormat), + "Specify the file format of contact pairs.") + .def("EnableOwnerWildcardOutput", &deme::DEMSolver::EnableOwnerWildcardOutput, + "Enable or disable owner wildcard output.", py::arg("enable") = true) + .def("EnableContactWildcardOutput", &deme::DEMSolver::EnableContactWildcardOutput, + "Enable or disable contact wildcard output.", py::arg("enable") = true) + .def("SetAllowIntraCombinedOwnerContacts", &deme::DEMSolver::SetAllowIntraCombinedOwnerContacts, + "Allow or suppress contacts among owners belonging to the same combined owner group.", + py::arg("allow") = true) + .def("SetVerbosity", static_cast(&deme::DEMSolver::SetVerbosity), + "Set the verbosity level of the solver. Select from 'QUIET', 'ERROR', 'WARNING', 'INFO', 'METRIC' or " + "'DEBUG'. Recommend 'INFO'.") + .def("AddKernelInclude", &deme::DEMSolver::AddKernelInclude, + "Add a library that the kernels will be compiled with (so that the user can use the provided methods in " + "their customized code, like force model).") + .def("SetKernelInclude", &deme::DEMSolver::SetKernelInclude, + "Set the kernels' headers' extra include lines. Useful for customization.") + .def("RemoveKernelInclude", &deme::DEMSolver::RemoveKernelInclude, + "Remove all extra libraries that the kernels `include' in their headers.") + + .def("LoadMaterial", + static_cast (deme::DEMSolver::*)( + const std::unordered_map&)>(&deme::DEMSolver::LoadMaterial), + "Load materials properties (Young's modulus, Poisson's ratio...) into the system.") + .def("LoadMaterial", + static_cast (deme::DEMSolver::*)(deme::DEMMaterial&)>( + &deme::DEMSolver::LoadMaterial), + "Load materials properties into the system.") + .def("InstructBoxDomainDimension", + static_cast( + &deme::DEMSolver::InstructBoxDomainDimension), + "Set the Box Domain Dimension", py::arg("x"), py::arg("y"), py::arg("z"), py::arg("dir_exact") = "none") + .def("InstructBoxDomainDimension", + static_cast&, const std::pair&, + const std::pair&, const std::string& dir_exact)>( + &deme::DEMSolver::InstructBoxDomainDimension), + "Set the span of the Box Domain", py::arg("x"), py::arg("y"), py::arg("z"), py::arg("dir_exact") = "none") + .def("LoadMeshType", + static_cast (deme::DEMSolver::*)( + const std::string&, const std::shared_ptr&, bool, bool)>( + &deme::DEMSolver::LoadMeshType), + "Load a mesh template into cache so it can be instantiated repeatedly.", py::arg("filename"), + py::arg("mat"), py::arg("load_normals") = true, py::arg("load_uv") = false) + .def("LoadMeshType", + static_cast (deme::DEMSolver::*)(const std::string&, bool, bool)>( + &deme::DEMSolver::LoadMeshType), + "Load a mesh template into cache (without explicitly assigning material here).", py::arg("filename"), + py::arg("load_normals") = true, py::arg("load_uv") = false) + .def("LoadMeshType", + static_cast (deme::DEMSolver::*)(deme::DEMMesh&)>( + &deme::DEMSolver::LoadMeshType), + "Load a user-constructed mesh object as a reusable template.", py::arg("mesh")) + .def("AddMeshFromTemplate", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&, const float3&)>(&deme::DEMSolver::AddMeshFromTemplate), + "Instantiate a mesh from a cached template at the requested initial position.", py::arg("mesh_template"), + py::arg("init_pos") = make_float3(0)) + .def("InstructBoxDomainBoundingBC", &deme::DEMSolver::InstructBoxDomainBoundingBC, + "Instruct if and how we should add boundaries to the simulation world upon initialization. Choose between " + "`none', `all' (add 6 boundary planes) and `top_open' (add 5 boundary planes and leave the z-directon top " + "open). Also specifies the material that should be assigned to those bounding boundaries.") + .def("SetMaterialPropertyPair", &deme::DEMSolver::SetMaterialPropertyPair, + "Set the value for a material property that by nature involves a pair of a materials (e.g. friction " + "coefficient).") + .def("AddBCPlane", + static_cast (deme::DEMSolver::*)( + const std::vector&, const std::vector&, const std::shared_ptr&)>( + &deme::DEMSolver::AddBCPlane), + "Add an analytical plane to the simulation.") + .def("Track", (&deme::DEMSolver::PythonTrack), + "Create a DEMTracker to allow direct control/modification/query to this external object/batch of " + "clumps/triangle mesh object.") + .def("Track", + static_cast (deme::DEMSolver::*)( + const std::shared_ptr&)>(&deme::DEMSolver::Track), + "Create a single tracker that tracks all member owners in a combined-instances batch.", + py::arg("combined_inst")) + .def("AddWavefrontMeshObject", + static_cast (deme::DEMSolver::*)( + const std::string&, const std::shared_ptr&, bool, bool)>( + &deme::DEMSolver::AddWavefrontMeshObject), + "Load a mesh-represented object.", py::arg("filename"), py::arg("mat"), py::arg("load_normals") = true, + py::arg("load_uv") = false) + .def("AddWavefrontMeshObject", + static_cast (deme::DEMSolver::*)(deme::DEMMeshConnected&)>( + &deme::DEMSolver::AddWavefrontMeshObject), + "Load a mesh-represented object.") + .def("AddWavefrontMeshObject", + static_cast (deme::DEMSolver::*)( + const std::string& filename, bool, bool)>(&deme::DEMSolver::AddWavefrontMeshObject), + "Load a mesh-represented object.", py::arg("filename"), py::arg("load_normals") = true, + py::arg("load_uv") = false) + .def("AddShellMesh", + static_cast (deme::DEMSolver::*)(deme::DEMMesh&, float)>( + &deme::DEMSolver::AddShellMesh), + "Load a finite-thickness shell mesh from a user-constructed mesh object.", py::arg("mesh"), + py::arg("shell_thickness")) + .def("AddWavefrontShellObject", + static_cast (deme::DEMSolver::*)( + const std::string&, const std::shared_ptr&, float, bool, bool)>( + &deme::DEMSolver::AddWavefrontShellObject), + "Load a finite-thickness shell mesh from a mesh file.", py::arg("filename"), py::arg("mat"), + py::arg("shell_thickness"), py::arg("load_normals") = true, py::arg("load_uv") = false) + .def("AddWavefrontShellObject", + static_cast (deme::DEMSolver::*)(const std::string&, float, bool, bool)>( + &deme::DEMSolver::AddWavefrontShellObject), + "Load a finite-thickness shell mesh from a mesh file without assigning material immediately.", + py::arg("filename"), py::arg("shell_thickness"), py::arg("load_normals") = true, + py::arg("load_uv") = false) + .def("LoadClumpType", + static_cast (deme::DEMSolver::*)( + float, const std::vector&, const std::string, const std::shared_ptr&)>( + &deme::DEMSolver::LoadClumpType), + "Load a clump type into the API-level cache") + .def("LoadClumpType", + static_cast (deme::DEMSolver::*)( + float mass, const std::vector&, const std::vector&, + const std::vector>&, const std::shared_ptr&)>( + &deme::DEMSolver::LoadClumpType), + "Load a clump type into the API-level cache") + .def("LoadClumpType", + static_cast (deme::DEMSolver::*)(deme::DEMClumpTemplate&)>( + &deme::DEMSolver::LoadClumpType), + "Load a clump type into the API-level cache") + .def("LoadClumpType", + static_cast (deme::DEMSolver::*)( + float, const std::vector&, const std::string, + const std::vector>&)>(&deme::DEMSolver::LoadClumpType), + "Load a clump type into the API-level cache") + .def("LoadClumpType", + static_cast (deme::DEMSolver::*)( + float, const std::vector&, const std::string, const std::shared_ptr&)>( + &deme::DEMSolver::LoadClumpType), + "Load a clump type into the API-level cache") + + .def("GetOwnerContactClumps", &deme::DEMSolver::GetOwnerContactClumps, + "Get the clumps that are in contact with this owner as a vector.") + .def("GetOwnerPosition", &deme::DEMSolver::GetOwnerPosition, "Get position of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetClumpPositionsHandover", &deme::DEMSolver::GetClumpPositionsHandover, + "Get all clump-owner center positions ordered by owner ID.") + .def("GetOwnerAngVel", &deme::DEMSolver::GetOwnerAngVel, "Get angular velocity of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerOriQ", &deme::DEMSolver::GetOwnerOriQ, "Get quaternion of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerVelocity", &deme::DEMSolver::GetOwnerVelocity, "Get velocity of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerAcc", &deme::DEMSolver::GetOwnerAcc, "Get the acceleration of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerAngAcc", &deme::DEMSolver::GetOwnerAngAcc, + "Get the angular acceleration of n consecutive owners.", py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerFamily", &deme::DEMSolver::GetOwnerFamily, "Get the family number of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("RequestContactUpdate", &deme::DEMSolver::RequestContactUpdate, + "Request an immediate contact detection update.") + .def("SetTrianglePVTrackingOwners", &deme::DEMSolver::SetTrianglePVTrackingOwners, + "Enable per-triangle P/V/PxV tracking for the specified mesh owners.") + .def("DisableTrianglePVTracking", &deme::DEMSolver::DisableTrianglePVTracking, + "Disable per-triangle P/V/PxV tracking and clear tracking state.") + .def("GetTrackedOwnerTrianglePV", + [](deme::DEMSolver& solver, deme::bodyID_t ownerID, bool reset_window) { + std::vector avgP; + std::vector avgV; + std::vector avgPV; + bool ok = solver.GetTrackedOwnerTrianglePV(ownerID, avgP, avgV, avgPV, reset_window); + return py::make_tuple(ok, avgP, avgV, avgPV); + }, + "Get frame-window averaged per-triangle P, V and P*V for one tracked owner.", py::arg("ownerID"), + py::arg("reset_window") = true) + .def("EnableMeshWearModel", &deme::DEMSolver::EnableMeshWearModel, + "Enable a mesh wear model driven by per-triangle P*V.", py::arg("ownerID"), py::arg("wear_rate"), + py::arg("update_interval"), py::arg("start_time") = 0.0, py::arg("end_time") = -1.0, + py::arg("normal_sign") = -1.0f) + .def("DisableMeshWearModel", &deme::DEMSolver::DisableMeshWearModel, + "Disable mesh wear model for one owner.") + .def("DisableAllMeshWearModels", &deme::DEMSolver::DisableAllMeshWearModels, + "Disable all active mesh wear models.") + .def("FlushMeshWearModels", &deme::DEMSolver::FlushMeshWearModels, + "Force-apply any pending wear deformation immediately.") + .def("GetOwnerMass", &deme::DEMSolver::GetOwnerMass, "Get the mass of n consecutive owners.", + py::arg("ownerID"), py::arg("n") = 1) + .def("GetOwnerMOI", &deme::DEMSolver::GetOwnerMOI, + "Get the moment of inertia (in principal axis frame) of n consecutive owners.", py::arg("ownerID"), + py::arg("n") = 1) + + .def("SetOwnerPosition", &deme::DEMSolver::SetOwnerPosition, + "Set position of consecutive owners starting from ownerID, based on input position vector. N (the size of " + "the input vector) elements will be modified.") + .def("SetOwnerAngVel", &deme::DEMSolver::SetOwnerAngVel, + "Set angular velocity of consecutive owners starting from ownerID, based on input angular velocity " + "vector. N (the size of the input vector) elements will be modified.") + .def("SetOwnerVelocity", &deme::DEMSolver::SetOwnerVelocity, + "Set velocity of consecutive owners starting from ownerID, based on input velocity vector. N (the size of " + "the input vector) elements will be modified.") + .def("SetOwnerOriQ", &deme::DEMSolver::SetOwnerOriQ, + "Set quaternion of consecutive owners starting from ownerID, based on input quaternion vector. N (the " + "size of the input vector) elements will be modified.") + .def("SetOwnerFamily", &deme::DEMSolver::SetOwnerFamily, "Set the family number of consecutive owners.", + py::arg("ownerID"), py::arg("fam"), py::arg("n") = 1) + + .def("SetTriNodeRelPos", &deme::DEMSolver::SetTriNodeRelPos, + "Rewrite the relative positions of the flattened triangle soup.") + .def("UpdateTriNodeRelPos", &deme::DEMSolver::UpdateTriNodeRelPos, + "Update the relative positions of the flattened triangle soup.") + .def("GetCachedMesh", &deme::DEMSolver::GetCachedMesh, "Get a handle for the mesh this tracker is tracking.") + .def("GetMeshNodesGlobal", &deme::DEMSolver::GetMeshNodesGlobal, + "Get the current locations of all the nodes in the mesh being tracked.") + + .def("GetClumpContacts", + static_cast> (deme::DEMSolver::*)() const>( + &deme::DEMSolver::GetClumpContacts), + "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " + "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") + .def("GetClumpContacts", + static_cast> (deme::DEMSolver::*)( + const std::set&) const>(&deme::DEMSolver::GetClumpContacts), + "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " + "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to " + "include only these families in the output.") + .def("GetClumpContacts", + static_cast> (deme::DEMSolver::*)( + std::vector>&) const>(&deme::DEMSolver::GetClumpContacts), + "Get all clump--clump contact ID pairs in the simulation system. Note all GetContact-like methods reports " + "potential contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") + + .def("GetContacts", + static_cast> (deme::DEMSolver::*)() const>( + &deme::DEMSolver::GetContacts), + "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " + "contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") + .def("GetContacts", + static_cast> (deme::DEMSolver::*)( + const std::set&) const>(&deme::DEMSolver::GetContacts), + "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " + "contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does. Use the argument to " + "include only these families in the output.") + .def("GetContacts", + static_cast> (deme::DEMSolver::*)( + std::vector>&) const>(&deme::DEMSolver::GetContacts), + "Get all contact ID pairs in the simulation system. Note all GetContact-like methods reports potential " + "contacts (not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.") + + .def("GetContactDetailedInfo", &deme::DEMSolver::GetContactDetailedInfo, + "Get all contact pairs' detailed information (actual content based on the setting with " + "SetContactOutputContent; default are owner IDs, contact point location, contact force, and associated " + "wildcard values) in the simulation system. Note all GetContact-like methods reports potential contacts " + "(not necessarily confirmed contacts), meaning they are similar to what " + "WriteContactFileIncludingPotentialPairs does, not what WriteContactFile does.", + py::arg("force_thres") = -1.0) + + .def("GetHostMemUsageDynamic", &deme::DEMSolver::GetHostMemUsageDynamic, + "Get the host memory usage (in bytes) on dT.") + .def("GetDeviceMemUsageDynamic", &deme::DEMSolver::GetDeviceMemUsageDynamic, + "Get the device memory usage (in bytes) on dT.") + .def("GetHostMemUsageKinematic", &deme::DEMSolver::GetHostMemUsageKinematic, + "Get the host memory usage (in bytes) on kT.") + .def("GetDeviceMemUsageKinematic", &deme::DEMSolver::GetDeviceMemUsageKinematic, + "Get the device memory usage (in bytes) on kT.") + .def("ShowMemStats", &deme::DEMSolver::ShowMemStats, "Print the current memory usage in pretty format.") + + .def("AddClumps", + static_cast (deme::DEMSolver::*)(deme::DEMClumpBatch&)>( + &deme::DEMSolver::AddClumps), + "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " + "location means the location of the clumps' CoM coordinates in the global frame.") + .def("AddClumps", + static_cast (deme::DEMSolver::*)( + std::shared_ptr&, const std::vector>&)>( + &deme::DEMSolver::AddClumps), + "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " + "location means the location of the clumps' CoM coordinates in the global frame.") + .def("AddClumps", + static_cast (deme::DEMSolver::*)( + const std::vector>&, const std::vector>&)>( + &deme::DEMSolver::AddClumps), + "Load input clumps (topology types and initial locations) on a per-pair basis. Note that the initial " + "location means the location of the clumps' CoM coordinates in the global frame.") + + .def("SetFamilyFixed", &deme::DEMSolver::SetFamilyFixed, "Mark all entities in this family to be fixed.") + + .def("SetFamilyPrescribedAngVel", + static_cast( + &deme::DEMSolver::SetFamilyPrescribedAngVel), + "Set the prescribed angular velocity to all entities in a family. If dictate is set to true, then this " + "family will not be influenced by the force exerted from other simulation entites (both linear and " + "rotational motions). If false, only specified components (that is, not specified with 'none') will not " + "be influenced by the force exerted from other simulation entites.", + py::arg("ID"), py::arg("velX"), py::arg("velY"), py::arg("velZ"), py::arg("dictate") = true, + py::arg("pre") = "none") + .def("SetFamilyPrescribedAngVel", + static_cast(&deme::DEMSolver::SetFamilyPrescribedAngVel), + "Let the angular velocities of all entites in this family always keep `as is', and not influenced by the " + "force exerted from other simulation entites.") + .def("SetFamilyPrescribedAngVelX", &deme::DEMSolver::SetFamilyPrescribedAngVelX, + "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + .def("SetFamilyPrescribedAngVelY", &deme::DEMSolver::SetFamilyPrescribedAngVelY, + "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + .def("SetFamilyPrescribedAngVelZ", &deme::DEMSolver::SetFamilyPrescribedAngVelZ, + "Let the X component of the angular velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + + .def("SetFamilyPrescribedLinVel", + static_cast( + &deme::DEMSolver::SetFamilyPrescribedLinVel), + "Set the prescribed linear velocity to all entities in a family. If dictate is set to true, then this " + "family will not be influenced by the force exerted from other simulation entites (both linear and " + "rotational motions). If false, only specified components (that is, not specified with 'none') will not " + "be influenced by the force exerted from other simulation entites.", + py::arg("ID"), py::arg("velX"), py::arg("velY"), py::arg("velZ"), py::arg("dictate") = true, + py::arg("pre") = "none") + .def("SetFamilyPrescribedLinVel", + static_cast(&deme::DEMSolver::SetFamilyPrescribedLinVel), + "Let the linear velocities of all entites in this family always keep `as is', and not influenced by the " + "force exerted from other simulation entites.") + .def("SetFamilyPrescribedLinVelX", &deme::DEMSolver::SetFamilyPrescribedLinVelX, + "Let the X component of the linear velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + .def("SetFamilyPrescribedLinVelY", &deme::DEMSolver::SetFamilyPrescribedLinVelY, + "Let the Y component of the linear velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + .def("SetFamilyPrescribedLinVelZ", &deme::DEMSolver::SetFamilyPrescribedLinVelZ, + "Let the Z component of the linear velocities of all entites in this family always keep `as is', and not " + "influenced by the force exerted from other simulation entites.") + + .def("SetFamilyPrescribedPosition", + static_cast( + &deme::DEMSolver::SetFamilyPrescribedPosition), + "Keep the positions of all entites in this family to remain exactly the user-specified values.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("dictate") = true, + py::arg("pre") = "none") + .def("SetFamilyPrescribedPosition", + static_cast(&deme::DEMSolver::SetFamilyPrescribedPosition), + "Keep the positions of all entites in this family to remain as is.") + .def("SetFamilyPrescribedPositionX", &deme::DEMSolver::SetFamilyPrescribedPositionX, + "Let the X component of the linear positions of all entites in this family always keep `as is'.") + .def("SetFamilyPrescribedPositionY", &deme::DEMSolver::SetFamilyPrescribedPositionY, + "Let the Y component of the linear positions of all entites in this family always keep `as is'.") + .def("SetFamilyPrescribedPositionZ", &deme::DEMSolver::SetFamilyPrescribedPositionZ, + "Let the Z component of the linear positions of all entites in this family always keep `as is'.") + + .def("SetFamilyPrescribedQuaternion", + static_cast( + &deme::DEMSolver::SetFamilyPrescribedQuaternion), + "Keep the orientation quaternions of all entites in this family to remain exactly the user-specified " + "values.", + py::arg("ID"), py::arg("q_formula"), py::arg("dictate") = true) + .def("SetFamilyPrescribedQuaternion", + static_cast(&deme::DEMSolver::SetFamilyPrescribedQuaternion), + "Let the orientation quaternions of all entites in this family always keep `as is'.") + + .def("AddFamilyPrescribedAcc", &deme::DEMSolver::AddFamilyPrescribedAcc, + "The entities in this family will always experienced an extra acceleration defined using this method.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") + .def("AddFamilyPrescribedAngAcc", &deme::DEMSolver::AddFamilyPrescribedAngAcc, + "The entities in this family will always experienced an extra angular acceleration defined using this " + "method.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") + + .def("CorrectFamilyLinVel", &deme::DEMSolver::CorrectFamilyLinVel, + "The entities in this family will always experience an added linear-velocity correction defined using " + "this method. At the same time, they are still subject to the `simulation physics'.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") + .def("CorrectFamilyAngVel", &deme::DEMSolver::CorrectFamilyAngVel, + "The entities in this family will always experience an added angular-velocity correction defined using " + "this method. At the same time, they are still subject to the `simulation physics'.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") + + .def("CorrectFamilyPosition", &deme::DEMSolver::CorrectFamilyPosition, + "The entities in this family will always experience an added positional correction defined using this " + "method. At the same time, they are still subject to the `simulation physics'.", + py::arg("ID"), py::arg("X"), py::arg("Y"), py::arg("Z"), py::arg("pre") = "none") + .def("CorrectFamilyQuaternion", &deme::DEMSolver::CorrectFamilyQuaternion, + "The entities in this family will always experience an added quaternion correction defined using this " + "method. At the same time, they are still subject to the `simulation physics'.", + py::arg("ID"), py::arg("q_formula")) + + .def("SetContactWildcards", &deme::DEMSolver::SetContactWildcards, + "Set the names for the extra quantities that will be associated with each contact pair.") + .def("SetOwnerWildcards", &deme::DEMSolver::SetOwnerWildcards, + "Set the names for the extra quantities that will be associated with each owner.") + + .def("SetFamilyContactWildcardValueEither", &deme::DEMSolver::SetFamilyContactWildcardValueEither, + "Change the value of contact wildcards to val if either of the contact geometries is in family N.") + .def("SetFamilyContactWildcardValueBoth", &deme::DEMSolver::SetFamilyContactWildcardValueBoth, + "Change the value of contact wildcards to val if both of the contact geometries are in family N.") + .def("SetFamilyContactWildcardValue", &deme::DEMSolver::SetFamilyContactWildcardValue, + "Change the value of contact wildcards to val if one of the contact geometry is in family N1, and the " + "other is in N2.") + .def("SetContactWildcardValue", &deme::DEMSolver::SetContactWildcardValue, + "Change the value of contact wildcards to val. Apply to all simulation bodies that are present.") + + .def("MarkFamilyPersistentContactEither", &deme::DEMSolver::MarkFamilyPersistentContactEither, + "Make it so that for any currently-existing contact, if one of its contact geometries is in family N, " + "then this contact will never be removed.") + .def("MarkFamilyPersistentContactBoth", &deme::DEMSolver::MarkFamilyPersistentContactBoth, + "Make it so that for any currently-existing contact, if both of its contact geometries are in family N, " + "then this contact will never be removed.") + .def("MarkFamilyPersistentContact", &deme::DEMSolver::MarkFamilyPersistentContact, + "Make it so that if for any currently-existing contact, if its two contact geometries are in family N1 " + "and N2 respectively, this contact will never be removed.") + .def("MarkPersistentContact", &deme::DEMSolver::MarkPersistentContact, + "Make it so that all currently-existing contacts in this simulation will never be removed.") + + .def("RemoveFamilyPersistentContactEither", &deme::DEMSolver::RemoveFamilyPersistentContactEither, + "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactEither.") + .def("RemoveFamilyPersistentContactBoth", &deme::DEMSolver::RemoveFamilyPersistentContactBoth, + "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContactBoth.") + .def("RemoveFamilyPersistentContact", &deme::DEMSolver::RemoveFamilyPersistentContact, + "Cancel contact persistence qualification. Work like the inverse of MarkFamilyPersistentContact.") + .def("RemovePersistentContact", &deme::DEMSolver::RemovePersistentContact, + "Cancel contact persistence qualification. Work like the inverse of MarkPersistentContact.") + + .def("GetOwnerContactForces", + static_cast& ownerIDs, + std::vector& points, std::vector& forces)>( + &deme::DEMSolver::GetOwnerContactForces), + "Get all contact forces that concern a list of owners.") + .def("GetOwnerContactForces", + static_cast& ownerIDs, std::vector& points, std::vector& forces, + std::vector& torques, bool torque_in_local)>(&deme::DEMSolver::GetOwnerContactForces), + "Get all contact forces and torque that concern a list of owners.", py::arg("ownerIDs"), py::arg("points"), + py::arg("forces"), py::arg("torques"), py::arg("torque_in_local") = false) + + .def("SetOwnerWildcardValue", + static_cast& vals)>( + &deme::DEMSolver::SetOwnerWildcardValue), + "Set the wildcard values of some owners using a list.", py::arg("ownerIDs"), py::arg("name"), + py::arg("vals")) + .def("SetOwnerWildcardValue", + static_cast(&deme::DEMSolver::SetOwnerWildcardValue), + "Set the wildcard values of some owners using a list.", py::arg("ownerIDs"), py::arg("name"), + py::arg("val"), py::arg("n") = 1) + + .def("GetOwnerWildcardValue", + static_cast (deme::DEMSolver::*)(deme::bodyID_t ownerID, const std::string& name, + deme::bodyID_t n)>( + &deme::DEMSolver::GetOwnerWildcardValue), + "Get the owner wildcard's values of some owners.", py::arg("ownerID"), py::arg("name"), py::arg("n") = 1) + .def("GetAllOwnerWildcardValue", &deme::DEMSolver::GetAllOwnerWildcardValue, + "Get the owner wildcard's values of all entities.") + .def("GetFamilyOwnerWildcardValue", &deme::DEMSolver::GetFamilyOwnerWildcardValue, + "Get the owner wildcard's values of all entities in family N.") + + .def("SyncMemoryTransfer", &deme::DEMSolver::SyncMemoryTransfer, + "If the user used async-ed version of a tracker's get/set methods (to get a speed boost in many piecemeal " + "accesses of a long array), this method should be called to mark the end of to-host transactions.") + + .def("SetFamilyOwnerWildcardValue", + static_cast& vals)>( + &deme::DEMSolver::SetFamilyOwnerWildcardValue), + "Modify the owner wildcard's values of all entities in family N.") + .def("SetFamilyOwnerWildcardValue", + static_cast( + &deme::DEMSolver::SetFamilyOwnerWildcardValue), + "Modify the owner wildcard's values of all entities in family N.") + + .def("SetFamilyClumpMaterial", &deme::DEMSolver::SetFamilyClumpMaterial, + "Set all clumps in this family to have this material.") + .def("SetFamilyMeshMaterial", &deme::DEMSolver::SetFamilyMeshMaterial, + "Set all meshes in this family to have this material.") + .def("SetFamilyExtraMargin", &deme::DEMSolver::SetFamilyExtraMargin, + "Add an extra contact margin to entities in a family so they are registered as potential contact pairs " + "earlier.") + + .def("ClearCache", &deme::DEMSolver::ClearCache, + "Remove host-side cached vectors (so you can re-define them, and then re-initialize system).") + + .def("CreateInspector", + static_cast (deme::DEMSolver::*)(const std::string&)>( + &deme::DEMSolver::CreateInspector), + "Create a inspector object that can help query some statistical info of the clumps in the simulation.", + py::arg("quantity") = "clump_max_z") + .def("GetNumClumps", &deme::DEMSolver::GetNumClumps, + "Return the number of clumps that are currently in the simulation. Must be used after initialization.") + .def("GetNumOwners", &deme::DEMSolver::GetNumOwners, + "Return the total number of owners (clumps + meshes + analytical objects) that are currently in the " + "simulation. Must be used after initialization.") + .def("CreateInspector", + static_cast (deme::DEMSolver::*)( + const std::string&, const std::string&)>(&deme::DEMSolver::CreateInspector), + "Create a inspector object that can help query some statistical info of the clumps in the simulation.") + .def("SetInitTimeStep", &deme::DEMSolver::SetInitTimeStep, + "Legacy name for SetTimeStepSize, kept for backward compatibility.", + py::arg("ts")) + .def("SetTimeStepSize", &deme::DEMSolver::SetTimeStepSize, + "Set the timestep in seconds before or after initialization. A post-initialization call takes effect on " + "the next step and must be made while the solver is synchronized.", + py::arg("ts")) + .def("SetGravitationalAcceleration", + static_cast&)>( + &deme::DEMSolver::SetGravitationalAcceleration), + "Set the global gravitational acceleration as ``[x, y, z]`` in length/time^2 units. This takes effect " + "immediately after initialization when called while the solver is synchronized.", + py::arg("acc")) + .def("SetMaxVelocity", &deme::DEMSolver::SetMaxVelocity, + "Set the maximum expected particle velocity. The solver will not use a velocity larger than this for " + "determining the margin thickness, and velocity larger than this will be considered a system anomaly. " + "After initialization, call UpdateSimParams() for this change to take effect in the workers.") + .def("SetErrorOutVelocity", &deme::DEMSolver::SetErrorOutVelocity, + "Set the velocity which when exceeded, the solver errors out. A huge number can be used to discourage " + "this error type. Defaulted to 5e4.") + .def("SetExpandSafetyMultiplier", &deme::DEMSolver::SetExpandSafetyMultiplier, + "Assign a multiplier to our estimated maximum system velocity, when deriving the thinckness of the " + "contact `safety' margin. After initialization, call UpdateSimParams() for this change to take effect in " + "the workers.") + .def("Initialize", &deme::DEMSolver::Initialize, + "Finalize cached setup data, allocate runtime state, and prepare JIT CUDA kernels. Call once after " + "materials, geometry, particles, and solver options have been configured. ``dry_run=True`` performs " + "initialization without starting normal dynamics.", + py::arg("dry_run") = false) + .def("WriteSphereFile", + static_cast(&deme::DEMSolver::WriteSphereFile), + "Write clumps as component spheres using the selected CSV or VTK output format.", + py::arg("outfilename")) + .def("WriteAnalyticalFile", + static_cast( + &deme::DEMSolver::WriteAnalyticalFile), + "Write analytical boundary surfaces as VTK clipped to the user-specified domain.", + py::arg("outfilename"), py::arg("circumferential_resolution") = 32) + .def("WriteMeshFile", + static_cast(&deme::DEMSolver::WriteMeshFile), + "Write the current status of all meshes to a file.") + .def("WriteClumpFile", + static_cast( + &deme::DEMSolver::WriteClumpFile), + "Write the current status of clumps to a file.", py::arg("outfilename"), py::arg("accuracy") = 10) + .def( + "WriteContactFile", + static_cast(&deme::DEMSolver::WriteContactFile), + "Write all contact pairs to a file. Forces smaller than threshold will not be outputted.", + py::arg("outfilename"), py::arg("force_thres") = 1e-30) + .def("WriteContactFileIncludingPotentialPairs", + static_cast( + &deme::DEMSolver::WriteContactFileIncludingPotentialPairs), + "Write all contact pairs kT-supplied to a file, thus including the potential ones (those are not yet in " + "contact, or recently used to be in contact).", + py::arg("outfilename")) + .def("WaitForPendingOutput", &deme::DEMSolver::WaitForPendingOutput, + "Wait for any in-flight async output to finish.") + + // Maybe add checkpoint-reading methods here... + .def("DoDynamics", &deme::DEMSolver::DoDynamics, + "Advance by ``duration`` seconds without forcing the dynamic and kinematic workers to synchronize at " + "the call boundary. This supports short calls and co-simulation; synchronize before immediately reading " + "state.", + py::arg("duration")) + .def("DoStepDynamics", &deme::DEMSolver::DoStepDynamics, + "Advance one current timestep without forcing a final dynamic/kinematic worker synchronization.") + .def("DoDynamicsThenSync", &deme::DEMSolver::DoDynamicsThenSync, + "Advance by ``duration`` seconds, then synchronize the dynamic and kinematic workers. Use this before " + "immediately querying or modifying runtime state.", + py::arg("duration")) + .def("UpdateSimParams", &deme::DEMSolver::UpdateSimParams, + "Transfer the cached sim params to the workers. Used for sim environment modification after system " + "initialization.") + .def("ChangeFamily", &deme::DEMSolver::ChangeFamily, + "Change all entities with family number ID_from to have a new number ID_to, when the condition defined by " + "the string is satisfied by the entities in question. This should be called before initialization, and " + "will be baked into the solver, so the conditions will be checked and changes applied every time step.") + .def("ChangeFamilyWhen", &deme::DEMSolver::ChangeFamilyWhen, + "Change all entities with family number ID_from to have a new number ID_to, when the condition defined by " + "the string is satisfied by the entities in question. This should be called before initialization, and " + "will be baked into the solver, so the conditions will be checked and changes applied every time step.") + .def("ChangeClumpFamily", &deme::DEMSolver::ChangeClumpFamily, + "Change the family number for the clumps in a box region to the specified value.", py::arg("fam_num"), + py::arg("X") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), + py::arg("Y") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), + py::arg("Z") = std::pair(-DEME_HUGE_FLOAT, DEME_HUGE_FLOAT), + py::arg("orig_fam") = std::set()) + + .def("ShowThreadCollaborationStats", &deme::DEMSolver::ShowThreadCollaborationStats, + "Show the collaboration stats between dT and kT. This is more useful for tweaking the number of time " + "steps that dT should be allowed to be in advance of kT.") + .def("ShowTimingStats", &deme::DEMSolver::ShowTimingStats, + "Show the wall time and percentages of wall time spend on various solver tasks.") + .def("SetGPUTimersEnabled", &deme::DEMSolver::SetGPUTimersEnabled, + "Enable or disable event-based GPU timing.", py::arg("enabled")) + .def("GetGPUTimersEnabled", &deme::DEMSolver::GetGPUTimersEnabled, + "Return whether event-based GPU timing is enabled.") + .def("PrintKinematicScratchSpaceUsage", &deme::DEMSolver::PrintKinematicScratchSpaceUsage, + "Print kT's scratch space usage. This is a debug method.") + .def("ShowAnomalies", &deme::DEMSolver::ShowAnomalies, + "Show potential anomalies that may have been there in the simulation, then clear the anomaly log.") + .def("ClearThreadCollaborationStats", &deme::DEMSolver::ClearThreadCollaborationStats, + "Reset the collaboration stats between dT and kT back to the initial value (0). You should call this if " + "you want to start over and re-inspect the stats of the new run; otherwise, it is generally not needed, " + "you can go ahead and destroy DEMSolver.") + .def("ClearTimingStats", &deme::DEMSolver::ClearTimingStats, + "Reset the recordings of the wall time and percentages of wall time spend on various solver tasks.") + .def("PurgeFamily", &deme::DEMSolver::PurgeFamily, + "Remove all entities in a family from runtime arrays to reclaim memory.", py::arg("family_num")) + .def("ReleaseFlattenedArrays", &deme::DEMSolver::ReleaseFlattenedArrays, + "Release setup-time flattened arrays used for preprocessing and worker transfer.") + .def("GetWhetherForceCollectInKernel", &deme::DEMSolver::GetWhetherForceCollectInKernel, + "Return whether the solver is currently reducing force in the force calculation kernel.") + .def("AddOwnerNextStepAcc", &deme::DEMSolver::AddOwnerNextStepAcc, + "Add an extra acceleration to a owner for the next time step.") + .def("AddOwnerNextStepAngAcc", &deme::DEMSolver::AddOwnerNextStepAngAcc, + " Add an extra angular acceleration to a owner for the next time step.") + .def("DisableContactBetweenFamilies", &deme::DEMSolver::DisableContactBetweenFamilies, + "Instruct the solver that the 2 input families should not have contacts (a.k.a. ignored, if such a pair " + "is encountered in contact detection). These 2 families can be the same (which means no contact within " + "members of that family).") + .def("EnableContactBetweenFamilies", &deme::DEMSolver::EnableContactBetweenFamilies, + "Re-enable contact between 2 families after the system is initialized.") + .def("DisableFamilyOutput", &deme::DEMSolver::DisableFamilyOutput, + "Prevent entites associated with this family to be outputted to files."); + +#ifdef DEME_HAS_VISUALIZER + py::class_(obj, "VisualizerColor", "RGBA color used by DEMVisualizer.") + .def(py::init(), py::arg("r") = 255, py::arg("g") = 255, + py::arg("b") = 255, py::arg("a") = 255) + .def_readwrite("r", &deme::DEMVisualizerColor::r) + .def_readwrite("g", &deme::DEMVisualizerColor::g) + .def_readwrite("b", &deme::DEMVisualizerColor::b) + .def_readwrite("a", &deme::DEMVisualizerColor::a); + + py::enum_(obj, "VisualizerColorMode") + .value("FAMILY", deme::DEMVisualizerColorMode::FAMILY) + .value("HEIGHT", deme::DEMVisualizerColorMode::HEIGHT) + .value("SPEED", deme::DEMVisualizerColorMode::SPEED); + + py::class_( + obj, "DEMVisualizer", + "Step-wise interactive viewer. Render() draws the current solver state without advancing the simulation.") + .def(py::init(), py::arg("solver"), py::keep_alive<1, 2>(), + "Create a visualizer and keep its solver alive for the lifetime of the viewer.") + .def("Initialize", &deme::DEMVisualizer::Initialize, "Create the visualization window and graphics resources.") + .def("Run", &deme::DEMVisualizer::Run, "Return true while the initialized window remains open.") + .def("Render", &deme::DEMVisualizer::Render, + "Synchronously capture the current solver state and draw one frame without advancing the solver.") + .def("Close", &deme::DEMVisualizer::Close, "Close the visualization window.") + .def("SetWindowSize", &deme::DEMVisualizer::SetWindowSize, py::arg("width"), py::arg("height")) + .def("SetWindowTitle", &deme::DEMVisualizer::SetWindowTitle, py::arg("title")) + .def("SetTargetFPS", &deme::DEMVisualizer::SetTargetFPS, py::arg("fps")) + .def("SetCameraPosition", &deme::DEMVisualizer::SetCameraPosition, py::arg("position")) + .def("SetCameraTarget", &deme::DEMVisualizer::SetCameraTarget, py::arg("target")) + .def("SetBackgroundColor", &deme::DEMVisualizer::SetBackgroundColor, py::arg("color")) + .def("SetFamilyColor", &deme::DEMVisualizer::SetFamilyColor, py::arg("family"), py::arg("color")) + .def("SetRenderSpheres", &deme::DEMVisualizer::SetRenderSpheres, py::arg("render") = true, + "Enable or disable component-sphere rendering; enabled by default.") + .def("SetRenderTriangles", &deme::DEMVisualizer::SetRenderTriangles, py::arg("render") = true, + "Enable or disable triangle rendering; enabled by default.") + .def("IsRenderingSpheres", &deme::DEMVisualizer::IsRenderingSpheres) + .def("IsRenderingTriangles", &deme::DEMVisualizer::IsRenderingTriangles) + .def("ShouldStep", &deme::DEMVisualizer::ShouldStep) + .def("SetPaused", &deme::DEMVisualizer::SetPaused, py::arg("paused")) + .def("IsPaused", &deme::DEMVisualizer::IsPaused) + .def("RequestStep", &deme::DEMVisualizer::RequestStep) + .def("SetFamilyVisible", &deme::DEMVisualizer::SetFamilyVisible, py::arg("family"), py::arg("visible")) + .def("SetColorMode", &deme::DEMVisualizer::SetColorMode, py::arg("mode")) + .def("FrameAll", &deme::DEMVisualizer::FrameAll) + .def("FrameSelected", &deme::DEMVisualizer::FrameSelected) + .def("PickAt", &deme::DEMVisualizer::PickAt, py::arg("x"), py::arg("y")) + .def("GetSelectedOwner", &deme::DEMVisualizer::GetSelectedOwner) + .def("GetSelectedGeometryID", &deme::DEMVisualizer::GetSelectedGeometryID) + .def("IsSelectedSphere", &deme::DEMVisualizer::IsSelectedSphere) + .def("RequestScreenshot", &deme::DEMVisualizer::RequestScreenshot, py::arg("path")); +#endif + + py::class_>( + obj, "DEMMaterial", "Material property name/value pairs used by a contact force model.") + .def(py::init&>(), py::arg("properties"), + "Create material properties. Normally use ``DEMSolver.LoadMaterial`` so the material is registered.") + .def_readwrite("mat_prop", &deme::DEMMaterial::mat_prop) + .def_readwrite("load_order", &deme::DEMMaterial::load_order); + + py::class_>( + obj, "DEMClumpTemplate", "Reusable rigid clump topology composed of one or more sphere components.") + .def(py::init<>(), "Create an empty clump template.") + .def("Mass", &deme::DEMClumpTemplate::GetMass, "Return the clump mass.") + .def("MOI", &deme::DEMClumpTemplate::GetMOI, + "Return principal moments of inertia in the clump-local principal frame.") + .def("SetMass", &deme::DEMClumpTemplate::SetMass, "Set the clump mass.", py::arg("mass")) + .def("SetMOI", + static_cast&)>(&deme::DEMClumpTemplate::SetMOI), + "Set the three principal moments of inertia in the clump-local frame.", py::arg("MOI")) + .def("SetMaterial", + static_cast>&)>( + &deme::DEMClumpTemplate::SetMaterial), + "Assign one material per sphere component. The sequence length must equal the component count.", + py::arg("materials")) + .def("SetMaterial", + static_cast& input)>( + &deme::DEMClumpTemplate::SetMaterial), + "Assign the same material to every sphere component.", py::arg("material")) + .def("SetVolume", &deme::DEMClumpTemplate::SetVolume, + "Set the clump volume, which is required for void-ratio queries.", py::arg("volume")) + .def("ReadComponentFromFile", &deme::DEMClumpTemplate::ReadComponentFromFile, + "Load sphere-component positions and radii from a delimited file.", py::arg("filename"), + py::arg("x_id") = "x", py::arg("y_id") = "y", py::arg("z_id") = "z", py::arg("r_id") = "r") + .def("InformCentroidPrincipal", + static_cast&, const std::vector&)>( + &deme::DEMClumpTemplate::InformCentroidPrincipal), + "Re-express component positions about the supplied center of mass and principal orientation.", + py::arg("center"), py::arg("orientation")) + .def("Move", + static_cast&, const std::vector&)>( + &deme::DEMClumpTemplate::Move), + "Apply a translation and quaternion rotation to the component coordinates.", py::arg("translation"), + py::arg("quaternion")) + .def("Scale", &deme::DEMClumpTemplate::Scale, "Uniformly scale component radii and relative positions.", + py::arg("factor")) + .def("AssignName", &deme::DEMClumpTemplate::AssignName, "Assign the clump type name written to output files.", + py::arg("name")); + + py::class_>( + obj, "DEMCombinedTemplate", + "Reusable rigid grouping of clump or mesh templates with fixed member-relative transforms.") + .def(py::init<>(), "Create an empty combined-owner template."); + + py::class_>( + obj, "DEMCombinedInstances", "A batch of instantiated combined owners and their member-owner metadata.") + .def(py::init<>(), "Create an empty combined-instance batch.") + .def("GetNumOwners", &deme::DEMCombinedInstances::GetNumOwners, + "Get total number of member owners in this combined batch.") + .def("AddOwnerWildcard", + static_cast&)>( + &deme::DEMCombinedInstances::AddOwnerWildcard), + "Add an owner wildcard to all member owners with per-owner values.", py::arg("name"), py::arg("vals")) + .def("AddOwnerWildcard", + static_cast( + &deme::DEMCombinedInstances::AddOwnerWildcard), + "Add an owner wildcard to all member owners with a uniform value.", py::arg("name"), py::arg("val")); + + py::class_>( + obj, "DEMClumpBatch", "Cached setup data for a batch of clump instances.") + .def(py::init(), py::arg("count"), "Create setup storage for ``count`` clumps.") + .def("GetNumClumps", &deme::DEMClumpBatch::GetNumClumps, "Return the number of clumps in this batch.") + .def("GetNumSpheres", &deme::DEMClumpBatch::GetNumSpheres, + "Return the total number of component spheres represented by this batch.") + .def("SetTypes", + static_cast>&)>( + &deme::DEMClumpBatch::SetTypes), + "Assign one clump template per instance. The sequence length must equal the batch size.", py::arg("types")) + .def("SetTypes", + static_cast&)>( + &deme::DEMClumpBatch::SetTypes), + "Assign the same clump template to every instance.", py::arg("type")) + .def("SetType", &deme::DEMClumpBatch::SetType, "Assign the same clump template to every instance.", + py::arg("type")) + .def("SetVel", + static_cast>&)>( + &deme::DEMClumpBatch::SetVel), + "Set one global initial linear velocity vector per clump.", py::arg("velocities")) + .def("SetVel", + static_cast&)>(&deme::DEMClumpBatch::SetVel), + "Set the same global initial linear velocity for every clump.", py::arg("velocity")) + // .def("SetAngVel", + // static_cast&)>(&deme::DEMClumpBatch::SetAngVel)) + .def("SetFamilies", + static_cast&)>( + &deme::DEMClumpBatch::SetFamilies), + "Set one family number per clump. Family behavior is configured on the solver.", py::arg("families")) + .def("SetFamilies", static_cast(&deme::DEMClumpBatch::SetFamilies), + "Set the same family number for every clump.", py::arg("family")) + .def("SetFamily", &deme::DEMClumpBatch::SetFamily, "Set the same family number for every clump.", + py::arg("family")) + .def("SetExistingContacts", &deme::DEMClumpBatch::SetExistingContacts, + "Supply sphere-sphere contact owner-ID pairs relative to this batch when restoring a simulation.", + py::arg("pairs")) + .def("SetExistingContactWildcards", &deme::DEMClumpBatch::SetExistingContactWildcards, + "Supply named restart wildcard arrays after ``SetExistingContacts``. Every array must match the " + "pre-existing contact count.", + py::arg("wildcards")) + .def("AddExistingContactWildcard", &deme::DEMClumpBatch::AddExistingContactWildcard, + "Add one named restart contact-wildcard array after ``SetExistingContacts``.", py::arg("name"), + py::arg("values")) + .def("SetOwnerWildcards", &deme::DEMClumpBatch::SetOwnerWildcards, + "Set named initial owner-wildcard arrays. Every array must match the clump count.", py::arg("wildcards")) + .def("AddOwnerWildcard", + static_cast&)>( + &deme::DEMClumpBatch::AddOwnerWildcard), + "Add one initial owner wildcard with one value per clump.", py::arg("name"), py::arg("values")) + .def("AddOwnerWildcard", + static_cast( + &deme::DEMClumpBatch::AddOwnerWildcard), + "Add one initial owner wildcard with the same value for every clump.", py::arg("name"), py::arg("value")) + .def("GetNumContacts", &deme::DEMClumpBatch::GetNumContacts, + "Return the number of pre-existing contacts supplied for restart initialization."); + + py::class_>( + obj, "DEMExternObj", "Analytical external owner composed of planes, cylinders, and related primitives.") + .def(py::init<>(), "Create an empty analytical external object.") + .def("Mass", &deme::DEMExternObj::GetMass, "Return the object's mass.") + .def("MOI", &deme::DEMExternObj::GetMOI, "Return principal moments of inertia in the object-local frame.") + .def("SetFamily", &deme::DEMExternObj::SetFamily, "Set the contact family number before initialization.", + py::arg("family")) + .def("SetMass", &deme::DEMExternObj::SetMass, "Set the object mass.", py::arg("mass")) + .def("SetMOI", + static_cast&)>(&deme::DEMExternObj::SetMOI), + "Set the three principal moments of inertia in the object-local frame.", py::arg("MOI")) + .def("SetInitQuat", + static_cast&)>(&deme::DEMExternObj::SetInitQuat), + "Set the initial local-to-global orientation quaternion ``(x, y, z, w)`` before initialization.", + py::arg("quaternion")) + .def("SetInitPos", + static_cast&)>(&deme::DEMExternObj::SetInitPos), + "Set the initial global center-of-mass position before initialization.", py::arg("position")) + .def("AddPlane", + static_cast&, const std::vector&, + const std::shared_ptr&)>( + &deme::DEMExternObj::AddPlane), + "Add an infinite plane specified by a point and normal in the object's local frame.", py::arg("point"), + py::arg("normal"), py::arg("material")) + //.def("AddPlate", static_cast&, const float, + const std::shared_ptr&, + const deme::objNormal_t)>(&deme::DEMExternObj::AddZCylinder), + "Add an infinite cylinder aligned with the object-local Z axis.", py::arg("pos"), py::arg("rad"), + py::arg("material"), py::arg("normal") = deme::ENTITY_NORMAL_INWARD) + .def("AddCylinder", + static_cast&, const std::vector&, const float, + const std::shared_ptr&, + const deme::objNormal_t)>(&deme::DEMExternObj::AddCylinder), + "Add an infinite cylinder along a user-specified object-local axis.", py::arg("pos"), py::arg("axis"), + py::arg("rad"), py::arg("material"), py::arg("normal") = deme::ENTITY_NORMAL_INWARD) + .def("AddCone", + static_cast&, const std::vector&, const float, + const std::shared_ptr&, + const deme::objNormal_t)>(&deme::DEMExternObj::AddCone), + "Add an analytical cone side extending indefinitely from its tip.", py::arg("tip"), py::arg("axis"), + py::arg("slope"), py::arg("material"), py::arg("normal") = deme::ENTITY_NORMAL_INWARD) + .def("AddConeSegment", + static_cast&, const std::vector&, const float, const float, const float, + const std::shared_ptr&, const deme::objNormal_t)>( + &deme::DEMExternObj::AddConeSegment), + "Add an analytical cone or frustum side clipped between two axial distances.", py::arg("tip"), + py::arg("axis"), py::arg("slope"), py::arg("hmin"), py::arg("hmax"), py::arg("material"), + py::arg("normal") = deme::ENTITY_NORMAL_INWARD) + .def_readwrite("types", &deme::DEMExternObj::types) + .def_readwrite("materials", &deme::DEMExternObj::materials) + .def_readwrite("family_code", &deme::DEMExternObj::family_code) + .def_readwrite("init_pos", &deme::DEMExternObj::init_pos) + .def_readwrite("init_oriQ", &deme::DEMExternObj::init_oriQ) + .def_readwrite("mass", &deme::DEMExternObj::mass) + .def_readwrite("MOI", &deme::DEMExternObj::MOI) + .def_readwrite("load_order", &deme::DEMExternObj::load_order) + .def_readwrite("entity_params", &deme::DEMExternObj::entity_params); + + py::class_>( + obj, "DEMMesh", "Connected triangle-mesh template or external mesh owner.") + .def(py::init<>(), "Create an empty mesh.") + .def(py::init(), py::arg("filename"), "Load a mesh from a Wavefront OBJ file.") + .def(py::init&>(), py::arg("filename"), + py::arg("material"), "Load a Wavefront OBJ mesh and assign one material to every triangle.") + .def("Mass", &deme::DEMMeshConnected::GetMass, "Return the mesh owner's mass.") + .def("MOI", &deme::DEMMeshConnected::GetMOI, "Return principal moments of inertia in the mesh-local frame.") + .def("Clear", &deme::DEMMeshConnected::Clear, "Remove all mesh geometry and cached attributes.") + .def("LoadWavefrontMesh", &deme::DEMMeshConnected::LoadWavefrontMesh, + "Load a triangle mesh saved as a Wavefront .obj file", py::arg("input_file"), + py::arg("load_normals") = true, py::arg("load_uv") = false) + .def("WriteWavefront", &deme::DEMMeshConnected::WriteWavefront, + "Write the mesh geometry to a Wavefront OBJ file.", py::arg("output_file")) + .def("GetNumTriangles", &deme::DEMMeshConnected::GetNumTriangles, + "Get the number of triangles already added to this mesh") + .def("GetNumNodes", &deme::DEMMeshConnected::GetNumNodes, "Get the number of nodes in the mesh") + .def("UseNormals", &deme::DEMMeshConnected::UseNormals, + "Instruct that when the mesh is initialized into the system, it will re-order the nodes of each triangle " + "so that the normals derived from right-hand-rule are the same as the normals in the mesh file", + py::arg("use") = true) + .def("SetShellThickness", &deme::DEMMeshConnected::SetShellThickness, + "Treat this mesh as a finite-thickness shell.", py::arg("thickness")) + .def("DisableShell", &deme::DEMMeshConnected::DisableShell, "Disable finite-thickness shell mode.") + .def("IsShell", &deme::DEMMeshConnected::IsShell, "Return whether this mesh is configured as a shell.") + .def("GetShellThickness", &deme::DEMMeshConnected::GetShellThickness, "Return the full shell thickness.") + .def("GetTriangle", &deme::DEMMeshConnected::GetTriangle, "Access the n-th triangle in mesh") + .def("SetPatchIDs", &deme::DEMMeshConnected::SetPatchIDs, "Set one patch ID per triangle.", + py::arg("patch_ids")) + .def("SetEachTriangleAsPatch", &deme::DEMMeshConnected::SetEachTriangleAsPatch, + "Assign every triangle to its own patch using consecutive patch IDs.") + .def("GetPatchIDs", &deme::DEMMeshConnected::GetPatchIDs, "Get one patch ID per triangle.") + .def("GetNumPatches", &deme::DEMMeshConnected::GetNumPatches, "Get the number of mesh patches.") + .def("ArePatchesExplicitlySet", &deme::DEMMeshConnected::ArePatchesExplicitlySet, + "Return whether patch IDs were explicitly supplied or computed.") + .def("SplitIntoConvexPatches", + static_cast( + &deme::DEMMeshConnected::SplitIntoConvexPatches), + "Split the mesh into connected angle-threshold patches.", py::arg("hard_angle_deg") = 30.0f) + .def("SetMass", &deme::DEMMeshConnected::SetMass, "Set the mesh owner's mass.", py::arg("mass")) + .def("SetMOI", + static_cast&)>(&deme::DEMMeshConnected::SetMOI), + "Set the three principal moments of inertia in the mesh-local frame.", py::arg("MOI")) + .def("SetFamily", &deme::DEMMeshConnected::SetFamily, "Set the contact family number before initialization.", + py::arg("family")) + .def("SetMaterial", + static_cast&)>( + &deme::DEMMeshConnected::SetMaterial), + "Assign the same material to every mesh triangle.", py::arg("material")) + .def("SetMaterial", + static_cast>&)>( + &deme::DEMMeshConnected::SetMaterial), + "Assign one material per triangle. The sequence length must equal the triangle count.", + py::arg("materials")) + .def("SetInitQuat", + static_cast&)>( + &deme::DEMMeshConnected::SetInitQuat), + "Set the initial local-to-global orientation quaternion ``(x, y, z, w)``.", py::arg("quaternion")) + .def("SetInitPos", + static_cast&)>( + &deme::DEMMeshConnected::SetInitPos), + "Set the initial global center-of-mass position.", py::arg("position")) + .def("InformCentroidPrincipal", + static_cast&, const std::vector&)>( + &deme::DEMMeshConnected::InformCentroidPrincipal), + "Re-express mesh vertices about the supplied center of mass and principal orientation.", py::arg("center"), + py::arg("orientation")) + .def("Move", + static_cast&, const std::vector&)>( + &deme::DEMMeshConnected::Move), + "Apply a translation and quaternion rotation to mesh vertices.", py::arg("translation"), + py::arg("quaternion")) + .def("Mirror", + static_cast&, const std::vector&)>( + &deme::DEMMeshConnected::Mirror), + "Mirror vertices about a plane specified by a point and normal.", py::arg("point"), py::arg("normal")) + .def("Scale", static_cast(&deme::DEMMeshConnected::Scale), + "Uniformly scale mesh vertices.", py::arg("factor")) + .def("Scale", + static_cast&)>(&deme::DEMMeshConnected::Scale), + "Scale mesh vertices independently along X, Y, and Z.", py::arg("factors")) + .def("ClearWildcards", &deme::DEMMeshConnected::ClearWildcards, "Remove cached triangle wildcard values.") + .def("GetCoordsVertices", &deme::DEMMeshConnected::GetCoordsVerticesAsVectorOfVectors, + "Return mesh vertex coordinates as ``[[x, y, z], ...]``.") + //.def("GetCoordsUV", &deme::DEMMeshConnected::GetCoordsUVPython) + //.def("GetCoordsColors", &deme::DEMMeshConnected::GetCoordsColorsPython) + .def("GetIndicesVertexes", &deme::DEMMeshConnected::GetIndicesVertexesAsVectorOfVectors, + "Return triangle vertex-index triplets."); + // .def("GetIndicesNormals", &deme::DEMMeshConnected::GetIndicesNormalsPython) + // .def("GetIndicesUV", &deme::DEMMeshConnected::GetIndicesUVPython) + // .def("GetIndicesColors", &deme::DEMMeshConnected::GetIndicesColorsPython); + + //// TODO: Insert readwrite functions to access all public class objects! + + py::enum_(obj, "TIME_INTEGRATOR") + .value("FORWARD_EULER", deme::TIME_INTEGRATOR::FORWARD_EULER) + .value("CENTERED_DIFFERENCE", deme::TIME_INTEGRATOR::CENTERED_DIFFERENCE) + .value("EXTENDED_TAYLOR", deme::TIME_INTEGRATOR::EXTENDED_TAYLOR) + .value("CHUNG", deme::TIME_INTEGRATOR::CHUNG) + .export_values(); + + py::enum_(obj, "OWNER_TYPE") + .value("CLUMP", deme::OWNER_TYPE::CLUMP) + .value("ANALYTICAL", deme::OWNER_TYPE::ANALYTICAL) + .value("MESH", deme::OWNER_TYPE::MESH) + .export_values(); + + py::enum_(obj, "FORCE_MODEL") + .value("HERTZIAN", deme::FORCE_MODEL::HERTZIAN) + .value("HERTZIAN_FRICTIONLES", deme::FORCE_MODEL::HERTZIAN_FRICTIONLESS) + .value("CUSTOM", deme::FORCE_MODEL::CUSTOM) + .export_values(); + + py::enum_(obj, "OUTPUT_CONTENT") + .value("XYZ", deme::OUTPUT_CONTENT::XYZ) + .value("QUAT", deme::OUTPUT_CONTENT::QUAT) + .value("ABSV", deme::OUTPUT_CONTENT::ABSV) + .value("VEL", deme::OUTPUT_CONTENT::VEL) + .value("ANG_VEL", deme::OUTPUT_CONTENT::ANG_VEL) + .value("ABS_ACC", deme::OUTPUT_CONTENT::ABS_ACC) + .value("ACC", deme::OUTPUT_CONTENT::ACC) + .value("ANG_ACC", deme::OUTPUT_CONTENT::ANG_ACC) + .value("FAMILY", deme::OUTPUT_CONTENT::FAMILY) + .value("MAT", deme::OUTPUT_CONTENT::MAT) + .value("OWNER_WILDCARD", deme::OUTPUT_CONTENT::OWNER_WILDCARD) + .export_values(); + + py::enum_(obj, "MESH_OUTPUT_CONTENT") + .value("XYZ", deme::MESH_OUTPUT_CONTENT::XYZ) + .value("QUAT", deme::MESH_OUTPUT_CONTENT::QUAT) + .value("ABSV", deme::MESH_OUTPUT_CONTENT::ABSV) + .value("VEL", deme::MESH_OUTPUT_CONTENT::VEL) + .value("ANG_VEL", deme::MESH_OUTPUT_CONTENT::ANG_VEL) + .value("ABS_ACC", deme::MESH_OUTPUT_CONTENT::ABS_ACC) + .value("ACC", deme::MESH_OUTPUT_CONTENT::ACC) + .value("ANG_ACC", deme::MESH_OUTPUT_CONTENT::ANG_ACC) + .value("FAMILY", deme::MESH_OUTPUT_CONTENT::FAMILY) + .value("MAT", deme::MESH_OUTPUT_CONTENT::MAT) + .value("OWNER_WILDCARD", deme::MESH_OUTPUT_CONTENT::OWNER_WILDCARD) + .value("GEO_WILDCARD", deme::MESH_OUTPUT_CONTENT::GEO_WILDCARD) + .value("OWNER", deme::MESH_OUTPUT_CONTENT::OWNER) + .value("MESH_ID", deme::MESH_OUTPUT_CONTENT::MESH_ID) + .value("TRI_ID", deme::MESH_OUTPUT_CONTENT::TRI_ID) + .value("PATCH_ID", deme::MESH_OUTPUT_CONTENT::PATCH_ID); + + py::enum_(obj, "OUTPUT_FORMAT") + .value("CSV", deme::OUTPUT_FORMAT::CSV) + .value("BINARY", deme::OUTPUT_FORMAT::BINARY) + .value("VTK", deme::OUTPUT_FORMAT::VTK) + .export_values(); + + py::enum_(obj, "MESH_FORMAT") + .value("VTK", deme::MESH_FORMAT::VTK) + .value("OBJ", deme::MESH_FORMAT::OBJ) + .value("STL", deme::MESH_FORMAT::STL) + .value("PLY", deme::MESH_FORMAT::PLY) + .export_values(); + + py::enum_(obj, "SPATIAL_DIR") + .value("X", deme::SPATIAL_DIR::X) + .value("Y", deme::SPATIAL_DIR::Y) + .value("Z", deme::SPATIAL_DIR::Z) + .value("NONE", deme::SPATIAL_DIR::NONE) + .export_values(); + + py::enum_(obj, "CNT_OUTPUT_CONTENT") + .value("CNT_TYPE", deme::CNT_OUTPUT_CONTENT::CNT_TYPE) + .value("FORCE", deme::CNT_OUTPUT_CONTENT::FORCE) + .value("POINT", deme::CNT_OUTPUT_CONTENT::CNT_POINT) + .value("COMPONENT", deme::CNT_OUTPUT_CONTENT::COMPONENT) + .value("NORMAL", deme::CNT_OUTPUT_CONTENT::NORMAL) + .value("TORQUE", deme::CNT_OUTPUT_CONTENT::TORQUE) + .value("CNT_WILDCARD", deme::CNT_OUTPUT_CONTENT::CNT_WILDCARD) + .value("OWNER", deme::CNT_OUTPUT_CONTENT::OWNER) + .value("GEO_ID", deme::CNT_OUTPUT_CONTENT::GEO_ID) + .value("NICKNAME", deme::CNT_OUTPUT_CONTENT::NICKNAME) + .export_values(); +} diff --git a/src/DEM/python/deme/__init__.py b/src/DEM/python/deme/__init__.py index f8e0496d..2e1d0366 100644 --- a/src/DEM/python/deme/__init__.py +++ b/src/DEM/python/deme/__init__.py @@ -1,4 +1,16 @@ -"""Python interface for DEM-Engine 2.""" +"""Python interface for DEM-Engine. + +The implementation lives in a private extension module so this package can +grow Python-level helpers without changing the public import path. +""" + +from ._cuda import configure_cuda as _configure_cuda + +_cuda_include_paths = _configure_cuda() +from ._deme import _set_cuda_include_paths + +_set_cuda_include_paths(_cuda_include_paths) +del _configure_cuda, _cuda_include_paths, _set_cuda_include_paths from ._deme import * # noqa: F401,F403 from ._deme import __version__ diff --git a/src/DEM/python/deme/_cuda.py b/src/DEM/python/deme/_cuda.py new file mode 100644 index 00000000..8360a762 --- /dev/null +++ b/src/DEM/python/deme/_cuda.py @@ -0,0 +1,44 @@ +"""Locate optional NVIDIA wheels before importing the native extension.""" + +import ctypes +from importlib.metadata import PackageNotFoundError, distribution +from pathlib import Path +import sys + + +# Retain handles for the lifetime of the extension, including NVRTC's builtins. +_libraries = [] + + +def configure_cuda(): + """Use a complete pip CUDA installation, or leave system CUDA discovery alone. + + Preloading by absolute path is necessary: changing LD_LIBRARY_PATH inside a + running Python process does not update the Linux dynamic loader's search. + """ + if sys.platform != "linux": + return [] + components = ("cuda_runtime", "cuda_nvrtc", "cuda_nvcc", "cuda_cccl", "curand") + roots = {} + for component in components: + try: + package = distribution("nvidia-" + component.replace("_", "-") + "-cu12") + except PackageNotFoundError: + return [] + roots[component] = Path(package.locate_file("nvidia/" + component)) + + includes = [root / "include" for root in roots.values()] + # CCCL wheels can put their header trees one level below include/. + includes.append(roots["cuda_cccl"] / "include" / "cccl") + for component, pattern in ( + ("cuda_runtime", "libcudart.so.12"), + ("cuda_nvrtc", "libnvrtc-builtins.so.*"), + ("cuda_nvrtc", "libnvrtc.so.12"), + ): + paths = sorted((roots[component] / "lib").glob(pattern)) + if not paths: + raise ImportError("Incomplete NVIDIA CUDA wheels: missing " + pattern) + _libraries.append(ctypes.CDLL(str(paths[0]), mode=ctypes.RTLD_GLOBAL)) + + # Return paths to the private binding; do not alter the process environment. + return [str(path) for path in includes if path.is_dir()] diff --git a/src/DEM/utils/AnalyticalOutput.cpp b/src/DEM/utils/AnalyticalOutput.cpp new file mode 100644 index 00000000..4a2b36bf --- /dev/null +++ b/src/DEM/utils/AnalyticalOutput.cpp @@ -0,0 +1,239 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "AnalyticalOutput.hpp" + +#include +#include +#include +#include + +#include "HostSideHelpers.hpp" + +namespace deme { +namespace { + +struct OutputTriangle { + float3 a; + float3 b; + float3 c; + bodyID_t owner; + family_t family; + objType_t type; + float normal_sign; +}; + +float component(const float3& point, unsigned int axis) { + return axis == 0 ? point.x : (axis == 1 ? point.y : point.z); +} + +// Clip a convex polygon against one axis-aligned half-space. Applying this to all six box faces performs +// Sutherland-Hodgman clipping and preserves the analytical surface rather than adding box-cap geometry. +std::vector clipHalfSpace(const std::vector& polygon, + unsigned int axis, + float bound, + bool keep_greater) { + std::vector clipped; + if (polygon.empty()) { + return clipped; + } + auto inside = [axis, bound, keep_greater](const float3& point) { + return keep_greater ? component(point, axis) >= bound : component(point, axis) <= bound; + }; + for (size_t i = 0; i < polygon.size(); i++) { + const float3 current = polygon[i]; + const float3 previous = polygon[(i + polygon.size() - 1) % polygon.size()]; + const bool current_inside = inside(current); + const bool previous_inside = inside(previous); + if (current_inside != previous_inside) { + const float denominator = component(current, axis) - component(previous, axis); + const float fraction = (bound - component(previous, axis)) / denominator; + clipped.push_back(previous + (current - previous) * fraction); + } + if (current_inside) { + clipped.push_back(current); + } + } + return clipped; +} + +void appendClippedTriangle(std::vector& output, + const float3& a, + const float3& b, + const float3& c, + const AnalyticalOutputComponent& component_data, + const float3& domain_min, + const float3& domain_max) { + std::vector polygon = {a, b, c}; + for (unsigned int axis = 0; axis < 3; axis++) { + polygon = clipHalfSpace(polygon, axis, component(domain_min, axis), true); + polygon = clipHalfSpace(polygon, axis, component(domain_max, axis), false); + } + for (size_t i = 1; i + 1 < polygon.size(); i++) { + if (length(cross(polygon[i] - polygon[0], polygon[i + 1] - polygon[0])) > DEME_TINY_FLOAT) { + output.push_back({polygon[0], polygon[i], polygon[i + 1], component_data.owner, component_data.family, + component_data.type, component_data.normal_sign}); + } + } +} + +std::array boxCorners(const float3& lower, const float3& upper) { + return {make_float3(lower.x, lower.y, lower.z), make_float3(upper.x, lower.y, lower.z), + make_float3(lower.x, upper.y, lower.z), make_float3(upper.x, upper.y, lower.z), + make_float3(lower.x, lower.y, upper.z), make_float3(upper.x, lower.y, upper.z), + make_float3(lower.x, upper.y, upper.z), make_float3(upper.x, upper.y, upper.z)}; +} + +void orthogonalBasis(const float3& axis, float3& first, float3& second) { + const float3 reference = std::abs(axis.z) < 0.9f ? make_float3(0, 0, 1) : make_float3(1, 0, 0); + first = normalize(cross(axis, reference)); + second = cross(axis, first); +} + +// Intersect an infinite plane with all box edges, sort the resulting convex polygon in-plane, then triangulate it. +void appendPlane(std::vector& output, + const AnalyticalOutputComponent& plane, + const float3& domain_min, + const float3& domain_max) { + static constexpr unsigned int edges[12][2] = {{0, 1}, {0, 2}, {1, 3}, {2, 3}, {4, 5}, {4, 6}, + {5, 7}, {6, 7}, {0, 4}, {1, 5}, {2, 6}, {3, 7}}; + const auto corners = boxCorners(domain_min, domain_max); + const float3 normal = normalize(plane.axis); + std::vector polygon; + for (const auto& edge : edges) { + const float da = dot(corners[edge[0]] - plane.position, normal); + const float db = dot(corners[edge[1]] - plane.position, normal); + if ((da > 0) == (db > 0) && std::abs(da) > DEME_TINY_FLOAT && std::abs(db) > DEME_TINY_FLOAT) { + continue; + } + const float denominator = da - db; + if (std::abs(denominator) <= DEME_TINY_FLOAT) { + continue; + } + const float fraction = da / denominator; + if (fraction >= 0 && fraction <= 1) { + const float3 point = corners[edge[0]] + (corners[edge[1]] - corners[edge[0]]) * fraction; + const bool duplicate = std::any_of(polygon.begin(), polygon.end(), [&point](const float3& existing) { + return length(point - existing) <= DEME_TINY_FLOAT; + }); + if (!duplicate) { + polygon.push_back(point); + } + } + } + if (polygon.size() < 3) { + return; + } + float3 center = make_float3(0); + for (const auto& point : polygon) { + center += point; + } + center /= static_cast(polygon.size()); + float3 basis_u, basis_v; + orthogonalBasis(normal, basis_u, basis_v); + std::sort(polygon.begin(), polygon.end(), [¢er, &basis_u, &basis_v](const float3& a, const float3& b) { + const float3 da = a - center; + const float3 db = b - center; + return std::atan2(dot(da, basis_v), dot(da, basis_u)) < std::atan2(dot(db, basis_v), dot(db, basis_u)); + }); + for (size_t i = 1; i + 1 < polygon.size(); i++) { + output.push_back( + {polygon[0], polygon[i], polygon[i + 1], plane.owner, plane.family, plane.type, plane.normal_sign}); + } +} + +void appendRoundSurface(std::vector& output, + const AnalyticalOutputComponent& surface, + const float3& domain_min, + const float3& domain_max, + unsigned int resolution) { + const float3 axis = normalize(surface.axis); + float projection_min = DEME_HUGE_FLOAT; + float projection_max = -DEME_HUGE_FLOAT; + for (const auto& corner : boxCorners(domain_min, domain_max)) { + const float projection = dot(corner - surface.position, axis); + projection_min = std::min(projection_min, projection); + projection_max = std::max(projection_max, projection); + } + + const bool is_cylinder = surface.type == ANAL_OBJ_TYPE_CYL_INF; + float axial_min = projection_min; + float axial_max = projection_max; + if (!is_cylinder) { + axial_min = std::max(axial_min, surface.size_2); + axial_max = std::min(axial_max, surface.size_3); + } + if (axial_max <= axial_min) { + return; + } + + float3 basis_u, basis_v; + orthogonalBasis(axis, basis_u, basis_v); + for (unsigned int i = 0; i < resolution; i++) { + const float angle_0 = 2.f * static_cast(M_PI) * i / resolution; + const float angle_1 = 2.f * static_cast(M_PI) * (i + 1) / resolution; + const float3 radial_0 = basis_u * std::cos(angle_0) + basis_v * std::sin(angle_0); + const float3 radial_1 = basis_u * std::cos(angle_1) + basis_v * std::sin(angle_1); + const float radius_min = is_cylinder ? surface.size_1 : surface.size_1 * axial_min; + const float radius_max = is_cylinder ? surface.size_1 : surface.size_1 * axial_max; + const float3 p00 = surface.position + axis * axial_min + radial_0 * radius_min; + const float3 p01 = surface.position + axis * axial_min + radial_1 * radius_min; + const float3 p10 = surface.position + axis * axial_max + radial_0 * radius_max; + const float3 p11 = surface.position + axis * axial_max + radial_1 * radius_max; + appendClippedTriangle(output, p00, p10, p11, surface, domain_min, domain_max); + appendClippedTriangle(output, p00, p11, p01, surface, domain_min, domain_max); + } +} + +} // namespace + +void writeAnalyticalAsVtk(std::ofstream& file, + const std::vector& components, + const float3 domain_min, + const float3 domain_max, + unsigned int circumferential_resolution) { + std::vector triangles; + for (const auto& component : components) { + switch (component.type) { + case ANAL_OBJ_TYPE_PLANE: + appendPlane(triangles, component, domain_min, domain_max); + break; + case ANAL_OBJ_TYPE_CYL_INF: + case ANAL_OBJ_TYPE_CONE_INF: + case ANAL_OBJ_TYPE_CONE: + appendRoundSurface(triangles, component, domain_min, domain_max, circumferential_resolution); + break; + default: + break; + } + } + + std::ostringstream out; + out << "# vtk DataFile Version 2.0\nDEME analytical boundaries\nASCII\nDATASET POLYDATA\n"; + out << "POINTS " << 3 * triangles.size() << " float\n"; + for (const auto& triangle : triangles) { + out << triangle.a.x << " " << triangle.a.y << " " << triangle.a.z << "\n"; + out << triangle.b.x << " " << triangle.b.y << " " << triangle.b.z << "\n"; + out << triangle.c.x << " " << triangle.c.y << " " << triangle.c.z << "\n"; + } + out << "POLYGONS " << triangles.size() << " " << 4 * triangles.size() << "\n"; + for (size_t i = 0; i < triangles.size(); i++) { + out << "3 " << 3 * i << " " << 3 * i + 1 << " " << 3 * i + 2 << "\n"; + } + out << "CELL_DATA " << triangles.size() << "\n"; + auto write_scalar = [&out, &triangles](const std::string& name, const auto& value) { + out << "SCALARS " << name << " int 1\nLOOKUP_TABLE default\n"; + for (const auto& triangle : triangles) { + out << value(triangle) << "\n"; + } + }; + write_scalar("owner", [](const OutputTriangle& triangle) { return triangle.owner; }); + write_scalar("family", [](const OutputTriangle& triangle) { return +triangle.family; }); + write_scalar("component_type", [](const OutputTriangle& triangle) { return +triangle.type; }); + write_scalar("normal_sign", [](const OutputTriangle& triangle) { return static_cast(triangle.normal_sign); }); + file << out.str(); +} + +} // namespace deme diff --git a/src/DEM/utils/AnalyticalOutput.hpp b/src/DEM/utils/AnalyticalOutput.hpp new file mode 100644 index 00000000..40ee0575 --- /dev/null +++ b/src/DEM/utils/AnalyticalOutput.hpp @@ -0,0 +1,36 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include +#include + +#include "../Defines.h" +#include "../VariableTypes.h" + +namespace deme { + +/// Host-side description of one analytical component in the current world frame. +struct AnalyticalOutputComponent { + objType_t type; + float3 position; + float3 axis; + float size_1; + float size_2; + float size_3; + float normal_sign; + bodyID_t owner; + family_t family; +}; + +/// Tessellate analytical surfaces, clip them to the user domain, and write directly displayable VTK PolyData. +void writeAnalyticalAsVtk(std::ofstream& file, + const std::vector& components, + const float3 domain_min, + const float3 domain_max, + unsigned int circumferential_resolution); + +} // namespace deme diff --git a/src/DEM/utils/ClumpMassProperties.cpp b/src/DEM/utils/ClumpMassProperties.cpp new file mode 100644 index 00000000..209e35ae --- /dev/null +++ b/src/DEM/utils/ClumpMassProperties.cpp @@ -0,0 +1,193 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace deme { + +bool computeClumpUnionMassPropsApprox(const DEMClumpTemplate& clump, double& volume, float3& center, float3& inertia) { + volume = 0.0; + center = make_float3(0, 0, 0); + inertia = make_float3(0, 0, 0); + if (clump.nComp == 0 || clump.radii.size() != clump.nComp || clump.relPos.size() != clump.nComp) { + return false; + } + + float3 bb_min = make_float3(std::numeric_limits::infinity()); + float3 bb_max = make_float3(-std::numeric_limits::infinity()); + for (size_t i = 0; i < clump.nComp; i++) { + const float r = clump.radii[i]; + if (!(r > DEME_TINY_FLOAT) || !std::isfinite(r)) { + continue; + } + const float3 c = clump.relPos[i]; + bb_min.x = std::min(bb_min.x, c.x - r); + bb_min.y = std::min(bb_min.y, c.y - r); + bb_min.z = std::min(bb_min.z, c.z - r); + bb_max.x = std::max(bb_max.x, c.x + r); + bb_max.y = std::max(bb_max.y, c.y + r); + bb_max.z = std::max(bb_max.z, c.z + r); + } + + const double lx = static_cast(bb_max.x) - bb_min.x; + const double ly = static_cast(bb_max.y) - bb_min.y; + const double lz = static_cast(bb_max.z) - bb_min.z; + if (!(lx > 0.0) || !(ly > 0.0) || !(lz > 0.0) || !std::isfinite(lx) || !std::isfinite(ly) || !std::isfinite(lz)) { + return false; + } + + const double bbox_vol = lx * ly * lz; + + auto radical_inverse = [](uint64_t n, uint32_t base) { + double inv_base = 1.0 / static_cast(base); + double inv_bi = inv_base; + double val = 0.0; + while (n) { + const uint32_t d = static_cast(n % base); + val += static_cast(d) * inv_bi; + n /= base; + inv_bi *= inv_base; + } + return val; + }; + + struct RunningStat { + double mean = 0.0; + double m2 = 0.0; + size_t n = 0; + void add(double x) { + n++; + const double d = x - mean; + mean += d / static_cast(n); + const double d2 = x - mean; + m2 += d * d2; + } + double var() const { return (n > 1) ? (m2 / static_cast(n - 1)) : 0.0; } + }; + + // E[I], E[I x], E[I y], E[I z], E[I xx], E[I yy], E[I zz], E[I xy], E[I yz], E[I zx] + std::array stats; + constexpr double z99 = 2.576; // 99% confidence interval factor + constexpr double target_rel_err = 0.01; // 1% target relative error + constexpr size_t min_samples = 100000; + constexpr size_t max_samples = 4000000; + constexpr size_t check_stride = 10000; + + bool converged = false; + size_t n_total = 0; + for (size_t i = 0; i < max_samples; i++) { + const uint64_t idx = static_cast(i + 1); + const double u = radical_inverse(idx, 2); + const double v = radical_inverse(idx, 3); + const double w = radical_inverse(idx, 5); + const double x = static_cast(bb_min.x) + u * lx; + const double y = static_cast(bb_min.y) + v * ly; + const double z = static_cast(bb_min.z) + w * lz; + + bool inside = false; + for (size_t s = 0; s < clump.nComp; s++) { + const double dx = x - clump.relPos[s].x; + const double dy = y - clump.relPos[s].y; + const double dz = z - clump.relPos[s].z; + const double rr = static_cast(clump.radii[s]) * clump.radii[s]; + if (dx * dx + dy * dy + dz * dz <= rr) { + inside = true; + break; + } + } + + const double I = inside ? 1.0 : 0.0; + stats[0].add(I); + stats[1].add(I * x); + stats[2].add(I * y); + stats[3].add(I * z); + stats[4].add(I * x * x); + stats[5].add(I * y * y); + stats[6].add(I * z * z); + stats[7].add(I * x * y); + stats[8].add(I * y * z); + stats[9].add(I * z * x); + n_total++; + + if (n_total < min_samples || (n_total % check_stride) != 0) { + continue; + } + const double mean_I = stats[0].mean; + if (!(mean_I > 1e-14)) { + continue; + } + const double stderr_I = std::sqrt(std::max(0.0, stats[0].var()) / static_cast(n_total)); + const double rel_err_I = z99 * stderr_I / mean_I; + + const double m2_sum = stats[4].mean + stats[5].mean + stats[6].mean; + double rel_err_m2 = 0.0; + if (m2_sum > 1e-14) { + const double var_sum = + std::max(0.0, stats[4].var()) + std::max(0.0, stats[5].var()) + std::max(0.0, stats[6].var()); + const double stderr_sum = std::sqrt(var_sum / static_cast(n_total)); + rel_err_m2 = z99 * stderr_sum / m2_sum; + } + + if (rel_err_I <= target_rel_err && rel_err_m2 <= target_rel_err) { + converged = true; + break; + } + } + + const double inside_frac = stats[0].mean; + if (!(inside_frac > 0.0) || !std::isfinite(inside_frac)) { + return false; + } + volume = bbox_vol * inside_frac; + if (!(volume > 0.0) || !std::isfinite(volume)) { + return false; + } + + const double cx = stats[1].mean / inside_frac; + const double cy = stats[2].mean / inside_frac; + const double cz = stats[3].mean / inside_frac; + center = make_float3(static_cast(cx), static_cast(cy), static_cast(cz)); + + const double int_xx = bbox_vol * stats[4].mean; + const double int_yy = bbox_vol * stats[5].mean; + const double int_zz = bbox_vol * stats[6].mean; + const double int_xy = bbox_vol * stats[7].mean; + const double int_yz = bbox_vol * stats[8].mean; + const double int_zx = bbox_vol * stats[9].mean; + + double Ixx = int_yy + int_zz; + double Iyy = int_xx + int_zz; + double Izz = int_xx + int_yy; + double Ixy = -int_xy; + double Iyz = -int_yz; + double Izx = -int_zx; + + Ixx -= volume * (cy * cy + cz * cz); + Iyy -= volume * (cx * cx + cz * cz); + Izz -= volume * (cx * cx + cy * cy); + Ixy += volume * cx * cy; + Iyz += volume * cy * cz; + Izx += volume * cz * cx; + + inertia = make_float3(static_cast(Ixx), static_cast(Iyy), static_cast(Izz)); + if (!converged) { + DEME_WARNING( + "Clump union mass/MOI estimator hit sample cap (%zu) before 99%%-confidence 1%%-error target; using best " + "estimate.", + n_total); + } + return std::isfinite(Ixx) && std::isfinite(Iyy) && std::isfinite(Izz) && Ixx > 0.0 && Iyy > 0.0 && Izz > 0.0; +} + +} // namespace deme diff --git a/src/DEM/utils/ClumpMassProperties.hpp b/src/DEM/utils/ClumpMassProperties.hpp new file mode 100644 index 00000000..26f6932a --- /dev/null +++ b/src/DEM/utils/ClumpMassProperties.hpp @@ -0,0 +1,16 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include + +namespace deme { + +class DEMClumpTemplate; + +bool computeClumpUnionMassPropsApprox(const DEMClumpTemplate& clump, double& volume, float3& center, float3& inertia); + +} // namespace deme diff --git a/src/DEM/utils/CombinedOwnerUtils.cpp b/src/DEM/utils/CombinedOwnerUtils.cpp new file mode 100644 index 00000000..04fb172b --- /dev/null +++ b/src/DEM/utils/CombinedOwnerUtils.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include + +#include + +namespace deme { + +float3 rotateDiagonalMOIToFrame(const float3& diagonal_moi, const float4& q) { + float3 x_axis = make_float3(1, 0, 0); + float3 y_axis = make_float3(0, 1, 0); + float3 z_axis = make_float3(0, 0, 1); + applyOriQToVector3(x_axis, q); + applyOriQToVector3(y_axis, q); + applyOriQToVector3(z_axis, q); + + return make_float3(diagonal_moi.x * x_axis.x * x_axis.x + diagonal_moi.y * y_axis.x * y_axis.x + + diagonal_moi.z * z_axis.x * z_axis.x, + diagonal_moi.x * x_axis.y * x_axis.y + diagonal_moi.y * y_axis.y * y_axis.y + + diagonal_moi.z * z_axis.y * z_axis.y, + diagonal_moi.x * x_axis.z * x_axis.z + diagonal_moi.y * y_axis.z * y_axis.z + + diagonal_moi.z * z_axis.z * z_axis.z); +} + +float3 parallelAxisDiagonal(float mass, const float3& rel_pos) { + return mass * make_float3(rel_pos.y * rel_pos.y + rel_pos.z * rel_pos.z, + rel_pos.x * rel_pos.x + rel_pos.z * rel_pos.z, + rel_pos.x * rel_pos.x + rel_pos.y * rel_pos.y); +} + +} // namespace deme diff --git a/src/DEM/utils/CombinedOwnerUtils.hpp b/src/DEM/utils/CombinedOwnerUtils.hpp new file mode 100644 index 00000000..6d23afc1 --- /dev/null +++ b/src/DEM/utils/CombinedOwnerUtils.hpp @@ -0,0 +1,15 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include + +namespace deme { + +float3 rotateDiagonalMOIToFrame(const float3& diagonal_moi, const float4& q); +float3 parallelAxisDiagonal(float mass, const float3& rel_pos); + +} // namespace deme diff --git a/src/DEM/utils/DEMVisualizer.h b/src/DEM/utils/DEMVisualizer.h new file mode 100644 index 00000000..ea6060e4 --- /dev/null +++ b/src/DEM/utils/DEMVisualizer.h @@ -0,0 +1,109 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_VISUALIZER_H +#define DEME_VISUALIZER_H + +#include +#include +#include +#include + +#include + +#include "../VariableTypes.h" +#include "VisualizationData.h" + +namespace deme { + +class DEMSolver; + +/// RGBA color used by the visualizer without exposing its rendering backend in DEME's public API. +struct DEMVisualizerColor { + constexpr DEMVisualizerColor(std::uint8_t red = 255, + std::uint8_t green = 255, + std::uint8_t blue = 255, + std::uint8_t alpha = 255) + : r(red), g(green), b(blue), a(alpha) {} + + std::uint8_t r; + std::uint8_t g; + std::uint8_t b; + std::uint8_t a; +}; + +/// Owner-based scalar coloring; Height uses the owner's global Z coordinate, Speed uses linear velocity magnitude. +enum class DEMVisualizerColorMode { FAMILY, HEIGHT, SPEED }; + +/// Step-wise interactive viewer for the current state of a DEMSolver. +/// +/// The visualizer never advances the simulation. Each Render call synchronously captures and draws exactly one solver +/// state, allowing applications to choose their own simulation-to-render cadence. +class DEMVisualizer { + public: + explicit DEMVisualizer(DEMSolver& solver); + ~DEMVisualizer(); + + DEMVisualizer(const DEMVisualizer&) = delete; + DEMVisualizer& operator=(const DEMVisualizer&) = delete; + DEMVisualizer(DEMVisualizer&&) = delete; + DEMVisualizer& operator=(DEMVisualizer&&) = delete; + + /// Create the native window and graphics resources. Calling this more than once has no effect. + void Initialize(); + /// Return true while the initialized visualization window remains open. + bool Run() const; + /// Capture the current solver state and draw one frame. This does not advance the solver. + void Render(); + /// Release the window and graphics resources. Calling this more than once has no effect. + void Close(); + + void SetWindowSize(int width, int height); + void SetWindowTitle(const std::string& title); + void SetTargetFPS(int fps); + void SetCameraPosition(float3 position); + void SetCameraTarget(float3 target); + void SetBackgroundColor(DEMVisualizerColor color); + void SetFamilyColor(family_t family, DEMVisualizerColor color); + + /// Enable or disable component-sphere rendering. Enabled by default. + void SetRenderSpheres(bool render = true); + /// Enable or disable triangle rendering. Enabled by default. + void SetRenderTriangles(bool render = true); + bool IsRenderingSpheres() const; + bool IsRenderingTriangles() const; + + /// Simulation controls are requests: the application must call ShouldStep before advancing its solver. + /// ShouldStep consumes one pending step when paused. Render itself never blocks on pause or advances dynamics. + bool ShouldStep(); + void SetPaused(bool paused); + bool IsPaused() const; + void RequestStep(); + void SetFamilyVisible(family_t family, bool visible); + void SetColorMode(DEMVisualizerColorMode mode); + /// Fit visible geometry (or the selected owner) using the last captured frame. + void FrameAll(); + void FrameSelected(); + /// Pick at logical window coordinates using the last rendered frame. Background clears the selection. + void PickAt(int x, int y); + bodyID_t GetSelectedOwner() const; + /// Global sphere or triangle index, or SIZE_MAX when nothing is selected. + size_t GetSelectedGeometryID() const; + bool IsSelectedSphere() const; + /// Save the next rendered frame, including the inspection UI, to a PNG path. + void RequestScreenshot(const std::string& path); + + private: + // The solver archive supplies the public constructor. Callbacks keep a second copy of the static solver/JIT + // globals out of the shared graphics library, where ELF symbol interposition would otherwise double-destroy them. + DEMVisualizer(std::function scene, + std::function frame); + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace deme + +#endif diff --git a/src/DEM/utils/DynamicThreadHelpers.hpp b/src/DEM/utils/DynamicThreadHelpers.hpp new file mode 100644 index 00000000..ccb2f346 --- /dev/null +++ b/src/DEM/utils/DynamicThreadHelpers.hpp @@ -0,0 +1,128 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_DYNAMIC_THREAD_HELPERS_HPP +#define DEME_DYNAMIC_THREAD_HELPERS_HPP + +#include +#include +#include +#include + +#include +#include + +namespace deme { + +// dT-side future-drift tuner. +// +// The scheduler's correctness-sensitive rule is that dT consumes a kT result and immediately posts the next kT work +// order. This helper deliberately does not decide *when* to send. It only chooses the non-negative drift/margin command +// attached to the next work order, using the measured age of the contact set that just got replaced. +struct FutureDriftRegulator { + double covered_drift_ema = 0.0; + bool has_covered_drift_sample = false; + unsigned int last_covered_drift = 0; + bool force_non_decreasing_once = false; + unsigned int cache_size = 200; + unsigned int samples_since_reset = 0; + + void Clear() { + const unsigned int preserved_cache_size = cache_size; + *this = FutureDriftRegulator{}; + cache_size = preserved_cache_size; + } + + void SetCacheSize(unsigned int n) { cache_size = std::max(1u, n); } + + void RecordContactSetCoverage(unsigned int covered_drift) { + last_covered_drift = covered_drift; + const double sample = static_cast(covered_drift); + if (!has_covered_drift_sample || samples_since_reset >= cache_size) { + covered_drift_ema = sample; + has_covered_drift_sample = true; + samples_since_reset = 1; + return; + } + samples_since_reset++; + + // React faster when the actual required coverage grows than when it falls. Undershooting can make dT wait or + // miss contacts; overshooting mostly costs extra kT work, so we decay conservatively. + constexpr double EMA_UP = 0.35; + constexpr double EMA_DOWN = 0.08; + const double alpha = (sample > covered_drift_ema) ? EMA_UP : EMA_DOWN; + covered_drift_ema = (1.0 - alpha) * covered_drift_ema + alpha * sample; + } + + bool HasContactSetCoverageSample() const { return has_covered_drift_sample; } + + unsigned int LastObservedContactSetCoverage() const { return last_covered_drift; } + + unsigned int Recommend(unsigned int current_cmd, + float target_multiple, + float target_more, + unsigned int upper_bound, + unsigned int tweak_step) { + if (upper_bound == 0) { + return 0; + } + + current_cmd = std::min(current_cmd, upper_bound); + if (!has_covered_drift_sample) { + return current_cmd; + } + + double target = covered_drift_ema * static_cast(target_multiple) + static_cast(target_more); + if (!std::isfinite(target) || target < 0.0) { + target = static_cast(current_cmd); + } + const unsigned int target_cmd = + std::min(upper_bound, static_cast(std::ceil(std::max(0.0, target)))); + + unsigned int next_cmd = current_cmd; + const unsigned int step = std::max(1u, tweak_step); + if (target_cmd > current_cmd) { + next_cmd = std::min(upper_bound, current_cmd + step); + } else if (target_cmd < current_cmd) { + if (force_non_decreasing_once) { + next_cmd = current_cmd; + } else { + next_cmd = (current_cmd > step) ? (current_cmd - step) : 0; + } + } + + force_non_decreasing_once = false; + return std::min(next_cmd, upper_bound); + } + + unsigned int BumpAfterWait(unsigned int current_cmd, unsigned int upper_bound, unsigned int tweak_step) { + if (upper_bound == 0) { + return 0; + } + force_non_decreasing_once = true; + const unsigned int step = std::max(1u, tweak_step); + return std::min(upper_bound, current_cmd + step); + } +}; + +// Host-side snapshot of dT's old contact arrays, used only for DEBUG-verbosity lost-contact diagnostics. +struct LostContactDebugSnapshot { + size_t nPatchContacts = 0; + size_t nPrimitiveContacts = 0; + std::vector idPatchA; + std::vector idPatchB; + std::vector contactTypePatch; + std::vector contactPatchIsland; + std::vector idPrimitiveA; + std::vector idPrimitiveB; + std::vector contactTypePrimitive; + std::vector geomToPatchMap; + std::vector primitivePenetrationStorage; + std::vector primitiveAreaStorage; +}; + +} // namespace deme + +#endif diff --git a/src/DEM/HostSideHelpers.hpp b/src/DEM/utils/HostSideHelpers.hpp similarity index 76% rename from src/DEM/HostSideHelpers.hpp rename to src/DEM/utils/HostSideHelpers.hpp index 20d9a53d..ed2cac11 100644 --- a/src/DEM/HostSideHelpers.hpp +++ b/src/DEM/utils/HostSideHelpers.hpp @@ -24,42 +24,93 @@ #include #include #include +#include +#include #include "../kernel/DEMHelperKernels.cuh" -#include "VariableTypes.h" +#include "../VariableTypes.h" +#include "../../core/utils/Logger.hpp" namespace deme { -// Generic helper function to run something in a thread and get the result -template -inline auto run_in_thread(Func&& func, Args&&... args) -> decltype(func(args...)) { - using ReturnType = decltype(func(args...)); - - std::promise prom; - std::future fut = prom.get_future(); - - // Capture everything in a tuple - auto bound_args = std::make_tuple(std::forward(func), std::forward(args)...); - - std::thread t([&prom, bound_args = std::move(bound_args)]() mutable { - try { - auto result = std::apply(std::move(std::get<0>(bound_args)), - std::apply( - [](auto&&, auto&&... args_inner) { - return std::make_tuple(std::forward(args_inner)...); - }, - std::move(bound_args))); - prom.set_value(std::move(result)); - } catch (...) { - prom.set_exception(std::current_exception()); +namespace detail { + +struct QuantKey3 { + int64_t x, y, z; + bool operator==(const QuantKey3& o) const noexcept { return x == o.x && y == o.y && z == o.z; } +}; + +struct QuantKey3Hash { + size_t operator()(const QuantKey3& k) const noexcept { + size_t h1 = std::hash{}(k.x); + size_t h2 = std::hash{}(k.y); + size_t h3 = std::hash{}(k.z); + size_t h = h1; + h ^= h2 + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); + h ^= h3 + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); + return h; + } +}; + +inline int64_t quantize_coord(double v, double eps) { + return static_cast(std::llround(v / eps)); +} + +} // namespace detail + +inline double computeVertexQuantEps(const std::vector& vertices) { + if (vertices.empty()) { + return 0.0; + } + double minx = vertices[0].x, miny = vertices[0].y, minz = vertices[0].z; + double maxx = minx, maxy = miny, maxz = minz; + for (const auto& v : vertices) { + minx = std::min(minx, static_cast(v.x)); + miny = std::min(miny, static_cast(v.y)); + minz = std::min(minz, static_cast(v.z)); + maxx = std::max(maxx, static_cast(v.x)); + maxy = std::max(maxy, static_cast(v.y)); + maxz = std::max(maxz, static_cast(v.z)); + } + const double dx = maxx - minx, dy = maxy - miny, dz = maxz - minz; + const double diag = std::sqrt(dx * dx + dy * dy + dz * dz); + return std::max(diag * 1e-9, 1e-12); +} + +inline std::vector buildCanonicalVertexMap(const std::vector& vertices, double eps) { + if (vertices.empty()) { + return {}; + } + if (eps <= 0.0) { + eps = 1e-12; + } + std::unordered_map rep; + rep.reserve(vertices.size()); + std::vector canon(vertices.size(), static_cast(-1)); + size_t next_id = 0; + for (size_t i = 0; i < vertices.size(); ++i) { + const auto& v = vertices[i]; + detail::QuantKey3 key{detail::quantize_coord(v.x, eps), detail::quantize_coord(v.y, eps), + detail::quantize_coord(v.z, eps)}; + auto it = rep.find(key); + if (it == rep.end()) { + rep.emplace(key, next_id); + canon[i] = next_id; + next_id++; + } else { + canon[i] = it->second; } - }); + } + return canon; +} - t.join(); - return fut.get(); +// Generic helper to access tuple of pointers +template +auto dereference_at(const Tuple& ptrs, size_t idx, std::index_sequence) { + return std::make_tuple((std::get(ptrs)[idx])...); } -inline int randomZeroOrOne() { +inline int random_zero_or_one() { std::random_device rd; // Random number device to seed the generator std::mt19937 gen(rd()); // Mersenne Twister generator std::uniform_int_distribution<> dist(0, 1); @@ -90,26 +141,52 @@ inline void displayArray(T1* arr, size_t n) { std::cout << std::endl; } +// Get device array to host +template +inline std::vector getDeviceArray(T1* d_arr, size_t n) { + std::vector h_arr(n); + DEME_GPU_CALL(cudaMemcpy(h_arr.data(), d_arr, n * sizeof(T1), cudaMemcpyDeviceToHost)); + return h_arr; +} + // An extremely inefficient device vector view function template inline void displayDeviceArray(T1* arr, size_t n) { - std::vector tmp(n); - DEME_GPU_CALL(cudaMemcpy(tmp.data(), arr, n * sizeof(T1), cudaMemcpyDeviceToHost)); + std::vector tmp = getDeviceArray(arr, n); for (size_t i = 0; i < n; i++) { std::cout << +(tmp[i]) << " "; } std::cout << std::endl; } -inline void displayDeviceFloat3(float3* arr, size_t n) { - std::vector tmp(n); - DEME_GPU_CALL(cudaMemcpy(tmp.data(), arr, n * sizeof(float3), cudaMemcpyDeviceToHost)); +// An extremely inefficient device float3 vector view function +template +inline void displayDeviceFloat3(T1* arr, size_t n) { + std::vector tmp = getDeviceArray(arr, n); for (size_t i = 0; i < n; i++) { std::cout << "(" << +(tmp[i].x) << ", " << +(tmp[i].y) << ", " << +(tmp[i].z) << "), "; } std::cout << std::endl; } +// A host vector view function +template +inline void displayHostArray(T1* arr, size_t n) { + for (size_t i = 0; i < n; i++) { + std::cout << +(arr[i]) << " "; + } + std::cout << std::endl; +} + +// A host float3 vector view function +template +inline void displayHostFloat3(T1* arr, size_t n) { + for (size_t i = 0; i < n; i++) { + std::cout << "(" << +(arr[i].x) << ", " << +(arr[i].y) << ", " << +(arr[i].z) << "), "; + } + std::cout << std::endl; +} + template inline T1 vector_sum(const std::vector& vect) { T1 sum_of_elems = std::accumulate(vect.begin(), vect.end(), T1(0)); @@ -148,13 +225,6 @@ inline void hostPrefixScan(T1* arr, size_t n) { } } -template -inline void elemSwap(T1* x, T1* y) { - T1 tmp = *x; - *x = *y; - *y = tmp; -} - /// Return a perpendicular unit vector of the input vector. Will not work well if the input vector is too short; /// consider normalizing it if it is the case. template @@ -174,9 +244,9 @@ inline T1 findPerpendicular(const T1& input_vec) { /// Generate a mapping between arr1 and arr2, where this mapping has the same length as arr1, and each element i in the /// mapping is arr1[i]'s corresponding (same) element number in arr2. Both arr1 and arr2 need to be sorted. template -inline void hostMergeSearchMapGen(T1* arr1, T1* arr2, T1* map, size_t size1, size_t size2, T1 NULL_ID) { - size_t ind2 = 0; - for (size_t ind1 = 0; ind1 < size1; ind1++) { +inline void hostMergeSearchMapGen(const T1* arr1, const T1* arr2, T1* map, size_t size1, size_t size2, T1 NULL_ID) { + T1 ind2 = 0; + for (T1 ind1 = 0; ind1 < size1; ind1++) { map[ind1] = NULL_ID; while (ind2 < size2) { if (arr1[ind1] < arr2[ind2]) { @@ -317,6 +387,27 @@ inline std::string compact_code(const std::string& prgm) { return res; } +inline std::string pretty_format_bytes(size_t bytes) { + // set up byte prefixes + constexpr size_t KIBI = 1024; + constexpr size_t MEBI = KIBI * KIBI; + constexpr size_t GIBI = KIBI * KIBI * KIBI; + float gibival = float(bytes) / GIBI; + float mebival = float(bytes) / MEBI; + float kibival = float(bytes) / KIBI; + std::stringstream ret; + if (gibival > 1) { + ret << gibival << " GiB"; + } else if (mebival > 1) { + ret << mebival << " MiB"; + } else if (kibival > 1) { + ret << kibival << " KiB"; + } else { + ret << bytes << " B"; + } + return ret.str(); +} + /// Get a quaternion from axis and angle inline float4 QuatFromAxisAngle(const float3& axis, const float& theta) { float4 Q; @@ -337,6 +428,19 @@ inline float4 hostHamiltonProduct(const float4& Q1, const float4& Q2) { return Q; } +inline float4 quatConjugate(const float4& q) { + return make_float4(-q.x, -q.y, -q.z, q.w); +} + +inline float4 quatNormalizeSafe(const float4& q) { + const float n2 = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w; + if (n2 <= DEME_TINY_FLOAT || !std::isfinite(n2)) { + return make_float4(0, 0, 0, 1); + } + const float inv = 1.f / std::sqrt(n2); + return make_float4(q.x * inv, q.y * inv, q.z * inv, q.w * inv); +} + /// Rotate a quaternion about an unit axis by an angle inline float4 RotateQuat(const float4& quat, const float3& axis, const float& theta) { // Rotation to quaternion first @@ -391,8 +495,8 @@ inline void hostSortByKey(T1* keys, T2* vals, size_t n) { swapped = false; for (size_t j = 0; j < n - i - 1; j++) { if (keys[j] > keys[j + 1]) { - elemSwap(&keys[j], &keys[j + 1]); - elemSwap(&vals[j], &vals[j + 1]); + std::swap(keys[j], keys[j + 1]); + std::swap(vals[j], vals[j + 1]); swapped = true; } } @@ -610,7 +714,11 @@ inline void applyFrameTransformGlobalToLocal(T1& pos, const T2& vec, const T3& r pos.x -= vec.x; pos.y -= vec.y; pos.z -= vec.z; - applyOriQToVector3(pos.x, pos.y, pos.z, rot_Q.w, -rot_Q.x, -rot_Q.y, -rot_Q.z); + auto conjQ = rot_Q; + conjQ.x = -conjQ.x; + conjQ.y = -conjQ.y; + conjQ.z = -conjQ.z; + applyOriQToVector3(pos, conjQ); } /// Translating the inverse of the provided vec then applying a local inverse rotation of the provided rot_Q, then /// return the result. @@ -684,33 +792,54 @@ inline std::string read_file_to_string(const std::filesystem::path& sourcefile) return buffer.str(); } +// Rank transform a vector, i.e. replace each element with its rank in the vector. The smallest element will be replaced +// by 0, the second smallest by 1, and so on. This is useful for mapping non-contiguous IDs to contiguous ones. +template +inline std::pair, bool> rank_transform(const std::vector& a) { + std::vector vals = a; + std::sort(vals.begin(), vals.end()); + vals.erase(unique(vals.begin(), vals.end()), vals.end()); + + std::unordered_map rank; + for (T i = 0; i < (T)vals.size(); ++i) + rank[vals[i]] = i; + + std::vector out; + out.reserve(a.size()); + bool changed = false; + + for (T x : a) { + T new_x = rank[x]; + if (new_x != x) + changed = true; + out.push_back(new_x); + } + return {out, changed}; +} + // Asserters (for the convenience of Python wrapper) template inline void assertPositive(const T& var, const std::string& func_name, const std::string& var_name) { if (var <= (T)0) { - std::stringstream out; - out << func_name << "'s " << var_name << " argument needs to be all-positive.\n"; - throw std::runtime_error(out.str()); + DEME_ERROR("%s's %s argument needs to be all-positive.", func_name.c_str(), var_name.c_str()); } } template inline void assertThreeElements(const std::vector& vec, const std::string& func_name, const std::string& var_name) { if (vec.size() != 3) { - std::stringstream out; - out << func_name << "'s " << var_name - << " argument needs to be, or be composed of, length-3 lists/vectors. The provided size is " << vec.size() - << ".\n"; - throw std::runtime_error(out.str()); + DEME_ERROR( + "%s's %s argument needs to be, or be composed of, length-3 lists/vectors. The provided size is " + "%zu.", + func_name.c_str(), var_name.c_str(), vec.size()); } } template inline void assertFourElements(const std::vector& vec, const std::string& func_name, const std::string& var_name) { if (vec.size() != 4) { - std::stringstream out; - out << func_name << "'s " << var_name - << " argument needs to be, or be composed of, length-4 lists/vectors. The provided size is " << vec.size() - << ".\n"; - throw std::runtime_error(out.str()); + DEME_ERROR( + "%s's %s argument needs to be, or be composed of, length-4 lists/vectors. The provided size is " + "%zu.", + func_name.c_str(), var_name.c_str(), vec.size()); } } template @@ -718,12 +847,10 @@ inline void assertThreeElementsVector(const std::vector>& vec, const std::string& func_name, const std::string& var_name) { if (vec.at(0).size() != 3) { - std::stringstream out; - out << func_name << "'s " << var_name - << " argument needs to be a list/vector of length 3 vectors (in other words, n by 3 matrix).\n The " - "provided size is " - << vec.size() << " by " << vec.at(0).size() << ".\n"; - throw std::runtime_error(out.str()); + DEME_ERROR( + "%s's %s argument needs to be a list/vector of length 3 vectors (in other words, n by 3 matrix).\n " + "The provided size is %zu by %zu.", + func_name.c_str(), var_name.c_str(), vec.size(), vec.at(0).size()); } } template @@ -731,12 +858,10 @@ inline void assertFourElementsVector(const std::vector>& vec, const std::string& func_name, const std::string& var_name) { if (vec.at(0).size() != 4) { - std::stringstream out; - out << func_name << "'s " << var_name - << " argument needs to be a list/vector of length 4 vectors (in other words, n by 4 matrix).\n The " - "provided size is " - << vec.size() << " by " << vec.at(0).size() << ".\n"; - throw std::runtime_error(out.str()); + DEME_ERROR( + "%s's %s argument needs to be a list/vector of length 4 vectors (in other words, n by 4 matrix).\n " + "The provided size is %zu by %zu.", + func_name.c_str(), var_name.c_str(), vec.size(), vec.at(0).size()); } } diff --git a/src/DEM/utils/MeshUtils.cpp b/src/DEM/utils/MeshUtils.cpp new file mode 100644 index 00000000..21bb05c5 --- /dev/null +++ b/src/DEM/utils/MeshUtils.cpp @@ -0,0 +1,1804 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// This file contains modifications of the code by Alessandro Tasora and Radu Serban +// Below is the BSD license + +// Copyright (c) 2016, Project Chrono Development Team +// All Rghts Reserved. + +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: + +// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// - Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DEM/BdrsAndObjs.h" +#include "DEM/utils/HostSideHelpers.hpp" +#include "DEM/utils/MeshUtils.hpp" +#include "core/utils/Logger.hpp" +#include "core/utils/WavefrontMeshLoader.hpp" +#include "kernel/DEMHelperKernels.cuh" + +namespace deme { + +using namespace WAVEFRONT; + +std::vector> DEMMesh::GetCoordsVerticesAsVectorOfVectors() { + auto vec = GetCoordsVertices(); + std::vector> res(vec.size()); + for (size_t i = 0; i < vec.size(); i++) { + res[i] = {vec[i].x, vec[i].y, vec[i].z}; + } + return res; +} + +std::vector> DEMMesh::GetIndicesVertexesAsVectorOfVectors() { + auto vec = GetIndicesVertexes(); + std::vector> res(vec.size()); + for (size_t i = 0; i < vec.size(); i++) { + res[i] = {vec[i].x, vec[i].y, vec[i].z}; + } + return res; +} + +bool DEMMesh::LoadSTLMesh(std::string input_file, bool load_normals) { + Clear(); + filename = input_file; + + std::ifstream file(filename, std::ios::binary); + if (!file) { + DEME_ERROR_NOTHROW("Error loading STL file %s", filename.c_str()); + return false; + } + std::vector buffer((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (buffer.size() < 84) { + DEME_ERROR_NOTHROW("STL file %s is too small to contain any triangles.", filename.c_str()); + return false; + } + + auto set_default_patch_info = [this]() { + this->nTri = m_face_v_indices.size(); + this->m_patch_ids.clear(); + this->m_patch_ids.resize(this->nTri, 0); + this->nPatches = 1; + this->patches_explicitly_set = false; + }; + + auto load_binary = [&](uint32_t tri_count) -> bool { + size_t expected_size = 84 + static_cast(tri_count) * 50; + if (buffer.size() < expected_size) { + DEME_ERROR_NOTHROW("Binary STL file %s ended unexpectedly.", filename.c_str()); + return false; + } + const unsigned char* data = reinterpret_cast(buffer.data()); + size_t offset = 84; + for (uint32_t i = 0; i < tri_count; i++) { + float floats[12]; + std::memcpy(floats, data + offset, sizeof(float) * 12); + float3 v0 = make_float3(floats[3], floats[4], floats[5]); + float3 v1 = make_float3(floats[6], floats[7], floats[8]); + float3 v2 = make_float3(floats[9], floats[10], floats[11]); + size_t base = m_vertices.size(); + m_vertices.push_back(v0); + m_vertices.push_back(v1); + m_vertices.push_back(v2); + m_face_v_indices.push_back(make_int3((int)base, (int)base + 1, (int)base + 2)); + offset += 50; + } + return true; + }; + + // Heuristics to decide if STL is binary + uint32_t tri_count = 0; + std::memcpy(&tri_count, buffer.data() + 80, sizeof(uint32_t)); + size_t expected_size = 84 + static_cast(tri_count) * 50; + bool looks_binary = expected_size == buffer.size(); + bool looks_ascii = false; + if (!looks_binary) { + std::string header(buffer.data(), buffer.data() + std::min(buffer.size(), 5)); + if (header == "solid") { + looks_ascii = true; + } + } + + bool parsed = false; + if (looks_binary) { + parsed = load_binary(tri_count); + } + if (!parsed) { + // Fallback to ASCII parsing + std::istringstream iss(std::string(buffer.begin(), buffer.end())); + std::string line; + std::vector facet_vertices; + facet_vertices.reserve(3); + while (std::getline(iss, line)) { + std::istringstream ls(line); + std::string token; + ls >> token; + if (token == "facet") { + continue; + } + if (token == "vertex") { + float3 v{}; + ls >> v.x >> v.y >> v.z; + facet_vertices.push_back(v); + if (facet_vertices.size() == 3) { + size_t base = m_vertices.size(); + m_vertices.push_back(facet_vertices[0]); + m_vertices.push_back(facet_vertices[1]); + m_vertices.push_back(facet_vertices[2]); + m_face_v_indices.push_back(make_int3((int)base, (int)base + 1, (int)base + 2)); + facet_vertices.clear(); + } + } + } + } + + if (m_face_v_indices.empty()) { + DEME_ERROR_NOTHROW("Failed to parse STL file %s.", filename.c_str()); + return false; + } + + set_default_patch_info(); + + // Compute one geometric normal per facet from triangle winding. + // This restores the previous DEM contact behavior and avoids relying on + // inconsistent/inverted normals embedded in STL files. + if (load_normals) { + m_normals.clear(); + m_face_n_indices.clear(); + m_normals.reserve(m_face_v_indices.size()); + m_face_n_indices.reserve(m_face_v_indices.size()); + for (size_t i = 0; i < m_face_v_indices.size(); ++i) { + const int3& f = m_face_v_indices[i]; + const float3& v0 = m_vertices[f.x]; + const float3& v1 = m_vertices[f.y]; + const float3& v2 = m_vertices[f.z]; + const float3 n = face_normal(v0, v1, v2); + m_normals.push_back(n); + m_face_n_indices.push_back(make_int3((int)i, (int)i, (int)i)); + } + } else { + m_normals.clear(); + m_face_n_indices.clear(); + } + // STL has no UV by design; clear to mirror OBJ loader when UVs are absent. + m_UV.clear(); + m_face_uv_indices.clear(); + + { + size_t boundary_edges = 0; + size_t nonmanifold_edges = 0; + if (!IsWatertight(&boundary_edges, &nonmanifold_edges)) { + DEME_WARNING( + "Mesh %s is not watertight (boundary edges: %zu, non-manifold edges: %zu). Auto Volume/MOI may be " + "inaccurate.", + filename.c_str(), boundary_edges, nonmanifold_edges); + } + } + return true; +} + +bool DEMMesh::LoadPLYMesh(std::string input_file, bool load_normals) { + Clear(); + filename = input_file; + + std::ifstream file(filename, std::ios::binary); + if (!file) { + DEME_ERROR_NOTHROW("Error loading PLY file %s", filename.c_str()); + return false; + } + + std::string line; + if (!std::getline(file, line) || line != "ply") { + DEME_ERROR_NOTHROW("PLY file %s is missing magic header.", filename.c_str()); + return false; + } + + enum class PLYFormat { ASCII, BINARY_LE, BINARY_BE }; + PLYFormat format = PLYFormat::ASCII; + size_t num_vertices = 0; + size_t num_faces = 0; + // Track vertex property order to find position/normal fields + std::vector vertex_props; + std::vector vertex_prop_types; + bool in_vertex = false; + // Face list types + std::string face_count_type; + std::string face_index_type; + + while (std::getline(file, line)) { + if (line == "end_header") { + break; + } + std::istringstream ls(line); + std::string token; + ls >> token; + if (token == "format") { + std::string fmt; + ls >> fmt; + if (fmt.find("ascii") == 0) { + format = PLYFormat::ASCII; + } else if (fmt.find("binary_little_endian") == 0) { + format = PLYFormat::BINARY_LE; + } else if (fmt.find("binary_big_endian") == 0) { + format = PLYFormat::BINARY_BE; + } + } else if (token == "element") { + std::string elem; + ls >> elem; + if (elem == "vertex") { + ls >> num_vertices; + in_vertex = true; + } else if (elem == "face") { + ls >> num_faces; + in_vertex = false; + } else { + in_vertex = false; + } + } else if (token == "property" && in_vertex) { + std::string type, name; + ls >> type >> name; + if (!name.empty()) { + vertex_props.push_back(name); + vertex_prop_types.push_back(type); + } + } else if (token == "property" && !in_vertex) { + std::string maybe_list; + ls >> maybe_list; + if (maybe_list == "list") { + ls >> face_count_type >> face_index_type; + // ignore name + } + } + } + + if (format == PLYFormat::BINARY_BE) { + DEME_ERROR_NOTHROW("PLY file %s uses big-endian binary, which is not supported.", filename.c_str()); + return false; + } + if (num_vertices == 0 || num_faces == 0) { + DEME_ERROR_NOTHROW("PLY file %s does not contain vertices or faces.", filename.c_str()); + return false; + } + + auto find_prop = [&](const std::string& name) -> int { + for (int i = 0; i < static_cast(vertex_props.size()); ++i) { + if (vertex_props[i] == name) + return i; + } + return -1; + }; + const int idx_x = find_prop("x"); + const int idx_y = find_prop("y"); + const int idx_z = find_prop("z"); + const int idx_nx = find_prop("nx"); + const int idx_ny = find_prop("ny"); + const int idx_nz = find_prop("nz"); + const bool has_vertex_normals = idx_nx >= 0 && idx_ny >= 0 && idx_nz >= 0; + + m_vertices.reserve(num_vertices); + m_face_v_indices.reserve(num_faces); + + auto read_scalar_le = [&](std::istream& is, const std::string& type, double& out) -> bool { + if (type == "float" || type == "float32") { + float v; + if (!is.read(reinterpret_cast(&v), sizeof(float))) + return false; + out = static_cast(v); + return true; + } + if (type == "double" || type == "float64") { + double v; + if (!is.read(reinterpret_cast(&v), sizeof(double))) + return false; + out = v; + return true; + } + if (type == "uchar" || type == "uint8") { + std::uint8_t v; + if (!is.read(reinterpret_cast(&v), sizeof(std::uint8_t))) + return false; + out = static_cast(v); + return true; + } + if (type == "char" || type == "int8") { + std::int8_t v; + if (!is.read(reinterpret_cast(&v), sizeof(std::int8_t))) + return false; + out = static_cast(v); + return true; + } + if (type == "int" || type == "int32") { + std::int32_t v; + if (!is.read(reinterpret_cast(&v), sizeof(std::int32_t))) + return false; + out = static_cast(v); + return true; + } + if (type == "uint" || type == "uint32") { + std::uint32_t v; + if (!is.read(reinterpret_cast(&v), sizeof(std::uint32_t))) + return false; + out = static_cast(v); + return true; + } + return false; + }; + + // Read vertices + for (size_t i = 0; i < num_vertices; ++i) { + if (format == PLYFormat::ASCII) { + if (!std::getline(file, line)) { + DEME_ERROR_NOTHROW("Unexpected EOF while reading vertices in %s.", filename.c_str()); + return false; + } + std::istringstream ls(line); + std::vector vals; + double v; + while (ls >> v) { + vals.push_back(v); + } + if (idx_x < 0 || idx_y < 0 || idx_z < 0 || vals.size() <= std::max({idx_x, idx_y, idx_z})) { + DEME_ERROR_NOTHROW("Vertex position data missing in %s.", filename.c_str()); + return false; + } + float3 p = make_float3(static_cast(vals[idx_x]), static_cast(vals[idx_y]), + static_cast(vals[idx_z])); + m_vertices.push_back(p); + if (has_vertex_normals && vals.size() > static_cast(std::max({idx_nx, idx_ny, idx_nz}))) { + float3 n = make_float3(static_cast(vals[idx_nx]), static_cast(vals[idx_ny]), + static_cast(vals[idx_nz])); + m_normals.push_back(n); + } + } else { + // Binary little-endian + std::vector vals(vertex_props.size(), 0.0); + for (size_t p = 0; p < vertex_props.size(); ++p) { + if (!read_scalar_le(file, vertex_prop_types[p], vals[p])) { + DEME_ERROR_NOTHROW("Failed to read vertex data in binary PLY %s.", filename.c_str()); + return false; + } + } + if (idx_x < 0 || idx_y < 0 || idx_z < 0) { + DEME_ERROR_NOTHROW("Vertex position data missing in %s.", filename.c_str()); + return false; + } + float3 p = make_float3(static_cast(vals[idx_x]), static_cast(vals[idx_y]), + static_cast(vals[idx_z])); + m_vertices.push_back(p); + if (has_vertex_normals && vals.size() > static_cast(std::max({idx_nx, idx_ny, idx_nz}))) { + float3 n = make_float3(static_cast(vals[idx_nx]), static_cast(vals[idx_ny]), + static_cast(vals[idx_nz])); + m_normals.push_back(n); + } + } + } + + // Read faces + std::vector faces; + faces.reserve(num_faces); + for (size_t i = 0; i < num_faces; ++i) { + if (format == PLYFormat::ASCII) { + if (!std::getline(file, line)) { + DEME_ERROR_NOTHROW("Unexpected EOF while reading faces in %s.", filename.c_str()); + return false; + } + std::istringstream ls(line); + int verts_in_face = 0; + ls >> verts_in_face; + if (verts_in_face < 3) { + continue; // ignore degenerate + } + std::vector idx(verts_in_face); + for (int j = 0; j < verts_in_face; ++j) { + ls >> idx[j]; + } + for (int t = 1; t < verts_in_face - 1; ++t) { + faces.push_back(make_int3(idx[0], idx[t], idx[t + 1])); + } + } else { + // Binary little-endian faces: expect list uchar count, int indices + double count_d = 0.0; + if (!read_scalar_le(file, face_count_type.empty() ? "uchar" : face_count_type, count_d)) { + DEME_ERROR_NOTHROW("Failed to read face count in binary PLY %s.", filename.c_str()); + return false; + } + int verts_in_face = static_cast(count_d); + if (verts_in_face < 3) { + // Skip indices + for (int j = 0; j < verts_in_face; ++j) { + double throwaway; + if (!read_scalar_le(file, face_index_type.empty() ? "int" : face_index_type, throwaway)) { + DEME_ERROR_NOTHROW("Failed to skip face indices in binary PLY %s.", filename.c_str()); + return false; + } + } + continue; + } + std::vector idx(verts_in_face); + for (int j = 0; j < verts_in_face; ++j) { + double v = 0.0; + if (!read_scalar_le(file, face_index_type.empty() ? "int" : face_index_type, v)) { + DEME_ERROR_NOTHROW("Failed to read face indices in binary PLY %s.", filename.c_str()); + return false; + } + idx[j] = static_cast(v); + } + for (int t = 1; t < verts_in_face - 1; ++t) { + faces.push_back(make_int3(idx[0], idx[t], idx[t + 1])); + } + } + } + + if (faces.empty()) { + DEME_ERROR_NOTHROW("No faces parsed from PLY file %s.", filename.c_str()); + return false; + } + + m_face_v_indices = std::move(faces); + nTri = m_face_v_indices.size(); + + if (load_normals) { + m_normals.clear(); + m_face_n_indices.clear(); + m_normals.reserve(nTri); + m_face_n_indices.reserve(nTri); + for (size_t i = 0; i < nTri; ++i) { + const int3& f = m_face_v_indices[i]; + const float3& v0 = m_vertices[f.x]; + const float3& v1 = m_vertices[f.y]; + const float3& v2 = m_vertices[f.z]; + float3 n = face_normal(v0, v1, v2); + m_normals.push_back(n); + m_face_n_indices.push_back(make_int3((int)i, (int)i, (int)i)); + } + } else { + m_normals.clear(); + m_face_n_indices.clear(); + } + m_UV.clear(); + m_face_uv_indices.clear(); + + // Default patch info: one patch + m_patch_ids.assign(nTri, 0); + nPatches = 1; + patches_explicitly_set = false; + return true; +} + +bool DEMMesh::LoadWavefrontMesh(std::string input_file, bool load_normals, bool load_uv) { + this->m_vertices.clear(); + this->m_normals.clear(); + this->m_UV.clear(); + this->m_face_v_indices.clear(); + this->m_face_n_indices.clear(); + this->m_face_uv_indices.clear(); + + GeometryInterface emptybm; // BuildMesh bm; + + filename = input_file; + + OBJ obj; + + int ret = obj.LoadMesh(filename.c_str(), &emptybm, true); + if (ret == -1) { + DEME_ERROR_NOTHROW("Error loading OBJ file %s", filename.c_str()); + return false; + } + + float3 tmp_f3; + int3 tmp_i3; + for (unsigned int iv = 0; iv < obj.mVerts.size(); iv += 3) { + tmp_f3.x = obj.mVerts[iv]; + tmp_f3.y = obj.mVerts[iv + 1]; + tmp_f3.z = obj.mVerts[iv + 2]; + this->m_vertices.push_back(tmp_f3); + } + for (unsigned int in = 0; in < obj.mNormals.size(); in += 3) { + tmp_f3.x = obj.mNormals[in]; + tmp_f3.y = obj.mNormals[in + 1]; + tmp_f3.z = obj.mNormals[in + 2]; + this->m_normals.push_back(tmp_f3); + } + for (unsigned int it = 0; it < obj.mTexels.size(); it += 2) // +2 because only u,v each texel + { + tmp_f3.x = obj.mTexels[it]; + tmp_f3.y = obj.mTexels[it + 1]; + tmp_f3.z = 0; + this->m_UV.push_back(tmp_f3); + } + for (unsigned int iiv = 0; iiv < obj.mIndexesVerts.size(); iiv += 3) { + tmp_i3.x = obj.mIndexesVerts[iiv]; + tmp_i3.y = obj.mIndexesVerts[iiv + 1]; + tmp_i3.z = obj.mIndexesVerts[iiv + 2]; + this->m_face_v_indices.push_back(tmp_i3); + } + for (unsigned int iin = 0; iin < obj.mIndexesNormals.size(); iin += 3) { + tmp_i3.x = obj.mIndexesNormals[iin]; + tmp_i3.y = obj.mIndexesNormals[iin + 1]; + tmp_i3.z = obj.mIndexesNormals[iin + 2]; + this->m_face_n_indices.push_back(tmp_i3); + } + for (unsigned int iit = 0; iit < obj.mIndexesTexels.size(); iit += 3) { + tmp_i3.x = obj.mIndexesTexels[iit]; + tmp_i3.y = obj.mIndexesTexels[iit + 1]; + tmp_i3.z = obj.mIndexesTexels[iit + 2]; + this->m_face_uv_indices.push_back(tmp_i3); + } + + if (!load_normals) { + this->m_normals.clear(); + this->m_face_n_indices.clear(); + } + if (!load_uv) { + this->m_UV.clear(); + this->m_face_uv_indices.clear(); + } + + this->nTri = m_face_v_indices.size(); + + // Initialize default patch info: all triangles in patch 0 (assuming convex mesh) + this->m_patch_ids.clear(); + this->m_patch_ids.resize(this->nTri, 0); + this->nPatches = 1; + this->patches_explicitly_set = false; + + { + size_t boundary_edges = 0; + size_t nonmanifold_edges = 0; + if (!IsWatertight(&boundary_edges, &nonmanifold_edges)) { + DEME_WARNING( + "Mesh %s is not watertight (boundary edges: %zu, non-manifold edges: %zu). Volume/MOI may be " + "inaccurate.", + filename.c_str(), boundary_edges, nonmanifold_edges); + } + } + + return true; +} + +// Write the specified meshes in a Wavefront .obj file +void DEMMesh::WriteWavefront(const std::string& filename, std::vector& meshes) { + std::ofstream mf(filename); + + //// TODO: include normal information if available + + // Create a single object mesh + std::vector v_offsets; + int v_off = 1; + for (auto& m : meshes) { + for (auto& v : m.m_vertices) { + mf << "v " << v.x << " " << v.y << " " << v.z << std::endl; + } + v_offsets.push_back(v_off); + v_off += static_cast(m.m_vertices.size()); + } + + std::vector has_normals; + std::vector vn_offsets; + int vn_off = 1; + for (auto& m : meshes) { + has_normals.push_back(m.m_normals.size() > 0); + for (auto& v : m.m_normals) { + mf << "vn " << v.x << " " << v.y << " " << v.z << std::endl; + } + vn_offsets.push_back(vn_off); + vn_off += static_cast(m.m_normals.size()); + } + + for (size_t i = 0; i < meshes.size(); i++) { + v_off = v_offsets[i]; + if (has_normals[i]) { + auto& idxV = meshes[i].m_face_v_indices; + auto& idxN = meshes[i].m_face_n_indices; + assert(idxV.size() == idxN.size()); + vn_off = vn_offsets[i]; + for (int j = 0; j < idxV.size(); j++) { + mf << "f " << idxV[j].x + v_off << "//" << idxN[j].x + vn_off << " " << idxV[j].y + v_off << "//" + << idxN[j].y + vn_off << " " << idxV[j].z + v_off << "//" << idxN[j].z + vn_off << std::endl; + } + } else { + for (auto& f : meshes[i].m_face_v_indices) { + mf << "f " << f.x + v_off << " " << f.y + v_off << " " << f.z + v_off << std::endl; + } + } + } + + mf.close(); +} + +// Helper function to compute face normal for a triangle +static float3 computeFaceNormal(const float3& v0, const float3& v1, const float3& v2) { + float3 edge1 = make_float3(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z); + float3 edge2 = make_float3(v2.x - v0.x, v2.y - v0.y, v2.z - v0.z); + + // Cross product + float3 normal = make_float3(edge1.y * edge2.z - edge1.z * edge2.y, edge1.z * edge2.x - edge1.x * edge2.z, + edge1.x * edge2.y - edge1.y * edge2.x); + + // Normalize + float length = std::sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z); + if (length > DEME_TINY_FLOAT) { + normal.x /= length; + normal.y /= length; + normal.z /= length; + } + + return normal; +} + +// Helper function to compute angle between two normals (in degrees) +static float computeAngleBetweenNormals(const float3& n1, const float3& n2) { + // Compute dot product + float dot_product = n1.x * n2.x + n1.y * n2.y + n1.z * n2.z; + + // Clamp to [-1, 1] to avoid numerical issues + dot_product = std::max(-1.0f, std::min(1.0f, dot_product)); + + // Compute angle in radians and convert to degrees + float angle_rad = std::acos(dot_product); + return angle_rad * 180.0f / deme::PI; +} + +// ------------------------------------------------------------ +// Helpers for advanced patching +// ------------------------------------------------------------ +struct EdgeAdjInfo { + size_t nbr = 0; + int va = -1; // oriented edge vertex A (as appears in the current triangle) + int vb = -1; // oriented edge vertex B (as appears in the current triangle) + bool oriented_ok = false; // true if the neighbor sees the shared edge reversed (good sign for oriented manifold) +}; + +static inline float dot3(const float3& a, const float3& b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} +static inline float3 cross3(const float3& a, const float3& b) { + return make_float3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); +} +static inline float norm3(const float3& v) { + return std::sqrt(dot3(v, v)); +} +static inline float3 normalize3(const float3& v) { + float n = norm3(v); + if (n > DEME_TINY_FLOAT) + return make_float3(v.x / n, v.y / n, v.z / n); + return make_float3(0, 0, 0); +} +static inline float3 add3(const float3& a, const float3& b) { + return make_float3(a.x + b.x, a.y + b.y, a.z + b.z); +} +static inline float3 mul3(const float3& v, float s) { + return make_float3(v.x * s, v.y * s, v.z * s); +} +static inline float clamp11(float x) { + return std::max(-1.0f, std::min(1.0f, x)); +} +static inline float deg2rad(float deg) { + return deg * (deme::PI / 180.0f); +} +static inline float rad2deg(float rad) { + return rad * (180.0f / deme::PI); +} + +static float computeTriangleArea(const float3& v0, const float3& v1, const float3& v2) { + float3 e1 = make_float3(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z); + float3 e2 = make_float3(v2.x - v0.x, v2.y - v0.y, v2.z - v0.z); + float3 c = cross3(e1, e2); + return 0.5f * norm3(c); +} + +// Signed dihedral angle (deg) around oriented edge va->vb of the current triangle. +// Sign is meaningful only when edge orientation is reliable (oriented_ok == true). +static float signedDihedralDeg(const float3& n_cur, const float3& n_nbr, const float3& vA, const float3& vB) { + float3 e = normalize3(make_float3(vB.x - vA.x, vB.y - vA.y, vB.z - vA.z)); + float s = dot3(e, cross3(n_cur, n_nbr)); + float c = clamp11(dot3(n_cur, n_nbr)); + float theta = std::atan2(s, c); // [-pi, pi] + return rad2deg(theta); +} + +// Build triangle adjacency WITH oriented shared-edge info. +// Patch splitting must use geometric adjacency, not raw OBJ/STL vertex-index adjacency: those formats commonly +// duplicate vertices per face to carry separate normals/UVs. Canonical vertex IDs let such coincident vertices share +// edges for grouping while preserving the mesh's stored topology. Non-manifold edges (shared by != 2 faces) are treated +// as boundaries. +static std::vector> buildAdjacencyWithEdgeInfo(const std::vector& face_v_indices, + const std::vector& vertices) { + struct EdgeRec { + size_t f; + int a_raw; + int b_raw; + size_t a_canon; + size_t b_canon; + }; + + const size_t num_faces = face_v_indices.size(); + std::vector> adj(num_faces); + + std::vector canon; + if (!vertices.empty()) { + const double eps = computeVertexQuantEps(vertices); + canon = buildCanonicalVertexMap(vertices, eps); + } + + std::map, std::vector> edge_map; + + auto add_edge = [&](size_t f, int a, int b) { + if (a < 0 || b < 0) { + return; + } + size_t ca = static_cast(a); + size_t cb = static_cast(b); + if (!canon.empty()) { + if (ca >= canon.size() || cb >= canon.size()) { + return; + } + ca = canon[ca]; + cb = canon[cb]; + } + if (ca == cb) { + return; + } + size_t lo = std::min(ca, cb); + size_t hi = std::max(ca, cb); + edge_map[{lo, hi}].push_back(EdgeRec{f, a, b, ca, cb}); + }; + + for (size_t i = 0; i < num_faces; ++i) { + const int3& tri = face_v_indices[i]; + add_edge(i, tri.x, tri.y); + add_edge(i, tri.y, tri.z); + add_edge(i, tri.z, tri.x); + } + + for (const auto& kv : edge_map) { + const auto& recs = kv.second; + if (recs.size() != 2) { + continue; // boundary or non-manifold + } + const EdgeRec& r0 = recs[0]; + const EdgeRec& r1 = recs[1]; + + bool oriented_ok_0 = (r0.a_canon == r1.b_canon && r0.b_canon == r1.a_canon); + bool oriented_ok_1 = oriented_ok_0; + + adj[r0.f].push_back(EdgeAdjInfo{r1.f, r0.a_raw, r0.b_raw, oriented_ok_0}); + adj[r1.f].push_back(EdgeAdjInfo{r0.f, r1.a_raw, r1.b_raw, oriented_ok_1}); + } + + return adj; +} + +// Split mesh into connected patches using region-growing. The default option set preserves the original behavior: +// merge adjacent triangles when their local face-normal angle is below hard_angle_deg. +unsigned int DEMMesh::SplitIntoConvexPatches(float hard_angle_deg, + const PatchSplitOptions& opt, + PatchQualityReport* out_report, + const PatchQualityOptions& qopt) { + auto set_empty_report = [&]() { + if (!out_report) { + return; + } + *out_report = PatchQualityReport(); + out_report->requested_min = opt.patch_min; + out_report->requested_max = opt.patch_max; + }; + + if (nTri == 0) { + patches_explicitly_set = false; + nPatches = 1; + set_empty_report(); + return 0; + } + + m_patch_ids.clear(); + m_patch_ids.resize(nTri, -1); + + std::vector face_normals(nTri); + std::vector face_areas(nTri, 0.0f); + for (size_t i = 0; i < nTri; ++i) { + const int3& face = m_face_v_indices[i]; + const float3& v0 = m_vertices[face.x]; + const float3& v1 = m_vertices[face.y]; + const float3& v2 = m_vertices[face.z]; + face_normals[i] = computeFaceNormal(v0, v1, v2); + face_areas[i] = std::max(computeTriangleArea(v0, v1, v2), static_cast(DEME_TINY_FLOAT)); + } + + const std::vector> adjacency = buildAdjacencyWithEdgeInfo(m_face_v_indices, m_vertices); + std::vector seed_order(nTri); + for (size_t i = 0; i < nTri; ++i) { + seed_order[i] = i; + } + if (opt.seed_largest_first) { + std::sort(seed_order.begin(), seed_order.end(), + [&](size_t a, size_t b) { return face_areas[a] > face_areas[b]; }); + } + + int current_patch_id = 0; + std::vector queue; + const float hard_angle = std::max(0.0f, hard_angle_deg); + const float patch_normal_max = opt.patch_normal_max_deg; + const float concave_allow = std::max(0.0f, opt.concave_allow_deg); + + for (size_t seed : seed_order) { + if (m_patch_ids[seed] != -1) { + continue; + } + + queue.clear(); + queue.push_back(seed); + m_patch_ids[seed] = current_patch_id; + float3 patch_normal_sum = mul3(face_normals[seed], face_areas[seed]); + float patch_area_sum = face_areas[seed]; + + size_t queue_idx = 0; + while (queue_idx < queue.size()) { + size_t current = queue[queue_idx++]; + + for (const EdgeAdjInfo& edge_info : adjacency[current]) { + const size_t neighbor = edge_info.nbr; + if (m_patch_ids[neighbor] != -1) { + continue; + } + + const float local_angle = computeAngleBetweenNormals(face_normals[current], face_normals[neighbor]); + if (local_angle > hard_angle) { + continue; + } + + if (opt.block_concave_edges && edge_info.oriented_ok) { + const float dihedral = signedDihedralDeg(face_normals[current], face_normals[neighbor], + m_vertices[edge_info.va], m_vertices[edge_info.vb]); + if (dihedral < -concave_allow) { + continue; + } + } + + if (patch_normal_max >= 0.0f && patch_area_sum > DEME_TINY_FLOAT) { + const float3 mean_normal = normalize3(patch_normal_sum); + const float patch_angle = computeAngleBetweenNormals(mean_normal, face_normals[neighbor]); + if (patch_angle > patch_normal_max) { + continue; + } + } + + m_patch_ids[neighbor] = current_patch_id; + queue.push_back(neighbor); + patch_normal_sum = add3(patch_normal_sum, mul3(face_normals[neighbor], face_areas[neighbor])); + patch_area_sum += face_areas[neighbor]; + } + } + + current_patch_id++; + } + + nPatches = current_patch_id; + patches_explicitly_set = true; + + if (isMaterialSet) { + if (materials.size() == 1) { + materials = std::vector>(nPatches, materials[0]); + } else if (materials.size() != nPatches) { + DEME_ERROR( + "The number of materials set (%zu) does not match the number of patches (%u). Please set the " + "material (SetMaterial) for each patch or use a single material for all patches.", + materials.size(), nPatches); + } + } + + if (out_report) { + *out_report = PatchQualityReport(); + out_report->achieved_patches = nPatches; + out_report->requested_min = opt.patch_min; + out_report->requested_max = opt.patch_max; + out_report->per_patch.resize(nPatches); + if (nPatches > opt.patch_max) { + out_report->constraint_status = PatchConstraintStatus::TOO_MANY_UNMERGEABLE; + } else if (nPatches < opt.patch_min) { + out_report->constraint_status = PatchConstraintStatus::TOO_FEW_UNSPLITTABLE; + } + + std::vector normal_sums(nPatches, make_float3(0, 0, 0)); + std::vector area_sums(nPatches, 0.0f); + for (size_t i = 0; i < nTri; ++i) { + const patchID_t patch_id = m_patch_ids[i]; + if (patch_id < 0 || patch_id >= static_cast(nPatches)) { + continue; + } + normal_sums[patch_id] = add3(normal_sums[patch_id], mul3(face_normals[i], face_areas[i])); + area_sums[patch_id] += face_areas[i]; + out_report->per_patch[patch_id].n_tris++; + } + + for (unsigned int p = 0; p < nPatches; ++p) { + PatchQualityPatch& patch_report = out_report->per_patch[p]; + const float sum_norm = norm3(normal_sums[p]); + patch_report.coherence_r = (area_sums[p] > DEME_TINY_FLOAT) ? (sum_norm / area_sums[p]) : 1.0f; + const float3 mean_normal = + (sum_norm > DEME_TINY_FLOAT) ? mul3(normal_sums[p], 1.0f / sum_norm) : make_float3(0, 0, 0); + for (size_t i = 0; i < nTri; ++i) { + if (m_patch_ids[i] != static_cast(p)) { + continue; + } + patch_report.worst_angle_deg = + std::max(patch_report.worst_angle_deg, computeAngleBetweenNormals(mean_normal, face_normals[i])); + for (const EdgeAdjInfo& edge_info : adjacency[i]) { + if (edge_info.nbr <= i || m_patch_ids[edge_info.nbr] != static_cast(p)) { + continue; + } + const float local_angle = computeAngleBetweenNormals(face_normals[i], face_normals[edge_info.nbr]); + if (local_angle > hard_angle) { + patch_report.hard_crossings++; + } + if (opt.block_concave_edges) { + if (!edge_info.oriented_ok) { + patch_report.unoriented_edges++; + } else { + const float dihedral = + signedDihedralDeg(face_normals[i], face_normals[edge_info.nbr], + m_vertices[edge_info.va], m_vertices[edge_info.vb]); + if (dihedral < -concave_allow) { + patch_report.concave_crossings++; + } + } + } + } + } + + const float reference_angle = (patch_normal_max >= 0.0f) ? patch_normal_max : hard_angle; + bool critical = false; + bool warn = false; + critical |= qopt.hard_crossings_are_critical && patch_report.hard_crossings > 0; + critical |= qopt.concave_crossings_are_critical && patch_report.concave_crossings > 0; + warn |= patch_report.hard_crossings > 0 || patch_report.concave_crossings > 0; + warn |= opt.block_concave_edges && patch_report.unoriented_edges > qopt.unoriented_warn_threshold; + warn |= patch_report.coherence_r < qopt.safe_r; + warn |= patch_report.worst_angle_deg > reference_angle + qopt.warn_worst_angle_margin_deg; + if (patch_report.coherence_r < qopt.warn_r) { + warn = true; + } + + patch_report.level = + critical ? PatchQualityLevel::CRITICAL : (warn ? PatchQualityLevel::WARN : PatchQualityLevel::SAFE); + if (static_cast(patch_report.level) > static_cast(out_report->overall)) { + out_report->overall = patch_report.level; + } + } + if (out_report->constraint_status != PatchConstraintStatus::SATISFIED && + out_report->overall == PatchQualityLevel::SAFE) { + out_report->overall = PatchQualityLevel::WARN; + } + } + + return nPatches; +} + +// Manually set patch IDs for each triangle. +void DEMMesh::SetPatchIDs(const std::vector& patch_ids) { + assertTriLength(patch_ids.size(), "SetPatchIDs"); + + auto [compressed_ids, changed] = rank_transform(patch_ids); + + if (changed) { + DEME_WARNING( + std::string("Patch IDs you supplied for a mesh were not contiguous or did not start from 0.\nThey have " + "been transformed to be contiguous and start from 0.")); + } + + m_patch_ids = compressed_ids; + + if (!compressed_ids.empty()) { + int max_patch_id = *std::max_element(compressed_ids.begin(), compressed_ids.end()); + nPatches = max_patch_id + 1; + } else { + nPatches = 1; + } + + patches_explicitly_set = true; + + if (isMaterialSet) { + if (materials.size() == 1) { + materials = std::vector>(nPatches, materials[0]); + } else if (materials.size() != nPatches) { + DEME_ERROR( + "The number of materials set (%zu) does not match the number of patches (%u). Please set the " + "material (SetMaterial) for each patch or use a single material for all patches.", + materials.size(), nPatches); + } + } +} + +// Assign consecutive patch IDs so no two triangles are combined into the same mesh patch. +void DEMMesh::SetEachTriangleAsPatch() { + constexpr size_t max_patch_count = static_cast(std::numeric_limits::max()) + 1; + if (nTri > max_patch_count) { + DEME_ERROR( + "SetEachTriangleAsPatch cannot assign %zu triangles to unique patches because patchID_t supports at most " + "%zu non-negative patch IDs.", + nTri, max_patch_count); + } + + // Preserve material assignment when the source mesh already uses one material per multi-triangle patch. + if (isMaterialSet && materials.size() > 1 && materials.size() != nTri) { + if (materials.size() != nPatches || m_patch_ids.size() != nTri) { + DEME_ERROR( + "SetEachTriangleAsPatch cannot expand %zu mesh materials over %zu triangles because the existing " + "patch assignment is inconsistent.", + materials.size(), nTri); + } + std::vector> triangle_materials(nTri); + for (size_t triangle = 0; triangle < nTri; triangle++) { + const patchID_t old_patch = m_patch_ids[triangle]; + if (old_patch < 0 || static_cast(old_patch) >= materials.size()) { + DEME_ERROR("SetEachTriangleAsPatch found invalid existing patch ID %d at triangle %zu.", old_patch, + triangle); + } + triangle_materials[triangle] = materials[old_patch]; + } + materials = std::move(triangle_materials); + } + + std::vector patch_ids(nTri); + for (size_t triangle = 0; triangle < nTri; triangle++) { + patch_ids[triangle] = static_cast(triangle); + } + SetPatchIDs(patch_ids); +} + +// Compute patch locations (relative to CoM, which is implicitly at 0,0,0) +// If not explicitly set, calculates as: +// - Single patch: (0,0,0) +// - Multiple patches: average of triangle centroids per patch +std::vector DEMMesh::ComputePatchLocations() const { + std::vector patch_locations(nPatches, make_float3(0, 0, 0)); + + if (nPatches == 1) { + // Single patch: location is just CoM (0,0,0) + return patch_locations; + } + + // Multiple patches: compute average of triangle centroids per patch + std::vector patch_triangle_counts(nPatches, 0); + + for (size_t i = 0; i < nTri; ++i) { + const int3& face = m_face_v_indices[i]; + const float3& v0 = m_vertices[face.x]; + const float3& v1 = m_vertices[face.y]; + const float3& v2 = m_vertices[face.z]; + + float3 centroid = triangleCentroid(v0, v1, v2); + patchID_t patch_id = (i < m_patch_ids.size()) ? m_patch_ids[i] : 0; + + // Validate patch_id is within bounds + if (patch_id >= 0 && patch_id < static_cast(nPatches)) { + patch_locations[patch_id].x += centroid.x; + patch_locations[patch_id].y += centroid.y; + patch_locations[patch_id].z += centroid.z; + patch_triangle_counts[patch_id]++; + } + } + + // Average the accumulated centroids + for (unsigned int p = 0; p < nPatches; ++p) { + if (patch_triangle_counts[p] > 0) { + patch_locations[p].x /= patch_triangle_counts[p]; + patch_locations[p].y /= patch_triangle_counts[p]; + patch_locations[p].z /= patch_triangle_counts[p]; + } + } + + return patch_locations; +} + +// Compute volume, centroid and MOI in CoM frame (unit density). +// ATTENTION: For solid meshes, this assumes a watertight mesh with fine and non-degenerate triangles. +void DEMMesh::ComputeMassProperties(double& volume, float3& center, float3& inertia) const { + float3 inertia_products = make_float3(0, 0, 0); + ComputeMassProperties(volume, center, inertia, inertia_products); +} + +// Compute volume, centroid and full inertia tensor in CoM frame (unit density). +// ATTENTION: Only correct for "watertight" meshes with fine and non-degenerated triangles. +void DEMMesh::ComputeMassProperties(double& volume, float3& center, float3& inertia, float3& inertia_products) const { + if (IsShell() && GetShellThickness() > DEME_TINY_FLOAT) { + const double thickness = static_cast(GetShellThickness()); + double area_total = 0.0; + double mx = 0.0; + double my = 0.0; + double mz = 0.0; + + for (const auto& face : m_face_v_indices) { + const float3& a = m_vertices[face.x]; + const float3& b = m_vertices[face.y]; + const float3& c = m_vertices[face.z]; + const float3 n = cross(b - a, c - a); + const double area = 0.5 * static_cast(length(n)); + if (area <= DEME_TINY_FLOAT) { + continue; + } + area_total += area; + mx += area * static_cast(a.x + b.x + c.x) / 3.0; + my += area * static_cast(a.y + b.y + c.y) / 3.0; + mz += area * static_cast(a.z + b.z + c.z) / 3.0; + } + + if (!(area_total > 0.0) || !std::isfinite(area_total)) { + volume = 0.0; + center = make_float3(0, 0, 0); + inertia = make_float3(0, 0, 0); + inertia_products = make_float3(0, 0, 0); + return; + } + + const double cx = mx / area_total; + const double cy = my / area_total; + const double cz = mz / area_total; + const double vol = area_total * thickness; + + double Ixx = 0.0; + double Iyy = 0.0; + double Izz = 0.0; + double Ixy = 0.0; + double Iyz = 0.0; + double Izx = 0.0; + + for (const auto& face : m_face_v_indices) { + const float3& a = m_vertices[face.x]; + const float3& b = m_vertices[face.y]; + const float3& c = m_vertices[face.z]; + + const float3 n_vec = cross(b - a, c - a); + const double n_len = static_cast(length(n_vec)); + const double area = 0.5 * n_len; + if (area <= DEME_TINY_FLOAT) { + continue; + } + + const double ax = static_cast(a.x), ay = static_cast(a.y), az = static_cast(a.z); + const double bx = static_cast(b.x), by = static_cast(b.y), bz = static_cast(b.z); + const double cxv = static_cast(c.x), cyv = static_cast(c.y), czv = static_cast(c.z); + + const double int_xx = area * ((ax * ax + bx * bx + cxv * cxv) + (ax * bx + ax * cxv + bx * cxv)) / 6.0; + const double int_yy = area * ((ay * ay + by * by + cyv * cyv) + (ay * by + ay * cyv + by * cyv)) / 6.0; + const double int_zz = area * ((az * az + bz * bz + czv * czv) + (az * bz + az * czv + bz * czv)) / 6.0; + const double int_xy = area * ((ax * ay + bx * by + cxv * cyv) / 6.0 + + (ax * by + bx * ay + ax * cyv + cxv * ay + bx * cyv + cxv * by) / 12.0); + const double int_yz = area * ((ay * az + by * bz + cyv * czv) / 6.0 + + (ay * bz + by * az + ay * czv + cyv * az + by * czv + cyv * bz) / 12.0); + const double int_zx = area * ((az * ax + bz * bx + czv * cxv) / 6.0 + + (az * bx + bz * ax + az * cxv + czv * ax + bz * cxv + czv * bx) / 12.0); + + // Mid-surface lamina contribution with areal density = thickness (unit volumetric density). + Ixx += thickness * (int_yy + int_zz); + Iyy += thickness * (int_xx + int_zz); + Izz += thickness * (int_xx + int_yy); + Ixy -= thickness * int_xy; + Iyz -= thickness * int_yz; + Izx -= thickness * int_zx; + + // Through-thickness contribution for centered extrusion along facet normal. + const double nx = static_cast(n_vec.x) / n_len; + const double ny = static_cast(n_vec.y) / n_len; + const double nz = static_cast(n_vec.z) / n_len; + const double coeff = area * thickness * thickness * thickness / 12.0; + Ixx += coeff * (1.0 - nx * nx); + Iyy += coeff * (1.0 - ny * ny); + Izz += coeff * (1.0 - nz * nz); + Ixy += coeff * (-nx * ny); + Iyz += coeff * (-ny * nz); + Izx += coeff * (-nz * nx); + } + + // Shift inertia tensor from origin to center of mass. + Ixx -= vol * (cy * cy + cz * cz); + Iyy -= vol * (cx * cx + cz * cz); + Izz -= vol * (cx * cx + cy * cy); + Ixy += vol * cx * cy; + Iyz += vol * cy * cz; + Izx += vol * cz * cx; + + volume = vol; + center = make_float3(static_cast(cx), static_cast(cy), static_cast(cz)); + inertia = make_float3(static_cast(Ixx), static_cast(Iyy), static_cast(Izz)); + inertia_products = make_float3(static_cast(Ixy), static_cast(Iyz), static_cast(Izx)); + return; + } + + double vol = 0.0; + double mx = 0.0; + double my = 0.0; + double mz = 0.0; + double ix2 = 0.0; + double iy2 = 0.0; + double iz2 = 0.0; + double ixy = 0.0; + double iyz = 0.0; + double izx = 0.0; + + for (const auto& face : m_face_v_indices) { + const float3& a = m_vertices[face.x]; + const float3& b = m_vertices[face.y]; + const float3& c = m_vertices[face.z]; + + const float3 bcross = cross(b, c); + const double v = static_cast(dot(a, bcross)) / 6.0; + + vol += v; + mx += v * (static_cast(a.x) + b.x + c.x) / 4.0; + my += v * (static_cast(a.y) + b.y + c.y) / 4.0; + mz += v * (static_cast(a.z) + b.z + c.z) / 4.0; + + const double ax = a.x, ay = a.y, az = a.z; + const double bx = b.x, by = b.y, bz = b.z; + const double cx = c.x, cy = c.y, cz = c.z; + + const double f1x = ax * ax + bx * bx + cx * cx + ax * bx + bx * cx + cx * ax; + const double f1y = ay * ay + by * by + cy * cy + ay * by + by * cy + cy * ay; + const double f1z = az * az + bz * bz + cz * cz + az * bz + bz * cz + cz * az; + + ix2 += v * f1x / 10.0; + iy2 += v * f1y / 10.0; + iz2 += v * f1z / 10.0; + + const double fxy = + 2.0 * (ax * ay + bx * by + cx * cy) + (ax * by + ay * bx + bx * cy + by * cx + cx * ay + cy * ax); + const double fyz = + 2.0 * (ay * az + by * bz + cy * cz) + (ay * bz + az * by + by * cz + bz * cy + cy * az + cz * ay); + const double fzx = + 2.0 * (az * ax + bz * bx + cz * cx) + (az * bx + ax * bz + bz * cx + bx * cz + cz * ax + cx * az); + + ixy += v * fxy / 20.0; + iyz += v * fyz / 20.0; + izx += v * fzx / 20.0; + } + + if (vol == 0.0) { + volume = 0.0; + center = make_float3(0, 0, 0); + inertia = make_float3(0, 0, 0); + inertia_products = make_float3(0, 0, 0); + return; + } + + if (vol < 0.0) { + vol = -vol; + mx = -mx; + my = -my; + mz = -mz; + ix2 = -ix2; + iy2 = -iy2; + iz2 = -iz2; + ixy = -ixy; + iyz = -iyz; + izx = -izx; + } + + const double cx = mx / vol; + const double cy = my / vol; + const double cz = mz / vol; + + double Ixx = iy2 + iz2; + double Iyy = ix2 + iz2; + double Izz = ix2 + iy2; + double Ixy = -ixy; + double Iyz = -iyz; + double Izx = -izx; + + // Shift to center of mass. + Ixx -= vol * (cy * cy + cz * cz); + Iyy -= vol * (cx * cx + cz * cz); + Izz -= vol * (cx * cx + cy * cy); + Ixy += vol * cx * cy; + Iyz += vol * cy * cz; + Izx += vol * cz * cx; + + volume = vol; + center = make_float3(static_cast(cx), static_cast(cy), static_cast(cz)); + inertia = make_float3(static_cast(Ixx), static_cast(Iyy), static_cast(Izz)); + inertia_products = make_float3(static_cast(Ixy), static_cast(Iyz), static_cast(Izx)); +} + +// Section for Watertight test, false if not + +bool DEMMesh::IsWatertight(size_t* boundary_edges, size_t* nonmanifold_edges) const { + if (boundary_edges) + *boundary_edges = 0; + if (nonmanifold_edges) + *nonmanifold_edges = 0; + if (m_face_v_indices.empty()) + return true; + + auto count_edges_by_index = [&](size_t& boundary, size_t& nonmanifold) { + std::map, size_t> edge_counts; + + for (const auto& face : m_face_v_indices) { + const int fx = face.x, fy = face.y, fz = face.z; + if (fx < 0 || fy < 0 || fz < 0) + continue; + + const size_t a = static_cast(fx); + const size_t b = static_cast(fy); + const size_t c = static_cast(fz); + if (a == b || b == c || c == a) + continue; + + std::pair edges[3] = { + {std::min(a, b), std::max(a, b)}, {std::min(b, c), std::max(b, c)}, {std::min(c, a), std::max(c, a)}}; + edge_counts[edges[0]]++; + edge_counts[edges[1]]++; + edge_counts[edges[2]]++; + } + + boundary = 0; + nonmanifold = 0; + for (const auto& kv : edge_counts) { + if (kv.second == 1) + boundary++; + else if (kv.second > 2) + nonmanifold++; + } + }; + + size_t boundary1 = 0, nonmanifold1 = 0; + count_edges_by_index(boundary1, nonmanifold1); + + if (boundary1 == 0 && nonmanifold1 == 0) { + if (boundary_edges) + *boundary_edges = 0; + if (nonmanifold_edges) + *nonmanifold_edges = 0; + return true; + } + + if (m_vertices.empty()) { + if (boundary_edges) + *boundary_edges = boundary1; + if (nonmanifold_edges) + *nonmanifold_edges = nonmanifold1; + return false; + } + + const double eps = computeVertexQuantEps(m_vertices); + const auto canon = buildCanonicalVertexMap(m_vertices, eps); + + std::map, size_t> edge_counts2; + for (const auto& face : m_face_v_indices) { + const int fx = face.x, fy = face.y, fz = face.z; + if (fx < 0 || fy < 0 || fz < 0) + continue; + + const size_t a0 = static_cast(fx); + const size_t b0 = static_cast(fy); + const size_t c0 = static_cast(fz); + if (a0 >= canon.size() || b0 >= canon.size() || c0 >= canon.size()) + continue; + + const size_t a = canon[a0]; + const size_t b = canon[b0]; + const size_t c = canon[c0]; + if (a == b || b == c || c == a) + continue; + + std::pair edges[3] = { + {std::min(a, b), std::max(a, b)}, {std::min(b, c), std::max(b, c)}, {std::min(c, a), std::max(c, a)}}; + edge_counts2[edges[0]]++; + edge_counts2[edges[1]]++; + edge_counts2[edges[2]]++; + } + + size_t boundary2 = 0, nonmanifold2 = 0; + for (const auto& kv : edge_counts2) { + if (kv.second == 1) + boundary2++; + else if (kv.second > 2) + nonmanifold2++; + } + + if (boundary_edges) + *boundary_edges = boundary2; + if (nonmanifold_edges) + *nonmanifold_edges = nonmanifold2; + return boundary2 == 0 && nonmanifold2 == 0; +} + +} // end namespace deme + +namespace deme { + +bool loadMeshByExtension(DEMMesh& mesh, const std::string& filename, bool load_normals, bool load_uv) { + std::string ext = std::filesystem::path(filename).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); }); + if (ext == ".stl") { + return mesh.LoadSTLMesh(filename, load_normals); + } + if (ext == ".ply") { + return mesh.LoadPLYMesh(filename, load_normals); + } + // Default to OBJ/Wavefront path + return mesh.LoadWavefrontMesh(filename, load_normals, load_uv); +} + +namespace { + +struct EdgeInfo { + size_t tri = 0; + int edge = 0; +}; + +inline uint64_t makeEdgeKey(int a, int b) { + const uint32_t lo = static_cast(std::min(a, b)); + const uint32_t hi = static_cast(std::max(a, b)); + return (static_cast(lo) << 32) | static_cast(hi); +} + +} // namespace + +std::vector> buildTriangleEdgeNeighbors(const std::vector& face_v_indices, + const std::vector& vertices) { + const size_t n_faces = face_v_indices.size(); + std::vector> neighbors(n_faces, {NULL_BODYID, NULL_BODYID, NULL_BODYID}); + if (n_faces == 0) { + return neighbors; + } + + std::vector canon; + if (!vertices.empty()) { + const double eps = computeVertexQuantEps(vertices); + canon = buildCanonicalVertexMap(vertices, eps); + } + + std::unordered_map> edge_map; + edge_map.reserve(n_faces * 3); + + for (size_t i = 0; i < n_faces; ++i) { + const int3& face = face_v_indices[i]; + const int v0_raw = face.x; + const int v1_raw = face.y; + const int v2_raw = face.z; + if (v0_raw < 0 || v1_raw < 0 || v2_raw < 0) { + continue; + } + int v0 = v0_raw; + int v1 = v1_raw; + int v2 = v2_raw; + if (!canon.empty()) { + if (static_cast(v0_raw) >= canon.size() || static_cast(v1_raw) >= canon.size() || + static_cast(v2_raw) >= canon.size()) { + continue; + } + v0 = static_cast(canon[static_cast(v0_raw)]); + v1 = static_cast(canon[static_cast(v1_raw)]); + v2 = static_cast(canon[static_cast(v2_raw)]); + } + if (v0 == v1 || v1 == v2 || v2 == v0) { + continue; + } + const uint64_t e0 = makeEdgeKey(v0, v1); + const uint64_t e1 = makeEdgeKey(v1, v2); + const uint64_t e2 = makeEdgeKey(v2, v0); + edge_map[e0].push_back(EdgeInfo{i, 0}); + edge_map[e1].push_back(EdgeInfo{i, 1}); + edge_map[e2].push_back(EdgeInfo{i, 2}); + } + + for (const auto& entry : edge_map) { + const auto& info = entry.second; + if (info.size() == 2) { + const EdgeInfo& a = info[0]; + const EdgeInfo& b = info[1]; + neighbors[a.tri][a.edge] = static_cast(b.tri); + neighbors[b.tri][b.edge] = static_cast(a.tri); + } + } + + return neighbors; +} + +namespace { + +inline float4 normalizeQuatSafe(const float4& q) { + const double n2 = static_cast(q.x) * q.x + static_cast(q.y) * q.y + static_cast(q.z) * q.z + + static_cast(q.w) * q.w; + if (!(n2 > 0.0) || !std::isfinite(n2)) { + return make_float4(0, 0, 0, 1); + } + const float inv_n = static_cast(1.0 / std::sqrt(n2)); + return make_float4(q.x * inv_n, q.y * inv_n, q.z * inv_n, q.w * inv_n); +} + +inline bool inferUniformScale(const float3& target, const float3& source, double& out_scale) { + constexpr double tiny = 1e-20; + const double src[3] = {source.x, source.y, source.z}; + const double dst[3] = {target.x, target.y, target.z}; + double ratios[3] = {0.0, 0.0, 0.0}; + int n = 0; + for (int i = 0; i < 3; i++) { + if (!std::isfinite(src[i]) || !std::isfinite(dst[i])) { + return false; + } + if (std::abs(src[i]) <= tiny) { + return false; + } + const double r = dst[i] / src[i]; + if (!(r > tiny) || !std::isfinite(r)) { + return false; + } + ratios[n++] = r; + } + if (n != 3) { + return false; + } + const double mean = (ratios[0] + ratios[1] + ratios[2]) / 3.0; + const double denom = std::max(std::abs(mean), tiny); + double max_rel_dev = 0.0; + for (int i = 0; i < 3; i++) { + max_rel_dev = std::max(max_rel_dev, std::abs(ratios[i] - mean) / denom); + } + if (max_rel_dev > 2e-2) { + return false; + } + out_scale = mean; + return true; +} + +inline bool jacobiEigenSymmetric3(const double in_A[3][3], double eigvals[3], double eigvecs[3][3]) { + double A[3][3]; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + A[r][c] = in_A[r][c]; + eigvecs[r][c] = (r == c) ? 1.0 : 0.0; + } + } + + constexpr int max_iters = 32; + constexpr double eps = 1e-18; + for (int it = 0; it < max_iters; it++) { + int p = 0; + int q = 1; + double max_off = std::abs(A[0][1]); + const double off_02 = std::abs(A[0][2]); + const double off_12 = std::abs(A[1][2]); + if (off_02 > max_off) { + p = 0; + q = 2; + max_off = off_02; + } + if (off_12 > max_off) { + p = 1; + q = 2; + max_off = off_12; + } + + const double diag_scale = std::abs(A[0][0]) + std::abs(A[1][1]) + std::abs(A[2][2]) + eps; + if (max_off <= diag_scale * 1e-14) { + break; + } + + const double app = A[p][p]; + const double aqq = A[q][q]; + const double apq = A[p][q]; + if (std::abs(apq) <= eps) { + continue; + } + + const double tau = (aqq - app) / (2.0 * apq); + const double t = + (tau >= 0.0) ? (1.0 / (tau + std::sqrt(1.0 + tau * tau))) : (-1.0 / (-tau + std::sqrt(1.0 + tau * tau))); + const double c = 1.0 / std::sqrt(1.0 + t * t); + const double s = t * c; + + for (int k = 0; k < 3; k++) { + if (k == p || k == q) { + continue; + } + const double aik = A[k][p]; + const double akq = A[k][q]; + A[k][p] = c * aik - s * akq; + A[p][k] = A[k][p]; + A[k][q] = c * akq + s * aik; + A[q][k] = A[k][q]; + } + + A[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq; + A[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq; + A[p][q] = 0.0; + A[q][p] = 0.0; + + for (int k = 0; k < 3; k++) { + const double vkp = eigvecs[k][p]; + const double vkq = eigvecs[k][q]; + eigvecs[k][p] = c * vkp - s * vkq; + eigvecs[k][q] = s * vkp + c * vkq; + } + } + + eigvals[0] = A[0][0]; + eigvals[1] = A[1][1]; + eigvals[2] = A[2][2]; + return std::isfinite(eigvals[0]) && std::isfinite(eigvals[1]) && std::isfinite(eigvals[2]); +} + +inline float4 quatFromRotationMatrix(const double R[3][3]) { + float4 q = make_float4(0, 0, 0, 1); + const double trace = R[0][0] + R[1][1] + R[2][2]; + if (trace > 0.0) { + const double s = std::sqrt(trace + 1.0) * 2.0; + q.w = static_cast(0.25 * s); + q.x = static_cast((R[2][1] - R[1][2]) / s); + q.y = static_cast((R[0][2] - R[2][0]) / s); + q.z = static_cast((R[1][0] - R[0][1]) / s); + } else if (R[0][0] > R[1][1] && R[0][0] > R[2][2]) { + const double s = std::sqrt(1.0 + R[0][0] - R[1][1] - R[2][2]) * 2.0; + q.w = static_cast((R[2][1] - R[1][2]) / s); + q.x = static_cast(0.25 * s); + q.y = static_cast((R[0][1] + R[1][0]) / s); + q.z = static_cast((R[0][2] + R[2][0]) / s); + } else if (R[1][1] > R[2][2]) { + const double s = std::sqrt(1.0 + R[1][1] - R[0][0] - R[2][2]) * 2.0; + q.w = static_cast((R[0][2] - R[2][0]) / s); + q.x = static_cast((R[0][1] + R[1][0]) / s); + q.y = static_cast(0.25 * s); + q.z = static_cast((R[1][2] + R[2][1]) / s); + } else { + const double s = std::sqrt(1.0 + R[2][2] - R[0][0] - R[1][1]) * 2.0; + q.w = static_cast((R[1][0] - R[0][1]) / s); + q.x = static_cast((R[0][2] + R[2][0]) / s); + q.y = static_cast((R[1][2] + R[2][1]) / s); + q.z = static_cast(0.25 * s); + } + return normalizeQuatSafe(q); +} + +} // namespace + +MeshCanonicalizationResult canonicalizeMeshOwnerFrame(DEMMesh& mesh) { + MeshCanonicalizationResult result; + + double volume = 0.0; + float3 center = make_float3(0, 0, 0); + float3 inertia = make_float3(0, 0, 0); + float3 inertia_products = make_float3(0, 0, 0); + mesh.ComputeMassProperties(volume, center, inertia, inertia_products); + if (!(volume > 0.0) || !std::isfinite(volume)) { + return result; + } + + result.center_norm = std::sqrt(center.x * center.x + center.y * center.y + center.z * center.z); + const double diag_norm = std::abs(inertia.x) + std::abs(inertia.y) + std::abs(inertia.z); + const double offdiag_norm = std::sqrt(static_cast(inertia_products.x) * inertia_products.x + + static_cast(inertia_products.y) * inertia_products.y + + static_cast(inertia_products.z) * inertia_products.z); + result.offdiag_rel = offdiag_norm / std::max(diag_norm, 1e-20); + + float bound_r = 0.f; + for (const auto& v : mesh.m_vertices) { + bound_r = std::max(bound_r, length(v)); + } + const double center_tol = std::max(1e-7, 1e-5 * std::max(1e-3, static_cast(bound_r))); + constexpr double offdiag_tol = 1e-6; + if (result.center_norm <= center_tol && result.offdiag_rel <= offdiag_tol) { + return result; + } + + double A[3][3] = { + {inertia.x, inertia_products.x, inertia_products.z}, + {inertia_products.x, inertia.y, inertia_products.y}, + {inertia_products.z, inertia_products.y, inertia.z}, + }; + double eigvals[3] = {0.0, 0.0, 0.0}; + double eigvecs[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + if (!jacobiEigenSymmetric3(A, eigvals, eigvecs)) { + return result; + } + + std::array order = {0, 1, 2}; + std::sort(order.begin(), order.end(), [&](int l, int r) { return eigvals[l] < eigvals[r]; }); + + double R[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + for (int col = 0; col < 3; col++) { + const int src_col = order[col]; + double nrm = 0.0; + for (int row = 0; row < 3; row++) { + R[row][col] = eigvecs[row][src_col]; + nrm += R[row][col] * R[row][col]; + } + nrm = std::sqrt(std::max(nrm, 1e-30)); + for (int row = 0; row < 3; row++) { + R[row][col] /= nrm; + } + } + + const double det = R[0][0] * (R[1][1] * R[2][2] - R[1][2] * R[2][1]) - + R[0][1] * (R[1][0] * R[2][2] - R[1][2] * R[2][0]) + + R[0][2] * (R[1][0] * R[2][1] - R[1][1] * R[2][0]); + if (det < 0.0) { + for (int row = 0; row < 3; row++) { + R[row][2] = -R[row][2]; + } + } + + const float4 principal_q = quatFromRotationMatrix(R); + float3 principal_inertia = make_float3(static_cast(std::max(0.0, eigvals[order[0]])), + static_cast(std::max(0.0, eigvals[order[1]])), + static_cast(std::max(0.0, eigvals[order[2]]))); + + const float3 old_init_pos = mesh.init_pos; + const float4 old_init_ori = mesh.init_oriQ; + + mesh.InformCentroidPrincipal(center, principal_q); + if (mesh.patch_locations_explicitly_set) { + for (auto& p_loc : mesh.m_patch_locations) { + applyFrameTransformGlobalToLocal(p_loc, center, principal_q); + } + } + + float3 new_init_pos = center; + applyFrameTransformLocalToGlobal(new_init_pos, old_init_pos, old_init_ori); + mesh.init_pos = new_init_pos; + mesh.init_oriQ = normalizeQuatSafe(hostHamiltonProduct(old_init_ori, principal_q)); + + double scale_from_old = 0.0; + double scale_from_principal = 0.0; + const bool moi_from_old = inferUniformScale(mesh.MOI, inertia, scale_from_old); + const bool moi_from_principal = inferUniformScale(mesh.MOI, principal_inertia, scale_from_principal); + if (moi_from_old && !moi_from_principal) { + mesh.MOI = principal_inertia * static_cast(scale_from_old); + result.moi_rescaled = true; + } else if (!moi_from_principal && result.offdiag_rel > 1e-4) { + result.moi_inconsistent = true; + } + + result.transformed = true; + return result; +} + +double overlapDuration(double a0, double a1, double b0, double b1) { + const double lo = std::max(a0, b0); + const double hi = std::min(a1, b1); + return (hi > lo) ? (hi - lo) : 0.0; +} + +bool hasPendingWear(const std::vector& pending_depth) { + for (float d : pending_depth) { + if (d > 0.f && std::isfinite(d)) { + return true; + } + } + return false; +} + +} // namespace deme diff --git a/src/DEM/utils/MeshUtils.hpp b/src/DEM/utils/MeshUtils.hpp new file mode 100644 index 00000000..d8c35450 --- /dev/null +++ b/src/DEM/utils/MeshUtils.hpp @@ -0,0 +1,57 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#pragma once + +#include "DEM/Defines.h" + +#include +#include +#include +#include + +namespace deme { + +class DEMMesh; + +struct MeshCanonicalizationResult { + bool transformed = false; + bool moi_rescaled = false; + bool moi_inconsistent = false; + double center_norm = 0.0; + double offdiag_rel = 0.0; +}; + +struct MeshMassJitKey { + size_t template_mark; + float mass; + float moi_x; + float moi_y; + float moi_z; + + bool operator<(const MeshMassJitKey& other) const { + if (template_mark != other.template_mark) + return template_mark < other.template_mark; + if (mass != other.mass) + return mass < other.mass; + if (moi_x != other.moi_x) + return moi_x < other.moi_x; + if (moi_y != other.moi_y) + return moi_y < other.moi_y; + return moi_z < other.moi_z; + } +}; + +bool loadMeshByExtension(DEMMesh& mesh, const std::string& filename, bool load_normals, bool load_uv); + +std::vector> buildTriangleEdgeNeighbors(const std::vector& face_v_indices, + const std::vector& vertices); + +MeshCanonicalizationResult canonicalizeMeshOwnerFrame(DEMMesh& mesh); + +double overlapDuration(double a0, double a1, double b0, double b1); +bool hasPendingWear(const std::vector& pending_depth); + +} // namespace deme diff --git a/src/DEM/utils/Samplers.hpp b/src/DEM/utils/Samplers.hpp index 7456ab50..89f7668d 100644 --- a/src/DEM/utils/Samplers.hpp +++ b/src/DEM/utils/Samplers.hpp @@ -38,8 +38,7 @@ #include #include #include - -#include "../HostSideHelpers.hpp" +#include "HostSideHelpers.hpp" namespace deme { diff --git a/src/DEM/utils/VisualizationData.h b/src/DEM/utils/VisualizationData.h new file mode 100644 index 00000000..aed862cd --- /dev/null +++ b/src/DEM/utils/VisualizationData.h @@ -0,0 +1,71 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_VISUALIZATION_DATA_H +#define DEME_VISUALIZATION_DATA_H + +#include + +#include +#include + +#include "../VariableTypes.h" + +namespace deme { + +/// Geometry in owner-local coordinates. IDs are stable indices in these arrays until the scene revision changes. +struct DEMVisualizationScene { + struct Sphere { + float3 offset; + float radius; + bodyID_t owner; + }; + struct Triangle { + float3 a, b, c; + bodyID_t owner; + }; + std::uint64_t revision = 0; + std::vector spheres; + std::vector triangles; +}; + +/// Compact host frame, indexed by solver owner ID. Geometry is obtained separately and cached by revision. +struct DEMVisualizationFrame { + double simulation_time = 0.0; + std::uint64_t revision = 0; + std::vector positions; + std::vector orientations; + std::vector families; + // Empty unless requested; avoids transferring velocities for family/height coloring. + std::vector velocities; +}; + +/// One sphere component in a host-side visualization snapshot. +struct DEMVisualizationSphere { + float3 position; + float radius; + family_t family; + bodyID_t owner; +}; + +/// One mesh facet in a host-side visualization snapshot. +struct DEMVisualizationTriangle { + float3 a; + float3 b; + float3 c; + family_t family; + bodyID_t owner; +}; + +/// Host-side geometry captured at one solver time for rendering or external visualization. +struct DEMVisualizationSnapshot { + double simulation_time = 0.0; + std::vector spheres; + std::vector triangles; +}; + +} // namespace deme + +#endif diff --git a/src/algorithms/CMakeLists.txt b/src/algorithms/CMakeLists.txt index ff9a88ca..e48c1f0b 100644 --- a/src/algorithms/CMakeLists.txt +++ b/src/algorithms/CMakeLists.txt @@ -15,34 +15,30 @@ target_include_directories( PUBLIC ${ProjectIncludeGenerated} ) -if(USE_HIP) - target_link_libraries( - algorithms - PUBLIC hip::hipcub - ) -else() - target_link_libraries( - algorithms - PUBLIC CUB::CUB - ) -endif() +target_link_libraries( + algorithms + PUBLIC CUB::CUB +) ### HOST HEADERS ONLY (.h, .hpp) ### set(algorithms_interface ${CMAKE_CURRENT_SOURCE_DIR}/DEMStaticDeviceSubroutines.h + ${CMAKE_CURRENT_SOURCE_DIR}/DEMOwnerDataRetrieval.h ) ### INTERNAL HEADERS ONLY (.h, .hpp, or .cuh) ### set(algorithms_headers ${CMAKE_CURRENT_SOURCE_DIR}/DEMStaticDeviceUtilities.cuh + ${CMAKE_CURRENT_SOURCE_DIR}/DEMContactDetectionKernels.cuh ) set(algorithms_sources - ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubWrappers.cu - ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubForceCollection.cu ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubInstantiations.cu - ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubContactDetection.cu + ${CMAKE_CURRENT_SOURCE_DIR}/DEMContactDetection.cu + ${CMAKE_CURRENT_SOURCE_DIR}/DEMCubWrappers.cu ${CMAKE_CURRENT_SOURCE_DIR}/DEMDynamicMisc.cu + ${CMAKE_CURRENT_SOURCE_DIR}/DEMOwnerDataRetrieval.cu + ${CMAKE_CURRENT_SOURCE_DIR}/DEMMiscKernels.cu ) target_sources( @@ -52,36 +48,23 @@ target_sources( PRIVATE ${algorithms_sources} ) -if(USE_HIP) - # Mark .cu sources as HIP language - set_source_files_properties(${algorithms_sources} PROPERTIES LANGUAGE HIP) - - set_target_properties( - algorithms PROPERTIES - POSITION_INDEPENDENT_CODE True - CXX_STANDARD ${CXXSTD_SUPPORTED} - PUBLIC_HEADER "${algorithms_interface}" - HIP_ARCHITECTURES "${CMAKE_HIP_ARCHITECTURES}" - ) -else() - # Get a list of supported architectures - include(../../cmake/CudaSupportedArchitectures.cmake) - cuda_supported_architectures() +# Get a list of supported architectures +include(${CMAKE_SOURCE_DIR}/cmake/CudaSupportedArchitectures.cmake) +cuda_supported_architectures() - # Convert the cuda supported architectures into their respective CMake properties - set(virtual_architectures "") - foreach(arch ${CUDASUP_ARCHITECTURES}) - list(APPEND virtual_architectures "${arch}-virtual") - endforeach() +# Convert the cuda supported architectures into their respective CMake properties +set(virtual_architectures "") +foreach(arch ${CUDASUP_ARCHITECTURES}) + list(APPEND virtual_architectures "${arch}-virtual") +endforeach() - set_target_properties( - algorithms PROPERTIES - POSITION_INDEPENDENT_CODE True - CXX_STANDARD ${CXXSTD_SUPPORTED} - PUBLIC_HEADER "${algorithms_interface}" - CUDA_ARCHITECTURES "${virtual_architectures}" - ) -endif() +set_target_properties( + algorithms PROPERTIES + POSITION_INDEPENDENT_CODE True + CXX_STANDARD ${CXXSTD_SUPPORTED} + PUBLIC_HEADER "${algorithms_interface}" + CUDA_ARCHITECTURES "${virtual_architectures}" +) install( TARGETS algorithms diff --git a/src/algorithms/DEMContactDetection.cu b/src/algorithms/DEMContactDetection.cu new file mode 100644 index 00000000..1aa1f36b --- /dev/null +++ b/src/algorithms/DEMContactDetection.cu @@ -0,0 +1,3129 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include + +#include +#include +#include +#include + +#include + +#include + +namespace deme { + +// Forward declarations (helpers are defined later in this TU) +inline void sortABTypePersistencyByA(bodyID_t* idA, + bodyID_t* idB, + contact_t* types, + notStupidBool_t* persistency, + bodyID_t* idA_sorted, + bodyID_t* idB_sorted, + contact_t* type_sorted, + notStupidBool_t* persistency_sorted, + size_t numCnts, + cudaStream_t& stream, + DEMSolverScratchData& scratchPad); + +inline void primitiveContactArraysResize(size_t nContactPairs, + DualArray& idPrimitiveA, + DualArray& idPrimitiveB, + DualArray& contactTypePrimitive, + DualArray& contactPersistency, + DualStruct& granData) { + // Note these resizing are automatically on kT's device + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPrimitiveA, nContactPairs); + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPrimitiveB, nContactPairs); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactTypePrimitive, nContactPairs); + + // In the case of user-loaded contacts, if the persistency array is not long enough then we have to manually + // extend it. + DEME_DUAL_ARRAY_RESIZE(contactPersistency, nContactPairs, CONTACT_NOT_PERSISTENT); + + // Re-packing pointers now is automatic + + // It's safe to toDevice even though kT is working now and dT may write to its buffer + // This is because all buffer arrays are not used in kernels so their pointers are only meaningfully stored on host, + // so writing from host to device won't change the destination where dT writes + granData.toDevice(); +} + +inline void patchArraysResize(size_t nPatchInvolvedContacts, + DualArray& idA, + DualArray& idB, + DualArray& contactTypePatch, + DualArray& contactPatchIsland, + DualStruct& granData) { + // Note these resizing are automatically on kT's device + DEME_DUAL_ARRAY_RESIZE_NOVAL(idA, nPatchInvolvedContacts); + DEME_DUAL_ARRAY_RESIZE_NOVAL(idB, nPatchInvolvedContacts); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactTypePatch, nPatchInvolvedContacts); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactPatchIsland, nPatchInvolvedContacts); + + // Re-packing pointers now is automatic + + // It's safe to toDevice even though kT is working now and dT may write to its buffer + // This is because all buffer arrays are not used in kernels so their pointers are only meaningfully stored on host, + // so writing from host to device won't change the destination where dT writes + granData.toDevice(); +} + +inline void primitiveHistoryArraysResize(size_t nContactPairs, + DualArray& previous_idPrimitiveA, + DualArray& previous_idPrimitiveB, + DualArray& previous_contactTypePrimitive, + DualArray& previous_primitivePatchIsland, + DualStruct& granData) { + // Snapshot every primitive contact's stable patch-island label for the next kT step. + DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idPrimitiveA, nContactPairs); + DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idPrimitiveB, nContactPairs); + DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_contactTypePrimitive, nContactPairs); + DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_primitivePatchIsland, nContactPairs); + granData.toDevice(); +} + +inline void removeNullPrimitiveContacts(DualStruct& granData, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + const size_t numTotalCnts = *scratchPad.numPrimitiveContacts; + if (numTotalCnts == 0) { + return; + } + + // kT kernels can emit NOT_A_CONTACT sentinels into fixed-size primitive report slots. Those records are useful + // while filling candidate arrays, but they are not physical contacts and their idA/idB values are not valid + // patch/history identities. Compact them out before any patch-contact derivation or history mapping can observe + // them. + const size_t blocks_needed = (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + notStupidBool_t* keep_flags = (notStupidBool_t*)scratchPad.allocateTempVector( + "non_null_primitive_keep_flags", numTotalCnts * sizeof(notStupidBool_t)); + markPhysicalPrimitiveContacts<<>>( + keep_flags, granData->contactTypePrimitive, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + + scratchPad.allocateDualStruct("numNonNullPrimitiveContacts"); + cubDEMSum(keep_flags, scratchPad.getDualStructDevice("numNonNullPrimitiveContacts"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numNonNullPrimitiveContacts"); + const size_t numKept = *scratchPad.getDualStructHost("numNonNullPrimitiveContacts"); + + if (numKept < numTotalCnts) { + const size_t alloc_kept = DEME_MAX((size_t)1, numKept); + const size_t ids_bytes = alloc_kept * sizeof(bodyID_t); + const size_t type_bytes = alloc_kept * sizeof(contact_t); + const size_t persist_bytes = alloc_kept * sizeof(notStupidBool_t); + bodyID_t* keptA = (bodyID_t*)scratchPad.allocateTempVector("non_null_primitive_idA", ids_bytes); + bodyID_t* keptB = (bodyID_t*)scratchPad.allocateTempVector("non_null_primitive_idB", ids_bytes); + contact_t* keptType = (contact_t*)scratchPad.allocateTempVector("non_null_primitive_type", type_bytes); + notStupidBool_t* keptPersist = + (notStupidBool_t*)scratchPad.allocateTempVector("non_null_primitive_persist", persist_bytes); + + cubDEMSelectFlagged(granData->idPrimitiveA, keptA, keep_flags, + scratchPad.getDualStructDevice("numNonNullPrimitiveContacts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged(granData->idPrimitiveB, keptB, keep_flags, + scratchPad.getDualStructDevice("numNonNullPrimitiveContacts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged(granData->contactTypePrimitive, keptType, keep_flags, + scratchPad.getDualStructDevice("numNonNullPrimitiveContacts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged( + granData->contactPersistency, keptPersist, keep_flags, + scratchPad.getDualStructDevice("numNonNullPrimitiveContacts"), numTotalCnts, this_stream, scratchPad); + + if (numKept > 0) { + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(granData->idPrimitiveA, keptA, ids_bytes, cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(granData->idPrimitiveB, keptB, ids_bytes, cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactTypePrimitive, keptType, type_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactPersistency, keptPersist, persist_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + } + + *scratchPad.numPrimitiveContacts = numKept; + scratchPad.finishUsingTempVector("non_null_primitive_idA"); + scratchPad.finishUsingTempVector("non_null_primitive_idB"); + scratchPad.finishUsingTempVector("non_null_primitive_type"); + scratchPad.finishUsingTempVector("non_null_primitive_persist"); + } + + scratchPad.finishUsingDualStruct("numNonNullPrimitiveContacts"); + scratchPad.finishUsingTempVector("non_null_primitive_keep_flags"); +} + +inline void removeDuplicateContacts(DualStruct& granData, + bodyID_t* idA_sorted, + bodyID_t* idB_sorted, + contact_t* contactType_sorted, + notStupidBool_t* persistency_sorted, + DualArray& idPrimitiveA, + DualArray& idPrimitiveB, + DualArray& contactTypePrimitive, + DualArray& contactPersistency, + bool process_persistency, + bodyID_t safe_entity_count, + size_t numTotalCnts, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + (void)safe_entity_count; + if (numTotalCnts == 0) { + return; + } + + // Old approach used an O(k^2) duplicate check per idA group. Under heavy mesh contact, k can be large enough to + // dominate contact detection. Sort contacts to make duplicates adjacent, then keep the first entry per + // (idA, idB, contactType) triple. If requested, persistent contacts sort before non-persistent duplicates. + const size_t blocks_needed = (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + + const size_t key_bytes = numTotalCnts * sizeof(unsigned long long); + unsigned long long* key_in = (unsigned long long*)scratchPad.allocateTempVector("dedup_key_in", key_bytes); + unsigned long long* key_sorted = (unsigned long long*)scratchPad.allocateTempVector("dedup_key_sorted", key_bytes); + + const size_t idx_bytes = numTotalCnts * sizeof(contactPairs_t); + contactPairs_t* idx = (contactPairs_t*)scratchPad.allocateTempVector("dedup_idx", idx_bytes); + contactPairs_t* idx_sorted = (contactPairs_t*)scratchPad.allocateTempVector("dedup_idx_sorted", idx_bytes); + + const size_t ids_bytes = numTotalCnts * sizeof(bodyID_t); + const size_t types_bytes = numTotalCnts * sizeof(contact_t); + const size_t pers_bytes = numTotalCnts * sizeof(notStupidBool_t); + bodyID_t* idA_byB = (bodyID_t*)scratchPad.allocateTempVector("dedup_idA_byB", ids_bytes); + bodyID_t* idB_byB = (bodyID_t*)scratchPad.allocateTempVector("dedup_idB_byB", ids_bytes); + contact_t* type_byB = (contact_t*)scratchPad.allocateTempVector("dedup_type_byB", types_bytes); + notStupidBool_t* pers_byB = (notStupidBool_t*)scratchPad.allocateTempVector("dedup_persist_byB", pers_bytes); + + if (blocks_needed > 0) { + buildKeyBTypePersist<<>>( + idB_sorted, contactType_sorted, persistency_sorted, key_in, numTotalCnts, process_persistency); + DEME_GPU_DEBUG_SYNC(this_stream); + lineNumbers<<>>(idx, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + cubDEMSortByKeys(key_in, key_sorted, idx, idx_sorted, numTotalCnts, this_stream, + scratchPad); + if (blocks_needed > 0) { + gatherByIndex<<>>( + idA_sorted, idA_byB, idx_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex<<>>( + idB_sorted, idB_byB, idx_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex<<>>( + contactType_sorted, type_byB, idx_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex<<>>( + persistency_sorted, pers_byB, idx_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + sortABTypePersistencyByA(idA_byB, idB_byB, type_byB, pers_byB, idA_sorted, idB_sorted, contactType_sorted, + persistency_sorted, numTotalCnts, this_stream, scratchPad); + + size_t retain_flags_size = numTotalCnts * sizeof(notStupidBool_t); + notStupidBool_t* retain_flags = (notStupidBool_t*)scratchPad.allocateTempVector("retain_flags", retain_flags_size); + if (blocks_needed > 0) { + markUniqueTriples<<>>( + idA_sorted, idB_sorted, contactType_sorted, retain_flags, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Then remove redundency based on the flag array... + // Note the contactPersistency array is managed by the current contact arr. It will also be copied over. + scratchPad.allocateDualStruct("numRetainedCnts"); + cubDEMSum(retain_flags, scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, + this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numRetainedCnts"); + size_t* pNumRetainedCnts = scratchPad.getDualStructHost("numRetainedCnts"); + // DEME_DEBUG_PRINTF("Found %zu contacts, including user-specified persistent contacts.", + // *pNumRetainedCnts); + // Potentially need to resize the contact arrays + if (*pNumRetainedCnts > idPrimitiveA.size()) { + primitiveContactArraysResize(*pNumRetainedCnts, idPrimitiveA, idPrimitiveB, contactTypePrimitive, + contactPersistency, granData); + } + // Then select those needed contacts + cubDEMSelectFlagged(idA_sorted, granData->idPrimitiveA, retain_flags, + scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, + this_stream, scratchPad); + cubDEMSelectFlagged(idB_sorted, granData->idPrimitiveB, retain_flags, + scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, + this_stream, scratchPad); + cubDEMSelectFlagged(contactType_sorted, granData->contactTypePrimitive, retain_flags, + scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, + this_stream, scratchPad); + if (process_persistency) { + cubDEMSelectFlagged( + persistency_sorted, granData->contactPersistency, retain_flags, + scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, this_stream, scratchPad); + } + scratchPad.syncDualStructDeviceToHost( + "numRetainedCnts"); // In theory no need, but when CONTACT_IS_PERSISTENT is not 1... + // DEME_DEBUG_PRINTF("CUB confirms there are %zu contacts, including user-specified persistent + // contacts.", *pNumRetainedCnts); + // std::cout << "Contacts after duplication check: " << std::endl; + // displayDeviceArray(granData->idPrimitiveA, *pNumRetainedCnts); + // displayDeviceArray(granData->idPrimitiveB, *pNumRetainedCnts); + // displayDeviceArray(granData->contactTypePrimitive, *pNumRetainedCnts); + // displayDeviceArray(granData->contactPersistency, *pNumRetainedCnts); + + // And update the number of contacts. + *scratchPad.numPrimitiveContacts = *pNumRetainedCnts; + scratchPad.finishUsingDualStruct("numRetainedCnts"); + + // Unclaim all temp vectors + scratchPad.finishUsingTempVector("retain_flags"); + scratchPad.finishUsingTempVector("dedup_key_in"); + scratchPad.finishUsingTempVector("dedup_key_sorted"); + scratchPad.finishUsingTempVector("dedup_idx"); + scratchPad.finishUsingTempVector("dedup_idx_sorted"); + scratchPad.finishUsingTempVector("dedup_idA_byB"); + scratchPad.finishUsingTempVector("dedup_idB_byB"); + scratchPad.finishUsingTempVector("dedup_type_byB"); + scratchPad.finishUsingTempVector("dedup_persist_byB"); +} + +inline void sortABTypePersistencyByA(bodyID_t* idA, + bodyID_t* idB, + contact_t* types, + notStupidBool_t* persistency, + bodyID_t* idA_sorted, + bodyID_t* idB_sorted, + contact_t* type_sorted, + notStupidBool_t* persistency_sorted, + size_t numCnts, + cudaStream_t& stream, + DEMSolverScratchData& scratchPad) { + const size_t idx_bytes = numCnts * sizeof(contactPairs_t); + contactPairs_t* idx = (contactPairs_t*)scratchPad.allocateTempVector("sortAB_idx", idx_bytes); + contactPairs_t* idx_sorted = (contactPairs_t*)scratchPad.allocateTempVector("sortAB_idx_sorted", idx_bytes); + + const size_t blocks_needed = (numCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + lineNumbers<<>>(idx, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + } + cubDEMSortByKeys(idA, idA_sorted, idx, idx_sorted, numCnts, stream, scratchPad); + + if (blocks_needed > 0) { + gatherByIndex<<>>( + idB, idB_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + gatherByIndex<<>>( + types, type_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + gatherByIndex<<>>( + persistency, persistency_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + } + + scratchPad.finishUsingTempVector("sortAB_idx"); + scratchPad.finishUsingTempVector("sortAB_idx_sorted"); +} + +inline void sortABTypePersistencyByType(bodyID_t* idA, + bodyID_t* idB, + contact_t* types, + notStupidBool_t* persistency, + bodyID_t* idA_sorted, + bodyID_t* idB_sorted, + contact_t* type_sorted, + notStupidBool_t* persistency_sorted, + size_t numCnts, + cudaStream_t& stream, + DEMSolverScratchData& scratchPad) { + const size_t idx_bytes = numCnts * sizeof(contactPairs_t); + contactPairs_t* idx = (contactPairs_t*)scratchPad.allocateTempVector("sortType_idx", idx_bytes); + contactPairs_t* idx_sorted = (contactPairs_t*)scratchPad.allocateTempVector("sortType_idx_sorted", idx_bytes); + + const size_t blocks_needed = (numCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + lineNumbers<<>>(idx, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + } + cubDEMSortByKeys(types, type_sorted, idx, idx_sorted, numCnts, stream, scratchPad); + + if (blocks_needed > 0) { + gatherByIndex<<>>( + idA, idA_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + gatherByIndex<<>>( + idB, idB_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + gatherByIndex<<>>( + persistency, persistency_sorted, idx_sorted, numCnts); + DEME_GPU_DEBUG_SYNC(stream); + } + + scratchPad.finishUsingTempVector("sortType_idx"); + scratchPad.finishUsingTempVector("sortType_idx_sorted"); +} + +inline void sortPrimitiveContactsByTypeInPlace(DualStruct& granData, + size_t numCnts, + cudaStream_t& stream, + DEMSolverScratchData& scratchPad) { + if (numCnts == 0) { + return; + } + + const size_t total_ids_bytes = numCnts * sizeof(bodyID_t); + const size_t total_types_bytes = numCnts * sizeof(contact_t); + const size_t total_persistency_bytes = numCnts * sizeof(notStupidBool_t); + contact_t* contactType_sorted = (contact_t*)scratchPad.allocateTempVector("contactType_sorted", total_types_bytes); + bodyID_t* idPrimitiveA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idPrimitiveA_sorted", total_ids_bytes); + bodyID_t* idPrimitiveB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idPrimitiveB_sorted", total_ids_bytes); + notStupidBool_t* contactPersistency_sorted = + (notStupidBool_t*)scratchPad.allocateTempVector("contactPersistency_sorted", total_persistency_bytes); + + sortABTypePersistencyByType(granData->idPrimitiveA, granData->idPrimitiveB, granData->contactTypePrimitive, + granData->contactPersistency, idPrimitiveA_sorted, idPrimitiveB_sorted, + contactType_sorted, contactPersistency_sorted, numCnts, stream, scratchPad); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(granData->idPrimitiveA, idPrimitiveA_sorted, total_ids_bytes, cudaMemcpyDeviceToDevice, stream), + stream); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(granData->idPrimitiveB, idPrimitiveB_sorted, total_ids_bytes, cudaMemcpyDeviceToDevice, stream), + stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactTypePrimitive, contactType_sorted, total_types_bytes, + cudaMemcpyDeviceToDevice, stream), + stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactPersistency, contactPersistency_sorted, + total_persistency_bytes, cudaMemcpyDeviceToDevice, stream), + stream); + + scratchPad.finishUsingTempVector("contactType_sorted"); + scratchPad.finishUsingTempVector("idPrimitiveA_sorted"); + scratchPad.finishUsingTempVector("idPrimitiveB_sorted"); + scratchPad.finishUsingTempVector("contactPersistency_sorted"); +} + +inline bool primitiveTypeRunsAreFragmented(contact_t* uniqueTypes, size_t numTypes) { + constexpr size_t maxKnownContactTypeRuns = (size_t)NUM_SUPPORTED_CONTACT_TYPES; + if (numTypes > maxKnownContactTypeRuns) { + return true; + } + + contact_t hostTypes[maxKnownContactTypeRuns]; + if (numTypes > 0) { + DEME_GPU_CALL(cudaMemcpy(hostTypes, uniqueTypes, numTypes * sizeof(contact_t), cudaMemcpyDeviceToHost)); + } + for (size_t i = 0; i < numTypes; i++) { + for (size_t j = i + 1; j < numTypes; j++) { + if (hostTypes[i] == hostTypes[j]) { + return true; + } + } + } + return false; +} + +inline void stabilizeFloodedPatchIslandIDs(DualStruct& granData, + DualArray& previous_primitivePatchIsland, + size_t numCurrentPrimitiveContacts, + size_t numCurrentPatchContacts, + size_t numPreviousPrimitiveContacts, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + if (numCurrentPrimitiveContacts == 0 || numCurrentPatchContacts == 0 || numPreviousPrimitiveContacts == 0 || + previous_primitivePatchIsland.size() < numPreviousPrimitiveContacts) { + return; + } + + const size_t prev_key_bytes = numPreviousPrimitiveContacts * sizeof(uint64_t); + uint64_t* prevKeyHi = (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyHi", prev_key_bytes); + uint64_t* prevKeyLo = (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyLo", prev_key_bytes); + uint64_t* prevKeyLoSorted = (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyLoSorted", prev_key_bytes); + uint64_t* prevKeyHiByLo = (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyHiByLo", prev_key_bytes); + uint64_t* prevKeyHiSorted = (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyHiSorted", prev_key_bytes); + uint64_t* prevKeyLoSortedByHi = + (uint64_t*)scratchPad.allocateTempVector("stablePrevKeyLoSortedByHi", prev_key_bytes); + + const size_t prev_idx_bytes = numPreviousPrimitiveContacts * sizeof(contactPairs_t); + contactPairs_t* prevIdx = (contactPairs_t*)scratchPad.allocateTempVector("stablePrevIdx", prev_idx_bytes); + contactPairs_t* prevIdxSorted = + (contactPairs_t*)scratchPad.allocateTempVector("stablePrevIdxSorted", prev_idx_bytes); + contactPairs_t* prevByLoIdx = (contactPairs_t*)scratchPad.allocateTempVector("stablePrevByLoIdx", prev_idx_bytes); + contactPairs_t* prevByLoIdxSorted = + (contactPairs_t*)scratchPad.allocateTempVector("stablePrevByLoIdxSorted", prev_idx_bytes); + + bodyID_t* prevIslandByLo = (bodyID_t*)scratchPad.allocateTempVector( + "stablePrevIslandByLo", numPreviousPrimitiveContacts * sizeof(bodyID_t)); + bodyID_t* prevIslandSorted = (bodyID_t*)scratchPad.allocateTempVector( + "stablePrevIslandSorted", numPreviousPrimitiveContacts * sizeof(bodyID_t)); + + size_t prev_blocks = (numPreviousPrimitiveContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + buildPrimitiveContactKeyParts<<>>( + granData->previous_idPrimitiveA, granData->previous_idPrimitiveB, granData->previous_contactTypePrimitive, + prevKeyHi, prevKeyLo, numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + lineNumbers<<>>(prevIdx, + numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + cubDEMSortByKeys(prevKeyLo, prevKeyLoSorted, prevIdx, prevIdxSorted, + numPreviousPrimitiveContacts, this_stream, scratchPad); + gatherByIndex<<>>( + prevKeyHi, prevKeyHiByLo, prevIdxSorted, numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex<<>>( + previous_primitivePatchIsland.data(), prevIslandByLo, prevIdxSorted, numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + + lineNumbers<<>>(prevByLoIdx, + numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + cubDEMSortByKeys(prevKeyHiByLo, prevKeyHiSorted, prevByLoIdx, prevByLoIdxSorted, + numPreviousPrimitiveContacts, this_stream, scratchPad); + gatherByIndex<<>>( + prevKeyLoSorted, prevKeyLoSortedByHi, prevByLoIdxSorted, numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex<<>>( + prevIslandByLo, prevIslandSorted, prevByLoIdxSorted, numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + + const size_t curr_vote_bytes = numCurrentPrimitiveContacts * sizeof(uint64_t); + uint64_t* voteKeysAll = (uint64_t*)scratchPad.allocateTempVector("stableVoteKeysAll", curr_vote_bytes); + uint64_t* voteKeys = (uint64_t*)scratchPad.allocateTempVector("stableVoteKeys", curr_vote_bytes); + notStupidBool_t* voteFlags = (notStupidBool_t*)scratchPad.allocateTempVector( + "stableVoteFlags", numCurrentPrimitiveContacts * sizeof(notStupidBool_t)); + scratchPad.allocateDualStruct("numStableVotes"); + + size_t curr_blocks = (numCurrentPrimitiveContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + buildStableIslandVotes<<>>( + granData->idPrimitiveA, granData->idPrimitiveB, granData->contactTypePrimitive, granData->geomToPatchMap, + prevKeyHiSorted, prevKeyLoSortedByHi, prevIslandSorted, voteKeysAll, voteFlags, numCurrentPrimitiveContacts, + numPreviousPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + cubDEMSelectFlagged(voteKeysAll, voteKeys, voteFlags, + scratchPad.getDualStructDevice("numStableVotes"), + numCurrentPrimitiveContacts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numStableVotes"); + size_t numStableVotes = *scratchPad.getDualStructHost("numStableVotes"); + + scratchPad.finishUsingTempVector("stableVoteFlags"); + scratchPad.finishUsingTempVector("stableVoteKeysAll"); + scratchPad.finishUsingTempVector("stablePrevIslandSorted"); + scratchPad.finishUsingTempVector("stablePrevIslandByLo"); + scratchPad.finishUsingTempVector("stablePrevByLoIdxSorted"); + scratchPad.finishUsingTempVector("stablePrevByLoIdx"); + scratchPad.finishUsingTempVector("stablePrevIdxSorted"); + scratchPad.finishUsingTempVector("stablePrevIdx"); + scratchPad.finishUsingTempVector("stablePrevKeyLoSortedByHi"); + scratchPad.finishUsingTempVector("stablePrevKeyHiSorted"); + scratchPad.finishUsingTempVector("stablePrevKeyHiByLo"); + scratchPad.finishUsingTempVector("stablePrevKeyLoSorted"); + scratchPad.finishUsingTempVector("stablePrevKeyLo"); + scratchPad.finishUsingTempVector("stablePrevKeyHi"); + + if (numStableVotes > 0) { + uint64_t* voteKeysSorted = + (uint64_t*)scratchPad.allocateTempVector("stableVoteKeysSorted", numStableVotes * sizeof(uint64_t)); + uint64_t* uniqueVoteKeys = + (uint64_t*)scratchPad.allocateTempVector("stableUniqueVoteKeys", numStableVotes * sizeof(uint64_t)); + contactPairs_t* voteCounts = + (contactPairs_t*)scratchPad.allocateTempVector("stableVoteCounts", numStableVotes * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numUniqueStableVotes"); + cubDEMSortKeys(voteKeys, voteKeysSorted, numStableVotes, this_stream, scratchPad); + cubDEMRunLengthEncode(voteKeysSorted, uniqueVoteKeys, voteCounts, + scratchPad.getDualStructDevice("numUniqueStableVotes"), + numStableVotes, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueStableVotes"); + size_t numUniqueStableVotes = *scratchPad.getDualStructHost("numUniqueStableVotes"); + + scratchPad.finishUsingTempVector("stableVoteKeys"); + scratchPad.finishUsingTempVector("stableVoteKeysSorted"); + + unsigned long long* bestPacked = (unsigned long long*)scratchPad.allocateTempVector( + "stableBestPacked", numCurrentPatchContacts * sizeof(unsigned long long)); + DEME_GPU_CALL_ASYNC( + cudaMemsetAsync(bestPacked, 0, numCurrentPatchContacts * sizeof(unsigned long long), this_stream), + this_stream); + if (numUniqueStableVotes > 0) { + size_t unique_blocks = (numUniqueStableVotes + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + scatterBestStableIslandVote<<>>( + uniqueVoteKeys, voteCounts, bestPacked, numUniqueStableVotes); + DEME_GPU_DEBUG_SYNC(this_stream); + size_t patch_blocks = + (numCurrentPatchContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + applyBestStableIslandVotes<<>>( + bestPacked, granData->contactPatchIsland, numCurrentPatchContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + scratchPad.finishUsingTempVector("stableBestPacked"); + scratchPad.finishUsingTempVector("stableUniqueVoteKeys"); + scratchPad.finishUsingTempVector("stableVoteCounts"); + scratchPad.finishUsingDualStruct("numUniqueStableVotes"); + } else { + scratchPad.finishUsingTempVector("stableVoteKeys"); + } + + scratchPad.finishUsingDualStruct("numStableVotes"); +} + +DEME_KERNEL void markContactsByCombinedOwnerMask(const bodyID_t* idA, + const bodyID_t* idB, + const contact_t* type, + const bodyID_t* ownerClumpBody, + const bodyID_t* ownerTriMesh, + const bodyID_t* ownerAnalBody, + const bodyID_t* ownerCombinedMaster, + notStupidBool_t* keepFlags, + size_t n, + bodyID_t nSpheresGM, + bodyID_t nTriGM, + objID_t nAnalGM, + bodyID_t nOwnerBodies) { + contactPairs_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) { + return; + } + const contact_t thisType = type[i]; + if (!isSupportedContactType(thisType) || ownerCombinedMaster == nullptr) { + keepFlags[i] = 1; + return; + } + + const geoType_t typeA = decodeTypeA(thisType); + const geoType_t typeB = decodeTypeB(thisType); + const bodyID_t primA = idA[i]; + const bodyID_t primB = idB[i]; + bodyID_t ownerA = NULL_BODYID; + bodyID_t ownerB = NULL_BODYID; + if (typeA == GEO_T_SPHERE && primA < nSpheresGM) { + ownerA = ownerClumpBody[primA]; + } else if (typeA == GEO_T_TRIANGLE && primA < nTriGM) { + ownerA = ownerTriMesh[primA]; + } else if (typeA == GEO_T_ANALYTICAL && primA < nAnalGM) { + ownerA = ownerAnalBody[primA]; + } + if (typeB == GEO_T_SPHERE && primB < nSpheresGM) { + ownerB = ownerClumpBody[primB]; + } else if (typeB == GEO_T_TRIANGLE && primB < nTriGM) { + ownerB = ownerTriMesh[primB]; + } else if (typeB == GEO_T_ANALYTICAL && primB < nAnalGM) { + ownerB = ownerAnalBody[primB]; + } + if (ownerA == NULL_BODYID || ownerB == NULL_BODYID || ownerA >= nOwnerBodies || ownerB >= nOwnerBodies) { + // If owner decoding fails, keep the contact to avoid accidental under-detection. + keepFlags[i] = 1; + return; + } + + const bodyID_t masterA = ownerCombinedMaster[ownerA]; + const bodyID_t masterB = ownerCombinedMaster[ownerB]; + if (masterA >= nOwnerBodies || masterB >= nOwnerBodies) { + keepFlags[i] = 1; + return; + } + keepFlags[i] = (masterA != NULL_BODYID && masterA == masterB) ? 0 : 1; +} + +void contactDetection(std::shared_ptr& bin_sphere_kernels, + std::shared_ptr& bin_triangle_kernels, + std::shared_ptr& sphere_contact_kernels, + std::shared_ptr& sphTri_contact_kernels, + DualStruct& granData, + DualStruct& simParams, + SolverFlags& solverFlags, + verbosity_t& verbosity, + DualArray& idPrimitiveA, + DualArray& idPrimitiveB, + DualArray& contactTypePrimitive, + DualArray& previous_idPrimitiveA, + DualArray& previous_idPrimitiveB, + DualArray& previous_contactTypePrimitive, + DualArray& previous_primitivePatchIsland, + DualArray& contactPersistency, + DualArray& contactMapping, + // NEW: Separate patch ID arrays and mapping + DualArray& idPatchA, + DualArray& idPatchB, + DualArray& previous_idPatchA, + DualArray& previous_idPatchB, + DualArray& contactTypePatch, + DualArray& previous_contactTypePatch, + DualArray& contactPatchIsland, + DualArray& previous_contactPatchIsland, + ContactTypeMap>& typeStartCountPatchMap, + DualArray& geomToPatchMap, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad, + SolverTimers& timers, + kTStateParams& stateParams) { + // A dumb check + if (simParams->nSpheresGM == 0 && simParams->nTriGM == 0) { + *scratchPad.numContacts = 0; + *scratchPad.numPrimitiveContacts = 0; + *scratchPad.numPrevContacts = 0; + *scratchPad.numPrevPrimitiveContacts = 0; + *scratchPad.numPrevSpheres = 0; + *scratchPad.numPrevTriangles = 0; + *scratchPad.numPrevMeshPatches = 0; + + scratchPad.numContacts.toDevice(); + scratchPad.numPrimitiveContacts.toDevice(); + scratchPad.numPrevContacts.toDevice(); + scratchPad.numPrevPrimitiveContacts.toDevice(); + scratchPad.numPrevSpheres.toDevice(); + scratchPad.numPrevTriangles.toDevice(); + scratchPad.numPrevMeshPatches.toDevice(); + return; + } + // These are needed for the solver to keep tab... But you know, we may have no triangles or no contacts, so + // initializing them is needed. + stateParams.maxSphFoundInBin = 0; + stateParams.maxTriFoundInBin = 0; + stateParams.avgCntsPerPrimitive = 0; + + // A special flag that marks if the contact arrays are sorted based on type. At the initial generation stage, they + // are indeed generated type by type + bool primitiveContactArraysAreSortedByType = true; + + // total bytes needed for temp arrays in contact detection + size_t CD_temp_arr_bytes = 0; + + // Contact counts + size_t nSphereSphereContact = 0, nTriSphereContact = 0, nTriTriContact = 0, nSphereGeoContact = 0, + nTriGeoContact = 0; + + // Track patch contact start/count per type (for the new redesigned mapping approach) + ContactTypeMap> typeStartCountPatchMap_thisStep; + typeStartCountPatchMap_thisStep.SetAll({0, 0}); + + { + timers.GetTimer("Discretize domain").start(); + // ----------------------------------------------------------------------------------------------------------- + // Sphere-related discretization & sphere--analytical contact detection + // ----------------------------------------------------------------------------------------------------------- + + // If there are spheres, the following information needs to be extracted and kept... + scratchPad.allocateDualStruct("numActiveBins"); + *scratchPad.getDualStructHost("numActiveBins") = 0; // Need a default value on host for no-sphere case + size_t* pNumActiveBins = scratchPad.getDualStructHost("numActiveBins"); // Can later be host or device pointers + bodyID_t* sphereIDsEachBinTouches_sorted; + binID_t* activeBinIDs; + spheresBinTouches_t* numSpheresBinTouches; + binSphereTouchPairs_t* sphereIDsLookUpTable; + if (simParams->nSpheresGM > 0) { + // 1st step: register the number of sphere--bin touching pairs for each sphere for further processing + CD_temp_arr_bytes = simParams->nSpheresGM * sizeof(binsSphereTouches_t); + binsSphereTouches_t* numBinsSphereTouches = + (binsSphereTouches_t*)scratchPad.allocateTempVector("numBinsSphereTouches", CD_temp_arr_bytes); + // This kernel is also tasked to find how many analytical objects each sphere touches + // We'll use a new vector 2 to store this + CD_temp_arr_bytes = simParams->nSpheresGM * sizeof(objID_t); + objID_t* numAnalGeoSphereTouches = + (objID_t*)scratchPad.allocateTempVector("numAnalGeoSphereTouches", CD_temp_arr_bytes); + size_t blocks_needed_for_bodies = + (simParams->nSpheresGM + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; + + bin_sphere_kernels->kernel("getNumberOfBinsEachSphereTouches") + .instantiate() + .configure(dim3(blocks_needed_for_bodies), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, numBinsSphereTouches, numAnalGeoSphereTouches); + DEME_GPU_DEBUG_SYNC(this_stream); + + // 2nd step: prefix scan sphere--bin touching pairs + // The last element of this scanned array is useful: it can be used to check if the 2 sweeps reach the same + // conclusion on bin--sph touch pairs + CD_temp_arr_bytes = (simParams->nSpheresGM + 1) * sizeof(binSphereTouchPairs_t); + binSphereTouchPairs_t* numBinsSphereTouchesScan = + (binSphereTouchPairs_t*)scratchPad.allocateTempVector("numBinsSphereTouchesScan", CD_temp_arr_bytes); + cubDEMPrefixScan( + numBinsSphereTouches, numBinsSphereTouchesScan, simParams->nSpheresGM, this_stream, scratchPad); + // If there are temp variables that need both device and host copies, we just create DualStruct on-spot + scratchPad.allocateDualStruct("numBinSphereTouchPairs"); + size_t* pNumBinSphereTouchPairs = scratchPad.getDualStructDevice("numBinSphereTouchPairs"); + deviceAdd( + pNumBinSphereTouchPairs, &(numBinsSphereTouchesScan[simParams->nSpheresGM - 1]), + &(numBinsSphereTouches[simParams->nSpheresGM - 1]), this_stream); + deviceAssign(&(numBinsSphereTouchesScan[simParams->nSpheresGM]), + pNumBinSphereTouchPairs, this_stream); + // numBinSphereTouchPairs stores data on device, but now we need it on host, so a migrantion is needed. This + // cannot be forgotten. + scratchPad.syncDualStructDeviceToHost("numBinSphereTouchPairs"); + // Now pNumBinSphereTouchPairs is host pointer and exclusively used on host + pNumBinSphereTouchPairs = scratchPad.getDualStructHost("numBinSphereTouchPairs"); + // The same process is done for sphere--analytical geometry pairs as well. + // One extra elem is used for storing the final elem in scan result. + CD_temp_arr_bytes = (simParams->nSpheresGM + 1) * sizeof(binSphereTouchPairs_t); + binSphereTouchPairs_t* numAnalGeoSphereTouchesScan = + (binSphereTouchPairs_t*)scratchPad.allocateTempVector("numAnalGeoSphereTouchesScan", CD_temp_arr_bytes); + cubDEMPrefixScan(numAnalGeoSphereTouches, numAnalGeoSphereTouchesScan, + simParams->nSpheresGM, this_stream, scratchPad); + deviceAdd( + &(numAnalGeoSphereTouchesScan[simParams->nSpheresGM]), + &(numAnalGeoSphereTouches[simParams->nSpheresGM - 1]), + &(numAnalGeoSphereTouchesScan[simParams->nSpheresGM - 1]), this_stream); + deviceAssign( + &(scratchPad.numPrimitiveContacts), &(numAnalGeoSphereTouchesScan[simParams->nSpheresGM]), this_stream); + // numContact is updated (with geo--sphere pair number), get it to host + scratchPad.numPrimitiveContacts.toHost(); + nSphereGeoContact = *scratchPad.numPrimitiveContacts; + if (*scratchPad.numPrimitiveContacts > idPrimitiveA.size()) { + primitiveContactArraysResize(*(scratchPad.numPrimitiveContacts), idPrimitiveA, idPrimitiveB, + contactTypePrimitive, contactPersistency, granData); + } + // std::cout << *pNumBinSphereTouchPairs << std::endl; + // displayDeviceArray(numBinsSphereTouches, simParams->nSpheresGM); + // displayDeviceArray(numBinsSphereTouchesScan, simParams->nSpheresGM); + + // 3rd step: use a custom kernel to figure out all sphere--bin touching pairs. Note numBinsSphereTouches can + // retire now. + scratchPad.finishUsingTempVector("numBinsSphereTouches"); + scratchPad.finishUsingTempVector("numAnalGeoSphereTouches"); + + CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(binID_t); + binID_t* binIDsEachSphereTouches = + (binID_t*)scratchPad.allocateTempVector("binIDsEachSphereTouches", CD_temp_arr_bytes); + CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(bodyID_t); + bodyID_t* sphereIDsEachBinTouches = + (bodyID_t*)scratchPad.allocateTempVector("sphereIDsEachBinTouches", CD_temp_arr_bytes); + // This kernel is also responsible of figuring out sphere--analytical geometry pairs + bin_sphere_kernels->kernel("populateBinSphereTouchingPairs") + .instantiate() + .configure(dim3(blocks_needed_for_bodies), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, numBinsSphereTouchesScan, numAnalGeoSphereTouchesScan, + binIDsEachSphereTouches, sphereIDsEachBinTouches, granData->idPrimitiveA, + granData->idPrimitiveB, granData->contactTypePrimitive); + DEME_GPU_DEBUG_SYNC(this_stream); + // std::cout << "Unsorted bin IDs: "; + // displayDeviceArray(binIDsEachSphereTouches, *pNumBinSphereTouchPairs); + // std::cout << "Corresponding sphere IDs: "; + // displayDeviceArray(sphereIDsEachBinTouches, *pNumBinSphereTouchPairs); + + // 4th step: allocate and populate SORTED binIDsEachSphereTouches and sphereIDsEachBinTouches. Note + // numBinsSphereTouchesScan can retire now (analytical contacts have been processed). + scratchPad.finishUsingTempVector("numBinsSphereTouchesScan"); + scratchPad.finishUsingTempVector("numAnalGeoSphereTouchesScan"); + CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(bodyID_t); + sphereIDsEachBinTouches_sorted = + (bodyID_t*)scratchPad.allocateTempVector("sphereIDsEachBinTouches_sorted", CD_temp_arr_bytes); + CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(binID_t); + binID_t* binIDsEachSphereTouches_sorted = + (binID_t*)scratchPad.allocateTempVector("binIDsEachSphereTouches_sorted", CD_temp_arr_bytes); + // hostSortByKey(granData->binIDsEachSphereTouches, granData->sphereIDsEachBinTouches, + // *pNumBinSphereTouchPairs); + cubDEMSortByKeys(binIDsEachSphereTouches, binIDsEachSphereTouches_sorted, + sphereIDsEachBinTouches, sphereIDsEachBinTouches_sorted, + *pNumBinSphereTouchPairs, this_stream, scratchPad); + // std::cout << "Sorted bin IDs: "; + // displayDeviceArray(binIDsEachSphereTouches_sorted, *pNumBinSphereTouchPairs); + // std::cout << "Corresponding sphere IDs: "; + // displayDeviceArray(sphereIDsEachBinTouches_sorted, *pNumBinSphereTouchPairs); + + // 5th step: use DeviceRunLengthEncode to identify those active (that have bodies in them) bins. + // Also, binIDsEachSphereTouches is large enough for a unique scan because total sphere--bin pairs are more + // than active bins. + binID_t* binIDsUnique = (binID_t*)binIDsEachSphereTouches; + pNumActiveBins = scratchPad.getDualStructDevice("numActiveBins"); + cubDEMUnique(binIDsEachSphereTouches_sorted, binIDsUnique, pNumActiveBins, + *pNumBinSphereTouchPairs, this_stream, scratchPad); + // Allocate space for encoding output, and run it. Note the (unsorted) binIDsEachSphereTouches and + // sphereIDsEachBinTouches can retire now. + scratchPad.finishUsingTempVector("binIDsEachSphereTouches"); + scratchPad.finishUsingTempVector("sphereIDsEachBinTouches"); + // Get the unique check result to host + scratchPad.syncDualStructDeviceToHost("numActiveBins"); + pNumActiveBins = scratchPad.getDualStructHost("numActiveBins"); + CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binID_t); + // This activeBinIDs will need some host treatment later on... + scratchPad.allocateDualArray("activeBinIDs", CD_temp_arr_bytes); + activeBinIDs = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDs"); + CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(spheresBinTouches_t); + numSpheresBinTouches = + (spheresBinTouches_t*)scratchPad.allocateTempVector("numSpheresBinTouches", CD_temp_arr_bytes); + // Here you don't have to toHost() again as the runlength should give the same numActiveBins as before + pNumActiveBins = scratchPad.getDualStructDevice("numActiveBins"); + cubDEMRunLengthEncode(binIDsEachSphereTouches_sorted, activeBinIDs, + numSpheresBinTouches, pNumActiveBins, + *pNumBinSphereTouchPairs, this_stream, scratchPad); + pNumActiveBins = scratchPad.getDualStructHost("numActiveBins"); + // std::cout << "numActiveBins: " << *pNumActiveBins << std::endl; + // std::cout << "activeBinIDs: "; + // displayDeviceArray(activeBinIDs, *pNumActiveBins); + // std::cout << "numSpheresBinTouches: "; + // displayDeviceArray(numSpheresBinTouches, *pNumActiveBins); + // std::cout << "binIDsEachSphereTouches_sorted: "; + // displayDeviceArray(binIDsEachSphereTouches_sorted, *pNumBinSphereTouchPairs); + scratchPad.finishUsingDualStruct("numBinSphereTouchPairs"); + + // We find the max geo num in a bin for the purpose of adjusting bin size. + scratchPad.allocateDualStruct("maxGeoInBin"); + spheresBinTouches_t* pMaxGeoInBin = (spheresBinTouches_t*)scratchPad.getDualStructDevice("maxGeoInBin"); + cubDEMMax(numSpheresBinTouches, pMaxGeoInBin, *pNumActiveBins, this_stream, + scratchPad); + scratchPad.syncDualStructDeviceToHost("maxGeoInBin"); + // Hmm... this only works in little-endian systems... I don't use undefined behavior that often but this + // one... + stateParams.maxSphFoundInBin = *((spheresBinTouches_t*)scratchPad.getDualStructHost("maxGeoInBin")); + scratchPad.finishUsingDualStruct("maxGeoInBin"); + + // Then, scan to find the offsets that are used to index into sphereIDsEachBinTouches_sorted to obtain + // bin-wise spheres. Note binIDsEachSphereTouches_sorted can retire. + scratchPad.finishUsingTempVector("binIDsEachSphereTouches_sorted"); + CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binSphereTouchPairs_t); + sphereIDsLookUpTable = + (binSphereTouchPairs_t*)scratchPad.allocateTempVector("sphereIDsLookUpTable", CD_temp_arr_bytes); + cubDEMPrefixScan(numSpheresBinTouches, sphereIDsLookUpTable, + *pNumActiveBins, this_stream, scratchPad); + // std::cout << "sphereIDsLookUpTable: "; + // displayDeviceArray(sphereIDsLookUpTable, *pNumActiveBins); + } + + // ----------------------------------------------------------------------------------------------------------- + // Triangle-related discretization and triangle--analytical contact detection + // ----------------------------------------------------------------------------------------------------------- + + // If there are meshes, they need to be processed too + scratchPad.allocateDualStruct("numActiveBinsForTri"); + *scratchPad.getDualStructHost("numActiveBinsForTri") = + 0; // May need a default value on host for no-triangle case + size_t* pNumActiveBinsForTri; // Can be host or device pointers + bodyID_t* triIDsEachBinTouches_sorted; + trianglesBinTouches_t* numTrianglesBinTouches; + binsTriangleTouchPairs_t* triIDsLookUpTable; + float3 *tri_vA1 = nullptr, *tri_vB1 = nullptr, *tri_vC1 = nullptr, *tri_shift = nullptr; + float3 *sandwichANode1, *sandwichANode2, *sandwichANode3, *sandwichBNode1; + // The following two pointers are device pointers, as outside this (simParams->nTriGM > 0) block, they are only + // used as read-only ingredients in kernels. But their data is stored using DualArray, as the data need at some + // point be processed on host. + binID_t *mapTriActBinToSphActBin, *activeBinIDsForTri; + if (simParams->nTriGM > 0) { + // 0-th step: Make `sandwich' for each triangle (or say, create a prism out of each triangle). This is + // obviously for our delayed contact detection safety. And finally, if a sphere's distance away from one of + // the 2 prism surfaces is smaller than its radius, it has contact with this prism, hence potentially with + // this triangle. + CD_temp_arr_bytes = simParams->nTriGM * sizeof(float3) * 3; + sandwichANode1 = (float3*)scratchPad.allocateTempVector("sandwichANode1", CD_temp_arr_bytes); + sandwichANode2 = sandwichANode1 + simParams->nTriGM; + sandwichANode3 = sandwichANode2 + simParams->nTriGM; + sandwichBNode1 = + (float3*)scratchPad.allocateTempVector("sandwichBNode1", simParams->nTriGM * sizeof(float3)); + size_t blocks_needed_for_tri = + (simParams->nTriGM + DEME_NUM_TRIANGLE_PER_BLOCK - 1) / DEME_NUM_TRIANGLE_PER_BLOCK; + bin_triangle_kernels->kernel("makeTriangleSandwich") + .instantiate() + .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1); + + // 1st step: register the number of triangle--bin touching pairs for each triangle for further processing. + // We also use the opportunity to find how many analytical objects each triangle touches. + CD_temp_arr_bytes = simParams->nTriGM * sizeof(binsTriangleTouches_t); + binsTriangleTouches_t* numBinsTriTouches = + (binsTriangleTouches_t*)scratchPad.allocateTempVector("numBinsTriTouches", CD_temp_arr_bytes); + objID_t* numAnalGeoTriTouches = nullptr; + if (solverFlags.meshUniversalContact) { + CD_temp_arr_bytes = simParams->nTriGM * sizeof(objID_t); + numAnalGeoTriTouches = + (objID_t*)scratchPad.allocateTempVector("numAnalGeoTriTouches", CD_temp_arr_bytes); + } + + // Triangle prepass: compute world vertices/bounds/shift once, reuse in both sweeps. + CD_temp_arr_bytes = simParams->nTriGM * sizeof(float3); + tri_vA1 = (float3*)scratchPad.allocateTempVector("tri_vA1", CD_temp_arr_bytes); + tri_vB1 = (float3*)scratchPad.allocateTempVector("tri_vB1", CD_temp_arr_bytes); + tri_vC1 = (float3*)scratchPad.allocateTempVector("tri_vC1", CD_temp_arr_bytes); + tri_shift = (float3*)scratchPad.allocateTempVector("tri_shift", CD_temp_arr_bytes); + + CD_temp_arr_bytes = simParams->nTriGM * sizeof(int3); + int3* tri_L1 = (int3*)scratchPad.allocateTempVector("tri_L1", CD_temp_arr_bytes); + int3* tri_U1 = (int3*)scratchPad.allocateTempVector("tri_U1", CD_temp_arr_bytes); + int3* tri_L2 = (int3*)scratchPad.allocateTempVector("tri_L2", CD_temp_arr_bytes); + int3* tri_U2 = (int3*)scratchPad.allocateTempVector("tri_U2", CD_temp_arr_bytes); + + CD_temp_arr_bytes = simParams->nTriGM * sizeof(uint8_t); + uint8_t* tri_ok1 = (uint8_t*)scratchPad.allocateTempVector("tri_ok1", CD_temp_arr_bytes); + uint8_t* tri_ok2 = (uint8_t*)scratchPad.allocateTempVector("tri_ok2", CD_temp_arr_bytes); + + // Precompute mesh-owner pose (position + rotation matrix rows) once per step (mesh owners only). + float3 *meshOwnerPos = nullptr, *meshR1 = nullptr, *meshR2 = nullptr, *meshR3 = nullptr; + if (simParams->nTriMeshes > 0) { + CD_temp_arr_bytes = simParams->nTriMeshes * sizeof(float3); + meshOwnerPos = (float3*)scratchPad.allocateTempVector("meshOwnerPos", CD_temp_arr_bytes); + meshR1 = (float3*)scratchPad.allocateTempVector("meshR1", CD_temp_arr_bytes); + meshR2 = (float3*)scratchPad.allocateTempVector("meshR2", CD_temp_arr_bytes); + meshR3 = (float3*)scratchPad.allocateTempVector("meshR3", CD_temp_arr_bytes); + size_t blocks_needed_for_mesh_owner = + (simParams->nTriMeshes + DEME_NUM_TRIANGLE_PER_BLOCK - 1) / DEME_NUM_TRIANGLE_PER_BLOCK; + bin_triangle_kernels->kernel("precomputeMeshOwnerPose") + .instantiate() + .configure(dim3(blocks_needed_for_mesh_owner), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, meshOwnerPos, meshR1, meshR2, meshR3); + } + + bin_triangle_kernels->kernel("precomputeTriangleSandwichData") + .instantiate() + .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, tri_vA1, tri_vB1, tri_vC1, tri_shift, tri_L1, tri_U1, tri_L2, tri_U2, + tri_ok1, tri_ok2, meshOwnerPos, meshR1, meshR2, meshR3, sandwichANode1, sandwichANode2, + sandwichANode3, sandwichBNode1); + + if (meshOwnerPos) { + scratchPad.finishUsingTempVector("meshOwnerPos"); + scratchPad.finishUsingTempVector("meshR1"); + scratchPad.finishUsingTempVector("meshR2"); + scratchPad.finishUsingTempVector("meshR3"); + } + + // Sandwich nodes are no longer needed beyond prepass. + scratchPad.finishUsingTempVector("sandwichANode1"); + scratchPad.finishUsingTempVector("sandwichBNode1"); + + bin_triangle_kernels->kernel("getNumberOfBinsEachTriangleTouches") + .instantiate() + .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, numBinsTriTouches, numAnalGeoTriTouches, tri_vA1, tri_vB1, tri_vC1, + tri_shift, tri_L1, tri_U1, tri_L2, tri_U2, tri_ok1, tri_ok2, solverFlags.meshUniversalContact); + // std::cout << "numBinsTriTouches: " << std::endl; + // displayDeviceArray(numBinsTriTouches, simParams->nTriGM); + // std::cout << "numAnalGeoTriTouches: " << std::endl; + // displayDeviceArray(numAnalGeoTriTouches, simParams->nTriGM); + + // 2nd step: prefix scan tri--bin touching pairs + // The last element of this scanned array is useful: it can be used to check if the 2 sweeps reach the same + // conclusion on bin--tri touch pairs + CD_temp_arr_bytes = (simParams->nTriGM + 1) * sizeof(binsTriangleTouchPairs_t); + binsTriangleTouchPairs_t* numBinsTriTouchesScan = + (binsTriangleTouchPairs_t*)scratchPad.allocateTempVector("numBinsTriTouchesScan", CD_temp_arr_bytes); + cubDEMPrefixScan( + numBinsTriTouches, numBinsTriTouchesScan, simParams->nTriGM, this_stream, scratchPad); + scratchPad.allocateDualStruct("numBinTriTouchPairs"); + size_t* pNumBinTriTouchPairs = scratchPad.getDualStructDevice("numBinTriTouchPairs"); + deviceAdd( + pNumBinTriTouchPairs, &(numBinsTriTouchesScan[simParams->nTriGM - 1]), + &(numBinsTriTouches[simParams->nTriGM - 1]), this_stream); + deviceAssign(&(numBinsTriTouchesScan[simParams->nTriGM]), + pNumBinTriTouchPairs, this_stream); + scratchPad.syncDualStructDeviceToHost("numBinTriTouchPairs"); + pNumBinTriTouchPairs = scratchPad.getDualStructHost("numBinTriTouchPairs"); + // Again, numBinsTriTouchesScan is used in populateBinTriangleTouchingPairs + + binsTriangleTouchPairs_t* numAnalGeoTriTouchesScan = nullptr; + if (solverFlags.meshUniversalContact) { + // Now, the same process is done for tri--analytical geometry pairs as well. + // One extra elem is used for storing the final elem in scan result. + CD_temp_arr_bytes = (simParams->nTriGM + 1) * sizeof(binsTriangleTouchPairs_t); + numAnalGeoTriTouchesScan = (binsTriangleTouchPairs_t*)scratchPad.allocateTempVector( + "numAnalGeoTriTouchesScan", CD_temp_arr_bytes); + cubDEMPrefixScan(numAnalGeoTriTouches, numAnalGeoTriTouchesScan, + simParams->nTriGM, this_stream, scratchPad); + // numPrimitiveContacts is for temp use, and it got overwritten in this step + deviceAdd( + &(numAnalGeoTriTouchesScan[simParams->nTriGM]), &(numAnalGeoTriTouches[simParams->nTriGM - 1]), + &(numAnalGeoTriTouchesScan[simParams->nTriGM - 1]), this_stream); + deviceAssign( + &(scratchPad.numPrimitiveContacts), &(numAnalGeoTriTouchesScan[simParams->nTriGM]), this_stream); + // numContact is updated (with geo--sphere pair number), get it to host + scratchPad.numPrimitiveContacts.toHost(); + nTriGeoContact = *scratchPad.numPrimitiveContacts; + // But we still want numPrimitiveContacts to keep the running length of contacts, so overwrite it; no + // worry as numPrimitiveContacts is host-major, so no need to update device + *scratchPad.numPrimitiveContacts = nSphereGeoContact + nTriGeoContact; + if (*scratchPad.numPrimitiveContacts > idPrimitiveA.size()) { + primitiveContactArraysResize(*(scratchPad.numPrimitiveContacts), idPrimitiveA, idPrimitiveB, + contactTypePrimitive, contactPersistency, granData); + } + // std::cout << "numAnalGeoTriTouchesScan: " << std::endl; + // displayDeviceArray(numAnalGeoTriTouchesScan, simParams->nTriGM); + } + + // 3rd step: use a custom kernel to figure out all tri--bin touching pairs. Note numBinsTriTouches can + // retire now. + scratchPad.finishUsingTempVector("numBinsTriTouches"); + scratchPad.finishUsingTempVector("numAnalGeoTriTouches"); + + CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(binID_t); + binID_t* binIDsEachTriTouches = + (binID_t*)scratchPad.allocateTempVector("binIDsEachTriTouches", CD_temp_arr_bytes); + CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(bodyID_t); + bodyID_t* triIDsEachBinTouches = + (bodyID_t*)scratchPad.allocateTempVector("triIDsEachBinTouches", CD_temp_arr_bytes); + // Tri--geo contact pairs go after sphere--anal-geo contacts + bodyID_t* idTriA = (granData->idPrimitiveA + nSphereGeoContact); + bodyID_t* idGeoB = (granData->idPrimitiveB + nSphereGeoContact); + contact_t* dType = (granData->contactTypePrimitive + nSphereGeoContact); + bin_triangle_kernels->kernel("populateBinTriangleTouchingPairs") + .instantiate() + .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, numBinsTriTouchesScan, numAnalGeoTriTouchesScan, binIDsEachTriTouches, + triIDsEachBinTouches, tri_vA1, tri_vB1, tri_vC1, tri_shift, tri_L1, tri_U1, tri_L2, tri_U2, + tri_ok1, tri_ok2, idTriA, idGeoB, dType, solverFlags.meshUniversalContact); + + scratchPad.finishUsingTempVector("tri_L1"); + scratchPad.finishUsingTempVector("tri_U1"); + scratchPad.finishUsingTempVector("tri_L2"); + scratchPad.finishUsingTempVector("tri_U2"); + scratchPad.finishUsingTempVector("tri_ok1"); + scratchPad.finishUsingTempVector("tri_ok2"); + // std::cout << "binIDsEachTriTouches: " << std::endl; + // displayDeviceArray(binIDsEachTriTouches, *pNumBinTriTouchPairs); + // std::cout << "dType: " << std::endl; + // displayDeviceArray(dType, nTriGeoContact); + // std::cout << "mesh patch pairs:" << std::endl; + // displayDeviceArray(patchPairs, nTriGeoContact); + + // 4th step: allocate and populate SORTED binIDsEachTriTouches and triIDsEachBinTouches. Note + // numBinsTriTouchesScan can retire now (analytical contacts also processed). + scratchPad.finishUsingTempVector("numBinsTriTouchesScan"); + scratchPad.finishUsingTempVector("numAnalGeoTriTouchesScan"); + CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(bodyID_t); + triIDsEachBinTouches_sorted = + (bodyID_t*)scratchPad.allocateTempVector("triIDsEachBinTouches_sorted", CD_temp_arr_bytes); + CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(binID_t); + binID_t* binIDsEachTriTouches_sorted = + (binID_t*)scratchPad.allocateTempVector("binIDsEachTriTouches_sorted", CD_temp_arr_bytes); + cubDEMSortByKeys(binIDsEachTriTouches, binIDsEachTriTouches_sorted, triIDsEachBinTouches, + triIDsEachBinTouches_sorted, *pNumBinTriTouchPairs, this_stream, + scratchPad); + + // 5th step: use DeviceRunLengthEncode to identify those active (that have tris in them) bins. + // Also, binIDsEachTriTouches is large enough for a unique scan because total sphere--bin pairs are more + // than active bins. + binID_t* binIDsUnique = (binID_t*)binIDsEachTriTouches; + pNumActiveBinsForTri = scratchPad.getDualStructDevice("numActiveBinsForTri"); + cubDEMUnique(binIDsEachTriTouches_sorted, binIDsUnique, pNumActiveBinsForTri, + *pNumBinTriTouchPairs, this_stream, scratchPad); + // Allocate space for encoding output, and run it. Note the (unsorted) binIDsEachTriTouches and + // triIDsEachBinTouches can retire now. + scratchPad.finishUsingTempVector("binIDsEachTriTouches"); + scratchPad.finishUsingTempVector("triIDsEachBinTouches"); + // Bring value to host + scratchPad.syncDualStructDeviceToHost("numActiveBinsForTri"); + pNumActiveBinsForTri = scratchPad.getDualStructHost("numActiveBinsForTri"); + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binID_t); + // Again, activeBinIDsForTri has some data processing needed on host, so allocated as DualArray + scratchPad.allocateDualArray("activeBinIDsForTri", CD_temp_arr_bytes); + activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDsForTri"); + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(trianglesBinTouches_t); + numTrianglesBinTouches = + (trianglesBinTouches_t*)scratchPad.allocateTempVector("numTrianglesBinTouches", CD_temp_arr_bytes); + // Again, no need to bring numActiveBinsForTri to host again, as values not changed + pNumActiveBinsForTri = scratchPad.getDualStructDevice("numActiveBinsForTri"); + cubDEMRunLengthEncode(binIDsEachTriTouches_sorted, activeBinIDsForTri, + numTrianglesBinTouches, pNumActiveBinsForTri, + *pNumBinTriTouchPairs, this_stream, scratchPad); + pNumActiveBinsForTri = scratchPad.getDualStructHost("numActiveBinsForTri"); + // std::cout << "activeBinIDsForTri: " << std::endl; + // displayDeviceArray(activeBinIDsForTri, *pNumActiveBinsForTri); + // std::cout << "NumActiveBinsForTri: " << *pNumActiveBinsForTri << std::endl; + // std::cout << "NumActiveBins: " << *pNumActiveBins << std::endl; + scratchPad.finishUsingDualStruct("numBinTriTouchPairs"); + + // We find the max tri num in a bin for the purpose of adjusting bin size + scratchPad.allocateDualStruct("maxGeoInBin"); + trianglesBinTouches_t* pMaxGeoInBin = (trianglesBinTouches_t*)scratchPad.getDualStructDevice("maxGeoInBin"); + cubDEMMax(numTrianglesBinTouches, pMaxGeoInBin, *pNumActiveBinsForTri, this_stream, + scratchPad); + scratchPad.syncDualStructDeviceToHost("maxGeoInBin"); + // Find the max tri number. Hmm... this only works in little-endian systems... I don't use undefined + // behavior that often but this one... + stateParams.maxTriFoundInBin = *((trianglesBinTouches_t*)scratchPad.getDualStructHost("maxGeoInBin")); + scratchPad.finishUsingDualStruct("maxGeoInBin"); + + // 6th step: map activeBinIDsForTri to activeBinIDs, so that when we are processing the bins in + // activeBinIDsForTri, we know where to find the corresponding bin that resides in activeBinIDs, to bring + // spheres into this bin-wise contact detection sweep. + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binID_t); + scratchPad.allocateDualArray("mapTriActBinToSphActBin", CD_temp_arr_bytes); + { + // This `merge search' task is very unsuitable for GPU... So we create host version of these work arrays + // temporarily and use them. Don't worry about the temporary part, as it's only in logic, no actual + // memory allocation, as vectorPool handles it. + scratchPad.syncDualArrayDeviceToHost("activeBinIDsForTri"); + binID_t* activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayHost("activeBinIDsForTri"); + // There has to be spheres for activeBinIDs to exist, so we have to check + binID_t* activeBinIDs = nullptr; + if (scratchPad.existDualArray("activeBinIDs")) { + scratchPad.syncDualArrayDeviceToHost("activeBinIDs"); + activeBinIDs = (binID_t*)scratchPad.getDualArrayHost("activeBinIDs"); + } + binID_t* mapTriActBinToSphActBin = (binID_t*)scratchPad.getDualArrayHost("mapTriActBinToSphActBin"); + if (activeBinIDs && *pNumActiveBins > 0) { + hostMergeSearchMapGen(activeBinIDsForTri, activeBinIDs, mapTriActBinToSphActBin, + *pNumActiveBinsForTri, *pNumActiveBins, deme::NULL_BINID); + } else { + // No active sphere bins this step: every tri-active bin maps to NULL. + std::fill_n(mapTriActBinToSphActBin, *pNumActiveBinsForTri, deme::NULL_BINID); + } + // activeBinIDsForTri and activeBinIDs are not changed, so no need to sync them back + scratchPad.syncDualArrayHostToDevice("mapTriActBinToSphActBin"); + } + // Bind back some of the device pointers is not necessary as they are not changed + // But it's needed for mapTriActBinToSphActBin as it's its first assignment... + mapTriActBinToSphActBin = (binID_t*)scratchPad.getDualArrayDevice("mapTriActBinToSphActBin"); + // activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDsForTri"); + // activeBinIDs = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDs"); + // std::cout << "mapTriActBinToSphActBin: " << std::endl; + // displayDeviceArray(mapTriActBinToSphActBin, *pNumActiveBinsForTri); + + // 7th step: scan to find the offsets that are used to index into triIDsEachBinTouches_sorted to obtain + // bin-wise triangles. Note binIDsEachTriTouches_sorted can retire. + scratchPad.finishUsingTempVector("binIDsEachTriTouches_sorted"); + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binsTriangleTouchPairs_t); + triIDsLookUpTable = + (binsTriangleTouchPairs_t*)scratchPad.allocateTempVector("triIDsLookUpTable", CD_temp_arr_bytes); + cubDEMPrefixScan( + numTrianglesBinTouches, triIDsLookUpTable, *pNumActiveBinsForTri, this_stream, scratchPad); + } + timers.GetTimer("Discretize domain").stop(); + + // ----------------------------------------------------------------------------------------------------------- + // Populating contact pairs + // ----------------------------------------------------------------------------------------------------------- + + timers.GetTimer("Find contact pairs").start(); + // Final step: find the contact pairs. One-two punch: first find num of contacts in each bin, then prescan, then + // find the actual pair names. A new temp array is needed for this numSphContactsInEachBin. Note that + // binContactPairs_t also doubles as the type for the number of tri--sph and tri--tri contact pairs. + binContactPairs_t *numSphContactsInEachBin, *numTriSphContactsInEachBin, *numTriTriContactsInEachBin = nullptr; + // Figure out how many blocks are needed for the contact detection, with one block per bin. + size_t blocks_needed_for_bins_sph = 0; + if (simParams->nSpheresGM > 0) { + blocks_needed_for_bins_sph = *pNumActiveBins; + CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binContactPairs_t); + numSphContactsInEachBin = + (binContactPairs_t*)scratchPad.allocateTempVector("numSphContactsInEachBin", CD_temp_arr_bytes); + } + size_t blocks_needed_for_bins_tri = 0; + if (simParams->nTriGM > 0) { + blocks_needed_for_bins_tri = *pNumActiveBinsForTri; + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binContactPairs_t); + numTriSphContactsInEachBin = + (binContactPairs_t*)scratchPad.allocateTempVector("numTriSphContactsInEachBin", CD_temp_arr_bytes); + // Naturally, for tri--tri contacts, only active bins for tri are needed + if (solverFlags.meshUniversalContact) { + CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binContactPairs_t); + numTriTriContactsInEachBin = + (binContactPairs_t*)scratchPad.allocateTempVector("numTriTriContactsInEachBin", CD_temp_arr_bytes); + } + } + + // Only needed when there are spheres or triangles + if (blocks_needed_for_bins_sph > 0 || blocks_needed_for_bins_tri > 0) { + if (blocks_needed_for_bins_sph > 0) { + sphere_contact_kernels->kernel("getNumberOfSphereContactsEachBin") + .instantiate() + .configure(dim3(blocks_needed_for_bins_sph), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, + sphereIDsLookUpTable, numSphContactsInEachBin, *pNumActiveBins); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + if (blocks_needed_for_bins_tri > 0) { + // We got both tri--sph and tri--tri contacts in this kernel + sphTri_contact_kernels->kernel("getNumberOfTriangleContactsEachBin") + .instantiate() + .configure(dim3(blocks_needed_for_bins_tri), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, + sphereIDsLookUpTable, mapTriActBinToSphActBin, triIDsEachBinTouches_sorted, + activeBinIDsForTri, numTrianglesBinTouches, triIDsLookUpTable, numTriSphContactsInEachBin, + numTriTriContactsInEachBin, tri_vA1, tri_vB1, tri_vC1, tri_shift, *pNumActiveBinsForTri, + solverFlags.meshUniversalContact); + DEME_GPU_DEBUG_SYNC(this_stream); + // std::cout << "numTriSphContactsInEachBin: " << std::endl; + // displayDeviceArray(numTriSphContactsInEachBin, *pNumActiveBinsForTri); + // std::cout << "numTriTriContactsInEachBin: " << std::endl; + // displayDeviceArray(numTriTriContactsInEachBin, *pNumActiveBinsForTri); + } + + // Prescan numSphContactsInEachBin to get the final sphSphContactReportOffsets and + // triSphContactReportOffsets. New vectors are needed. + // The extra entry is maybe superfluous and is for extra safety, in case the 2 sweeps do not agree with each + // other. + contactPairs_t* sphSphContactReportOffsets; + if (simParams->nSpheresGM > 0) { + CD_temp_arr_bytes = (*pNumActiveBins + 1) * sizeof(contactPairs_t); + sphSphContactReportOffsets = + (contactPairs_t*)scratchPad.allocateTempVector("sphSphContactReportOffsets", CD_temp_arr_bytes); + cubDEMPrefixScan(numSphContactsInEachBin, sphSphContactReportOffsets, + *pNumActiveBins, this_stream, scratchPad); + } + contactPairs_t *triSphContactReportOffsets, *triTriContactReportOffsets = nullptr; + if (simParams->nTriGM > 0) { + CD_temp_arr_bytes = (*pNumActiveBinsForTri + 1) * sizeof(contactPairs_t); + triSphContactReportOffsets = + (contactPairs_t*)scratchPad.allocateTempVector("triSphContactReportOffsets", CD_temp_arr_bytes); + cubDEMPrefixScan(numTriSphContactsInEachBin, + triSphContactReportOffsets, *pNumActiveBinsForTri, + this_stream, scratchPad); + if (solverFlags.meshUniversalContact) { + // tri--tri contact report offsets... + CD_temp_arr_bytes = (*pNumActiveBinsForTri + 1) * sizeof(contactPairs_t); + triTriContactReportOffsets = + (contactPairs_t*)scratchPad.allocateTempVector("triTriContactReportOffsets", CD_temp_arr_bytes); + cubDEMPrefixScan(numTriTriContactsInEachBin, + triTriContactReportOffsets, + *pNumActiveBinsForTri, this_stream, scratchPad); + } + } + // DEME_DEBUG_PRINTF("Num contacts each bin:"); + // DEME_DEBUG_EXEC(displayDeviceArray(numSphContactsInEachBin, *pNumActiveBins)); + // DEME_DEBUG_PRINTF("Tri contact report offsets:"); + // DEME_DEBUG_EXEC(displayDeviceArray(triSphContactReportOffsets, *pNumActiveBinsForTri)); + // DEME_DEBUG_PRINTF("Family number:"); + // DEME_DEBUG_EXEC(displayDeviceArray(granData->familyID.device(), simParams->nOwnerBodies)); + + // Add sphere--sphere contacts together with sphere--analytical geometry contacts + if (simParams->nSpheresGM > 0) { + scratchPad.allocateDualStruct("numSSContact"); + deviceAdd( + scratchPad.getDualStructDevice("numSSContact"), &(numSphContactsInEachBin[*pNumActiveBins - 1]), + &(sphSphContactReportOffsets[*pNumActiveBins - 1]), this_stream); + deviceAssign(&(sphSphContactReportOffsets[*pNumActiveBins]), + scratchPad.getDualStructDevice("numSSContact"), this_stream); + scratchPad.syncDualStructDeviceToHost("numSSContact"); + nSphereSphereContact = *scratchPad.getDualStructHost("numSSContact"); + scratchPad.finishUsingDualStruct("numSSContact"); + } + if (simParams->nTriGM > 0) { + scratchPad.allocateDualStruct("numSMContact"); + deviceAdd( + scratchPad.getDualStructDevice("numSMContact"), + &(numTriSphContactsInEachBin[*pNumActiveBinsForTri - 1]), + &(triSphContactReportOffsets[*pNumActiveBinsForTri - 1]), this_stream); + deviceAssign(&(triSphContactReportOffsets[*pNumActiveBinsForTri]), + scratchPad.getDualStructDevice("numSMContact"), this_stream); + scratchPad.syncDualStructDeviceToHost("numSMContact"); + nTriSphereContact = *scratchPad.getDualStructHost("numSMContact"); + scratchPad.finishUsingDualStruct("numSMContact"); + // If mesh-universal contact is on, tri--tri contacts are also possible, add it here... + if (solverFlags.meshUniversalContact) { + scratchPad.allocateDualStruct("numMMContact"); + deviceAdd( + scratchPad.getDualStructDevice("numMMContact"), + &(numTriTriContactsInEachBin[*pNumActiveBinsForTri - 1]), + &(triTriContactReportOffsets[*pNumActiveBinsForTri - 1]), this_stream); + deviceAssign(&(triTriContactReportOffsets[*pNumActiveBinsForTri]), + scratchPad.getDualStructDevice("numMMContact"), this_stream); + scratchPad.syncDualStructDeviceToHost("numMMContact"); + nTriTriContact = *scratchPad.getDualStructHost("numMMContact"); + scratchPad.finishUsingDualStruct("numMMContact"); + } + } + // std::cout << "nSphereGeoContact: " << nSphereGeoContact << std::endl; + // std::cout << "nSphereSphereContact: " << nSphereSphereContact << std::endl; + // std::cout << "nTriSphereContact: " << nTriSphereContact << std::endl; + // std::cout << "nTriTriContact: " << nTriTriContact << std::endl; + // ---------------------------------------------------------------------------------------- + // IMPORTANT NOTE: contacts got here may have duplicates, as we do not rule out duplicates in tri--tri + // contacts, so the numbers here are not reliable. These duplicates will be filtered out later. + // ---------------------------------------------------------------------------------------- + + *scratchPad.numPrimitiveContacts = + nSphereSphereContact + nSphereGeoContact + nTriGeoContact + nTriSphereContact + nTriTriContact; + if (*scratchPad.numPrimitiveContacts > idPrimitiveA.size()) { + primitiveContactArraysResize(*scratchPad.numPrimitiveContacts, idPrimitiveA, idPrimitiveB, + contactTypePrimitive, contactPersistency, granData); + } + + // Sphere--sphere contact pairs go after tri--anal-geo contacts + bodyID_t* idSphA = (granData->idPrimitiveA + nSphereGeoContact + nTriGeoContact); + bodyID_t* idSphB = (granData->idPrimitiveB + nSphereGeoContact + nTriGeoContact); + contact_t* dType = (granData->contactTypePrimitive + nSphereGeoContact + nTriGeoContact); + // Then fill in those contacts + if (blocks_needed_for_bins_sph > 0) { + sphere_contact_kernels->kernel("populateSphereContactPairsEachBin") + .instantiate() + .configure(dim3(blocks_needed_for_bins_sph), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, + sphereIDsLookUpTable, sphSphContactReportOffsets, idSphA, idSphB, dType, *pNumActiveBins); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Triangle--sphere contact pairs go after sphere--sphere contacts. Remember to mark their type. + if (blocks_needed_for_bins_tri > 0) { + // The contact in this section can be sph--tri... + bodyID_t* idSphA_sm = + (granData->idPrimitiveA + nSphereGeoContact + nTriGeoContact + nSphereSphereContact); + bodyID_t* idTriB_sm = + (granData->idPrimitiveB + nSphereGeoContact + nTriGeoContact + nSphereSphereContact); + contact_t* dType_sm = + (granData->contactTypePrimitive + nSphereGeoContact + nTriGeoContact + nSphereSphereContact); + // Mesh patch IDs of SM, MM contacts go after MA contacts + // Or tri--tri... They go after tri--sph contacts + bodyID_t* idTriA_mm = (granData->idPrimitiveA + nSphereGeoContact + nTriGeoContact + + nSphereSphereContact + nTriSphereContact); + bodyID_t* idTriB_mm = (granData->idPrimitiveB + nSphereGeoContact + nTriGeoContact + + nSphereSphereContact + nTriSphereContact); + contact_t* dType_mm = (granData->contactTypePrimitive + nSphereGeoContact + nTriGeoContact + + nSphereSphereContact + nTriSphereContact); + // And two possible types of contact are resolved both in this kernel + sphTri_contact_kernels->kernel("populateTriangleContactsEachBin") + .instantiate() + .configure(dim3(blocks_needed_for_bins_tri), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) + .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, + sphereIDsLookUpTable, mapTriActBinToSphActBin, triIDsEachBinTouches_sorted, + activeBinIDsForTri, numTrianglesBinTouches, triIDsLookUpTable, triSphContactReportOffsets, + triTriContactReportOffsets, idSphA_sm, idTriB_sm, dType_sm, idTriA_mm, idTriB_mm, dType_mm, + tri_vA1, tri_vB1, tri_vC1, tri_shift, *pNumActiveBinsForTri, + solverFlags.meshUniversalContact); + DEME_GPU_DEBUG_SYNC(this_stream); + } + // std::cout << "idPrimitiveA: " << std::endl; + // displayDeviceArray(granData->idPrimitiveA, *scratchPad.numPrimitiveContacts); + // std::cout << "idPrimitiveB: " << std::endl; + // displayDeviceArray(granData->idPrimitiveB, *scratchPad.numPrimitiveContacts); + // std::cout << "contactTypePrimitive: " << std::endl; + // displayDeviceArray(granData->contactTypePrimitive, *scratchPad.numPrimitiveContacts); + } // End of bin-wise contact detection subroutine + + // The following vectors are used till the end + scratchPad.finishUsingTempVector("sphereIDsEachBinTouches_sorted"); + scratchPad.finishUsingDualArray("activeBinIDs"); + scratchPad.finishUsingTempVector("numSpheresBinTouches"); + scratchPad.finishUsingTempVector("sphereIDsLookUpTable"); + scratchPad.finishUsingTempVector("triIDsEachBinTouches_sorted"); + scratchPad.finishUsingDualArray("activeBinIDsForTri"); + scratchPad.finishUsingTempVector("numTrianglesBinTouches"); + scratchPad.finishUsingDualArray("mapTriActBinToSphActBin"); + scratchPad.finishUsingTempVector("triIDsLookUpTable"); + scratchPad.finishUsingTempVector("numSphContactsInEachBin"); + scratchPad.finishUsingTempVector("numTriSphContactsInEachBin"); + scratchPad.finishUsingTempVector("numTriTriContactsInEachBin"); + scratchPad.finishUsingTempVector("sphSphContactReportOffsets"); + scratchPad.finishUsingTempVector("triSphContactReportOffsets"); + scratchPad.finishUsingTempVector("triTriContactReportOffsets"); + + // Release triangle precompute buffers (needed by tri contact kernels above). + if (simParams->nTriGM > 0) { + scratchPad.finishUsingTempVector("tri_vA1"); + scratchPad.finishUsingTempVector("tri_vB1"); + scratchPad.finishUsingTempVector("tri_vC1"); + scratchPad.finishUsingTempVector("tri_shift"); + } + + scratchPad.finishUsingDualStruct("numActiveBins"); + scratchPad.finishUsingDualStruct("numActiveBinsForTri"); + + // Raw primitive CD uses fixed-size report regions, so NOT_A_CONTACT records are expected. They must not survive + // into persistent-contact carryover, duplicate removal, or patch-contact derivation because their primitive IDs + // are report-slot artifacts rather than valid physical contact identities. + removeNullPrimitiveContacts(granData, this_stream, scratchPad); + + // ----------------------------------------------------------------------------------------------------------- + // One more task: If the user specified persistent contacts, we check the previous contact list + // and see if there are some contacts we need to add to the current list. Even if we detected 0 contacts, we + // might still have persistent contacts to add to the list. + // ----------------------------------------------------------------------------------------------------------- + + // At user API level, persistent contacts are only supported for history-based models. + if (solverFlags.hasPersistentContacts) { + if (solverFlags.isHistoryless) { + DEME_ERROR(std::string( + "You cannot mark persistent contacts when using a wildcard-less/history-less contact model (since " + "persistency is a part of the history).\nYou can use a different force model, and if you have to " + "use this one, add a placeholder wildcard.")); + } + // A bool array to help find what persistent contacts from the prev array need to be processed... + // Contact persistency array is kT-only, and since the solver does not modify it, it can be seen as the + // prev-primitive contact array's persistency info. + size_t flag_arr_bytes = (*scratchPad.numPrevPrimitiveContacts) * sizeof(notStupidBool_t); + notStupidBool_t* grab_flags = (notStupidBool_t*)scratchPad.allocateTempVector("grab_flags", flag_arr_bytes); + size_t blocks_needed_for_flagging = + (*scratchPad.numPrevPrimitiveContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_flagging > 0) { + markBoolIf<<>>( + grab_flags, granData->contactPersistency, CONTACT_IS_PERSISTENT, + *scratchPad.numPrevPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + // Store the number of persistent contacts + scratchPad.allocateDualStruct("numPersistCnts"); + + // Then extract the persistent array + // This many elements are sufficient, at very least... + size_t selected_ids_bytes = (*scratchPad.numPrevPrimitiveContacts) * sizeof(bodyID_t); + size_t selected_types_bytes = (*scratchPad.numPrevPrimitiveContacts) * sizeof(contact_t); + bodyID_t* selected_idA = (bodyID_t*)scratchPad.allocateTempVector("selected_idA", selected_ids_bytes); + bodyID_t* selected_idB = (bodyID_t*)scratchPad.allocateTempVector("selected_idB", selected_ids_bytes); + contact_t* selected_types = + (contact_t*)scratchPad.allocateTempVector("selected_types", selected_types_bytes); + + cubDEMSelectFlagged(granData->previous_idPrimitiveA, selected_idA, grab_flags, + scratchPad.getDualStructDevice("numPersistCnts"), + *scratchPad.numPrevPrimitiveContacts, this_stream, + scratchPad); + cubDEMSelectFlagged(granData->previous_idPrimitiveB, selected_idB, grab_flags, + scratchPad.getDualStructDevice("numPersistCnts"), + *scratchPad.numPrevPrimitiveContacts, this_stream, + scratchPad); + cubDEMSelectFlagged( + granData->previous_contactTypePrimitive, selected_types, grab_flags, + scratchPad.getDualStructDevice("numPersistCnts"), *scratchPad.numPrevPrimitiveContacts, this_stream, + scratchPad); + // Those flag selections give the same result. Bring it to host. + scratchPad.syncDualStructDeviceToHost("numPersistCnts"); + size_t* pNumPersistCnts = scratchPad.getDualStructHost("numPersistCnts"); + + // Then concatenate the persistent primitive contacts (goes first) and the newly detected contacts (follows + // after) + size_t total_ids_bytes = (*scratchPad.numPrimitiveContacts + *pNumPersistCnts) * sizeof(bodyID_t); + size_t total_types_bytes = (*scratchPad.numPrimitiveContacts + *pNumPersistCnts) * sizeof(contact_t); + size_t total_persistency_bytes = + (*scratchPad.numPrimitiveContacts + *pNumPersistCnts) * sizeof(notStupidBool_t); + selected_ids_bytes = (*pNumPersistCnts) * sizeof(bodyID_t); + selected_types_bytes = (*pNumPersistCnts) * sizeof(contact_t); + bodyID_t* total_idA = (bodyID_t*)scratchPad.allocateTempVector("total_idA", total_ids_bytes); + bodyID_t* total_idB = (bodyID_t*)scratchPad.allocateTempVector("total_idB", total_ids_bytes); + contact_t* total_types = (contact_t*)scratchPad.allocateTempVector("total_types", total_types_bytes); + notStupidBool_t* total_persistency = + (notStupidBool_t*)scratchPad.allocateTempVector("total_persistency", total_persistency_bytes); + DEME_GPU_CALL(cudaMemcpy(total_idA, selected_idA, selected_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(total_idA + *pNumPersistCnts, granData->idPrimitiveA, + total_ids_bytes - selected_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(total_idB, selected_idB, selected_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(total_idB + *pNumPersistCnts, granData->idPrimitiveB, + total_ids_bytes - selected_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(total_types, selected_types, selected_types_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(total_types + *pNumPersistCnts, granData->contactTypePrimitive, + total_types_bytes - selected_types_bytes, cudaMemcpyDeviceToDevice)); + // For the selected portion, persistency is all 1, the rest is all 0 + DEME_GPU_CALL(cudaMemset(total_persistency, CONTACT_NOT_PERSISTENT, total_persistency_bytes)); + size_t blocks_needed_for_setting_1 = + (*pNumPersistCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_setting_1 > 0) { + setArr<<>>( + total_persistency, *pNumPersistCnts, CONTACT_IS_PERSISTENT); + DEME_GPU_DEBUG_SYNC(this_stream); + } + scratchPad.finishUsingTempVector("grab_flags"); + scratchPad.finishUsingTempVector("selected_idA"); + scratchPad.finishUsingTempVector("selected_idB"); + scratchPad.finishUsingTempVector("selected_types"); + + // Then remove potential redundency in the current contact array. + // To do that, we sort by idA... + size_t numTotalCnts = *scratchPad.numPrimitiveContacts + *pNumPersistCnts; + contact_t* contactType_sorted = + (contact_t*)scratchPad.allocateTempVector("contactType_sorted", total_types_bytes); + bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", total_ids_bytes); + bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", total_ids_bytes); + notStupidBool_t* persistency_sorted = + (notStupidBool_t*)scratchPad.allocateTempVector("persistency_sorted", total_persistency_bytes); + // Pay attention to that it does leverage the fact that RadixSort is stable. + sortABTypePersistencyByA(total_idA, total_idB, total_types, total_persistency, idA_sorted, idB_sorted, + contactType_sorted, persistency_sorted, numTotalCnts, this_stream, scratchPad); + + // std::cout << "Contacts before duplication check: " << std::endl; + // displayDeviceArray(idA_sorted, numTotalCnts); + // displayDeviceArray(idB_sorted, numTotalCnts); + // displayDeviceArray(contactType_sorted, numTotalCnts); + // displayDeviceArray(persistency_sorted, numTotalCnts); + + scratchPad.finishUsingTempVector("total_idA"); + scratchPad.finishUsingTempVector("total_idB"); + scratchPad.finishUsingTempVector("total_types"); + scratchPad.finishUsingTempVector("total_persistency"); + scratchPad.finishUsingDualStruct("numPersistCnts"); + + removeDuplicateContacts(granData, idA_sorted, idB_sorted, contactType_sorted, persistency_sorted, + idPrimitiveA, idPrimitiveB, contactTypePrimitive, contactPersistency, true, + DEME_MAX(simParams->nSpheresGM, simParams->nTriGM), numTotalCnts, this_stream, + scratchPad); + + scratchPad.finishUsingTempVector("contactType_sorted"); + scratchPad.finishUsingTempVector("idA_sorted"); + scratchPad.finishUsingTempVector("idB_sorted"); + scratchPad.finishUsingTempVector("persistency_sorted"); + + // Here, not sorted by type actually means sorted by idA + primitiveContactArraysAreSortedByType = false; + } + + // ----------------------------------------------------------------------------------------------------------- + // One more thing: We need to remove duplicate primitive contacts in the contact list, because tri--tri contacts + // are not guaranteed to be unique. But of course, if hasPersistentContacts is on, then the duplication removal + // has already been done in the previous step. + // ----------------------------------------------------------------------------------------------------------- + + if (solverFlags.meshUniversalContact && !solverFlags.hasPersistentContacts) { + // To remove duplicates, we sort by idA... + size_t numTotalCnts = *scratchPad.numPrimitiveContacts; + size_t total_ids_bytes = numTotalCnts * sizeof(bodyID_t); + size_t total_types_bytes = numTotalCnts * sizeof(contact_t); + size_t total_persistency_bytes = numTotalCnts * sizeof(notStupidBool_t); + contact_t* contactType_sorted = + (contact_t*)scratchPad.allocateTempVector("contactType_sorted", total_types_bytes); + bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", total_ids_bytes); + bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", total_ids_bytes); + notStupidBool_t* persistency_sorted = + (notStupidBool_t*)scratchPad.allocateTempVector("persistency_sorted", total_persistency_bytes); + // Pay attention to that it does leverage the fact that RadixSort is stable. + sortABTypePersistencyByA(granData->idPrimitiveA, granData->idPrimitiveB, granData->contactTypePrimitive, + granData->contactPersistency, idA_sorted, idB_sorted, contactType_sorted, + persistency_sorted, numTotalCnts, this_stream, scratchPad); + // std::cout << "Contacts before duplication check: " << std::endl; + // displayDeviceArray(idA_sorted, numTotalCnts); + // displayDeviceArray(idB_sorted, numTotalCnts); + // displayDeviceArray(contactType_sorted, numTotalCnts); + + removeDuplicateContacts(granData, idA_sorted, idB_sorted, contactType_sorted, persistency_sorted, + idPrimitiveA, idPrimitiveB, contactTypePrimitive, contactPersistency, false, + DEME_MAX(simParams->nSpheresGM, simParams->nTriGM), numTotalCnts, this_stream, + scratchPad); + // std::cout << "Contacts after duplication check: " << std::endl; + // displayDeviceArray(granData->idPrimitiveA, *scratchPad.numPrimitiveContacts); + // displayDeviceArray(granData->idPrimitiveB, *scratchPad.numPrimitiveContacts); + // displayDeviceArray(granData->contactTypePrimitive, *scratchPad.numPrimitiveContacts); + + scratchPad.finishUsingTempVector("contactType_sorted"); + scratchPad.finishUsingTempVector("idA_sorted"); + scratchPad.finishUsingTempVector("idB_sorted"); + scratchPad.finishUsingTempVector("persistency_sorted"); + + // Here, not sorted by type actually means sorted by idA + primitiveContactArraysAreSortedByType = false; + } + // std::cout << "Primitive contacts: " << std::endl; + // displayDeviceArray(granData->idPrimitiveA, *scratchPad.numPrimitiveContacts); + // displayDeviceArray(granData->idPrimitiveB, *scratchPad.numPrimitiveContacts); + // displayDeviceArray(granData->contactTypePrimitive, *scratchPad.numPrimitiveContacts); + + if (simParams->nCombinedOwners > 0 && !simParams->allowIntraCombinedOwnerContacts && + granData->ownerCombinedMaster != nullptr && *scratchPad.numPrimitiveContacts > 0) { + // Optional combined-owner internal-contact suppression. It is active only when combined members exist, and + // it is bypassed when users explicitly allow intra-combined-owner contacts. + const size_t numTotalCnts = *scratchPad.numPrimitiveContacts; + const size_t blocks_needed = (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + notStupidBool_t* keepFlags = (notStupidBool_t*)scratchPad.allocateTempVector( + "combined_keep_flags", numTotalCnts * sizeof(notStupidBool_t)); + + markContactsByCombinedOwnerMask<<>>( + granData->idPrimitiveA, granData->idPrimitiveB, granData->contactTypePrimitive, + granData->ownerClumpBody, granData->ownerTriMesh, granData->ownerAnalBody, + granData->ownerCombinedMaster, keepFlags, numTotalCnts, simParams->nSpheresGM, simParams->nTriGM, + simParams->nAnalGM, simParams->nOwnerBodies); + DEME_GPU_DEBUG_SYNC(this_stream); + + scratchPad.allocateDualStruct("numCombinedKeptCnts"); + cubDEMSum(keepFlags, scratchPad.getDualStructDevice("numCombinedKeptCnts"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numCombinedKeptCnts"); + const size_t numKept = *scratchPad.getDualStructHost("numCombinedKeptCnts"); + + if (numKept < numTotalCnts) { + const size_t alloc_kept = DEME_MAX((size_t)1, numKept); + const size_t ids_bytes = alloc_kept * sizeof(bodyID_t); + const size_t type_bytes = alloc_kept * sizeof(contact_t); + const size_t persist_bytes = alloc_kept * sizeof(notStupidBool_t); + bodyID_t* keptA = (bodyID_t*)scratchPad.allocateTempVector("combined_kept_idA", ids_bytes); + bodyID_t* keptB = (bodyID_t*)scratchPad.allocateTempVector("combined_kept_idB", ids_bytes); + contact_t* keptType = (contact_t*)scratchPad.allocateTempVector("combined_kept_type", type_bytes); + notStupidBool_t* keptPersist = + (notStupidBool_t*)scratchPad.allocateTempVector("combined_kept_persist", persist_bytes); + + cubDEMSelectFlagged(granData->idPrimitiveA, keptA, keepFlags, + scratchPad.getDualStructDevice("numCombinedKeptCnts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged(granData->idPrimitiveB, keptB, keepFlags, + scratchPad.getDualStructDevice("numCombinedKeptCnts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged(granData->contactTypePrimitive, keptType, keepFlags, + scratchPad.getDualStructDevice("numCombinedKeptCnts"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged( + granData->contactPersistency, keptPersist, keepFlags, + scratchPad.getDualStructDevice("numCombinedKeptCnts"), numTotalCnts, this_stream, scratchPad); + + if (numKept > 0) { + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->idPrimitiveA, keptA, ids_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->idPrimitiveB, keptB, ids_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactTypePrimitive, keptType, type_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactPersistency, keptPersist, persist_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + } + + scratchPad.finishUsingTempVector("combined_kept_idA"); + scratchPad.finishUsingTempVector("combined_kept_idB"); + scratchPad.finishUsingTempVector("combined_kept_type"); + scratchPad.finishUsingTempVector("combined_kept_persist"); + *scratchPad.numPrimitiveContacts = numKept; + } + + scratchPad.finishUsingDualStruct("numCombinedKeptCnts"); + scratchPad.finishUsingTempVector("combined_keep_flags"); + } + + // Defensive cleanup: this is normally a no-op because raw CD sentinels were compacted above. Keep it here so + // stale previous arrays or future filters cannot reintroduce NOT_A_CONTACT into patch-level grouping. + removeNullPrimitiveContacts(granData, this_stream, scratchPad); + + // ----------------------------------------------------------------------------------------------------------- + // We need to now do some sanity checks. If primitive contacts are already sorted by idA, we just use them (they + // are stored in granData). If sorted by type, then we have to sort by idA but do not change the granData + // arrays, as they will be used later. + // ----------------------------------------------------------------------------------------------------------- + if (*scratchPad.numPrimitiveContacts == 0) { + stateParams.avgCntsOverLimitStreak = 0; + stateParams.avgCntsOverLimitPeak = 0.f; + } + if (*scratchPad.numPrimitiveContacts > 0) { + size_t numTotalCnts = *scratchPad.numPrimitiveContacts; + size_t total_ids_bytes = numTotalCnts * sizeof(bodyID_t); + bodyID_t* idA = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted_for_sanity_check", total_ids_bytes); + if (!primitiveContactArraysAreSortedByType) { + // A sorted already... + idA = granData->idPrimitiveA; + } else { + // Need to sort A... + cubDEMSortKeys(granData->idPrimitiveA, idA, numTotalCnts, this_stream, scratchPad); + } + + size_t nGeoSafe = DEME_MAX(simParams->nSpheresGM, simParams->nTriGM); + // For tab-keeping: how many contacts on average each primitive has. + // First, identify unique idA in primitive contacts + size_t run_length_bytes = nGeoSafe * sizeof(primitivesPrimTouches_t); + primitivesPrimTouches_t* new_idA_runlength = + (primitivesPrimTouches_t*)scratchPad.allocateTempVector("new_idA_runlength", run_length_bytes); + size_t unique_id_bytes = nGeoSafe * sizeof(bodyID_t); + bodyID_t* unique_new_idA = (bodyID_t*)scratchPad.allocateTempVector("unique_new_idA", unique_id_bytes); + scratchPad.allocateDualStruct("numUniqueNewA"); + + cubDEMRunLengthEncode(idA, unique_new_idA, new_idA_runlength, + scratchPad.getDualStructDevice("numUniqueNewA"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueNewA"); + size_t* pNumUniqueNewA = scratchPad.getDualStructHost("numUniqueNewA"); + + // Figure out how many contacts an item in idA array typically has. + stateParams.avgCntsPerPrimitive = + (*pNumUniqueNewA > 0) ? (float)(*scratchPad.numPrimitiveContacts) / (float)(*pNumUniqueNewA) : 0.0; + + DEME_DEBUG_PRINTF("Average number of contacts for each geometry: %.7g", stateParams.avgCntsPerPrimitive); + constexpr unsigned int kOverLimitStreakToError = 6; + constexpr float kSevereOverLimitMultiplier = 1.35f; + if (stateParams.avgCntsPerPrimitive > solverFlags.errOutAvgPrimitiveCnts) { + stateParams.avgCntsOverLimitStreak++; + stateParams.avgCntsOverLimitPeak = + DEME_MAX(stateParams.avgCntsOverLimitPeak, stateParams.avgCntsPerPrimitive); + const bool severe_over_limit = + stateParams.avgCntsPerPrimitive > (kSevereOverLimitMultiplier * solverFlags.errOutAvgPrimitiveCnts); + const bool persistent_over_limit = stateParams.avgCntsOverLimitStreak >= kOverLimitStreakToError; + if (!severe_over_limit && !persistent_over_limit) { + if (stateParams.avgCntsOverLimitStreak == 1 || + stateParams.avgCntsOverLimitStreak == kOverLimitStreakToError - 1) { + DEME_WARNING( + "Average primitive contacts temporarily above limit: %.7g (limit %.7g), streak %u/%u.\n" + "If this remains transient, simulation continues. If repeated or severe, it will error " + "out.", + stateParams.avgCntsPerPrimitive, solverFlags.errOutAvgPrimitiveCnts, + stateParams.avgCntsOverLimitStreak, kOverLimitStreakToError); + } + } else { + DEME_ERROR( + "On average a primitive (spheres, triangle facets) has %.7g contacts with other primitives, " + "more than the max allowance (%.7g).\nOver-limit streak: %u step(s), peak in streak: %.7g.\n" + "If you believe this number is expected with the physics you are simulating, set the allowance " + "high using SetErrorOutAvgContacts before initialization.\nIf you think this is because dT " + "drifting too much ahead of kT so the contact margin added is too big, use SetCDMaxUpdateFreq " + "to limit the max dT future drift.\nOtherwise, the simulation may have diverged with " + "unreasonable entity velocities and relaxing the physics may help, such as decreasing the step " + "size and modifying material properties.\nIf this happens at the start of simulation, check if " + "there are initial penetrations, a.k.a. elements initialized inside walls.\nIf none works and " + "you are going to discuss this on forum https://groups.google.com/g/projectchrono, please " + "include a visual rendering of the simulation before crash in the post.\n", + stateParams.avgCntsPerPrimitive, solverFlags.errOutAvgPrimitiveCnts, + stateParams.avgCntsOverLimitStreak, stateParams.avgCntsOverLimitPeak); + } + } else { + stateParams.avgCntsOverLimitStreak = 0; + stateParams.avgCntsOverLimitPeak = 0.f; + } + + scratchPad.finishUsingTempVector("new_idA_runlength"); + scratchPad.finishUsingTempVector("unique_new_idA"); + scratchPad.finishUsingDualStruct("numUniqueNewA"); + scratchPad.finishUsingTempVector("idA_sorted_for_sanity_check"); + } + + // ----------------------------------------------------------------------------------------------------------- + // Up to this point, we have been working with primitive contacts (sphere-sphere, sphere-triangle, + // triangle-triangle, sphere-analytical-geometry, triangle-analytical-geometry). Now we need to convert them to + // patch/convex shape based contacts for dT consumption. + // ----------------------------------------------------------------------------------------------------------- + + // Generate patch IDs (idPatchA/B) and geomToPatchMap (mapping between primitive contacts and patch-based + // contacts) + if (*scratchPad.numPrimitiveContacts > 0) { + size_t numTotalCnts = *scratchPad.numPrimitiveContacts; + if (solverFlags.useSimplePatchCombination) { + // =============================================================================================== + // BEGIN SIMPLE PATCH ROUTE (useSimplePatchCombination == true) + // + // This entire if-body, through the matching END marker below, collapses primitive contacts directly + // to one patch contact per (contact type, patch A, patch B). It deliberately does not run island + // flooding; contactPatchIsland is a deterministic dummy label so history keys depend only on type and + // patch pair. + // =============================================================================================== + + // RefBranch-style simple grouping: sort and group patch pairs inside each contact-type segment. + if (!primitiveContactArraysAreSortedByType) { + sortPrimitiveContactsByTypeInPlace(granData, numTotalCnts, this_stream, scratchPad); + } + primitiveContactArraysAreSortedByType = true; + + // The simple patch route stores one start/count range per physical contact type. Use the RLE we need + // anyway to detect repeated physical type runs, and only pay for a full type sort when fragmentation is + // present. + const size_t type_buf_len = DEME_MAX((size_t)1, numTotalCnts); + contact_t* unique_types = + (contact_t*)scratchPad.allocateTempVector("unique_types", type_buf_len * sizeof(contact_t)); + contactPairs_t* type_counts = (contactPairs_t*)scratchPad.allocateTempVector( + "type_counts", type_buf_len * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numUniqueTypes"); + + cubDEMRunLengthEncode( + granData->contactTypePrimitive, unique_types, type_counts, + scratchPad.getDualStructDevice("numUniqueTypes"), numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueTypes"); + size_t numTypes = *scratchPad.getDualStructHost("numUniqueTypes"); + if (primitiveTypeRunsAreFragmented(unique_types, numTypes)) { + sortPrimitiveContactsByTypeInPlace(granData, numTotalCnts, this_stream, scratchPad); + cubDEMRunLengthEncode( + granData->contactTypePrimitive, unique_types, type_counts, + scratchPad.getDualStructDevice("numUniqueTypes"), numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueTypes"); + numTypes = *scratchPad.getDualStructHost("numUniqueTypes"); + if (numTypes > NUM_SUPPORTED_CONTACT_TYPES) { + DEME_ERROR( + "Primitive contact type run-length output (%zu) exceeds known contact type count (%u) " + "after type canonicalization.", + numTypes, (unsigned int)NUM_SUPPORTED_CONTACT_TYPES); + } + } + + patchIDPair_t* contactPatchPairs = (patchIDPair_t*)scratchPad.allocateTempVector( + "contactPatchPairs", numTotalCnts * sizeof(patchIDPair_t)); + size_t blocks_needed_for_patch_ids = + (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_patch_ids > 0) { + extractPatchInvolvedContactPatchIDPairs<<>>( + contactPatchPairs, granData->contactTypePrimitive, granData->idPrimitiveA, + granData->idPrimitiveB, granData->triPatchID, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + if (numTypes > 0) { + contactPairs_t* host_type_counts = new contactPairs_t[numTypes]; + DEME_GPU_CALL(cudaMemcpy(host_type_counts, type_counts, numTypes * sizeof(contactPairs_t), + cudaMemcpyDeviceToHost)); + + contact_t* host_unique_types = new contact_t[numTypes]; + DEME_GPU_CALL(cudaMemcpy(host_unique_types, unique_types, numTypes * sizeof(contact_t), + cudaMemcpyDeviceToHost)); + + size_t patch_arr_bytes = numTotalCnts * sizeof(patchIDPair_t); + patchIDPair_t* patchPairs_sorted = + (patchIDPair_t*)scratchPad.allocateTempVector("patchPairs_sorted", patch_arr_bytes); + size_t id_arr_bytes = numTotalCnts * sizeof(bodyID_t); + bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", id_arr_bytes); + bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", id_arr_bytes); + size_t type_arr_bytes = numTotalCnts * sizeof(contact_t); + contact_t* contactType_sorted = + (contact_t*)scratchPad.allocateTempVector("contactType_sorted", type_arr_bytes); + size_t persistency_arr_bytes = numTotalCnts * sizeof(notStupidBool_t); + notStupidBool_t* contactPersistency_sorted = (notStupidBool_t*)scratchPad.allocateTempVector( + "contactPersistency_sorted", persistency_arr_bytes); + + size_t max_unique_per_segment = numTotalCnts; + patchIDPair_t* unique_patch_pairs = (patchIDPair_t*)scratchPad.allocateTempVector( + "unique_patch_pairs", max_unique_per_segment * sizeof(patchIDPair_t)); + contactPairs_t* patch_pair_counts = (contactPairs_t*)scratchPad.allocateTempVector( + "patch_pair_counts", max_unique_per_segment * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numUniquePatchPairs"); + + if (numTotalCnts > geomToPatchMap.size()) { + DEME_DUAL_ARRAY_RESIZE_NOVAL(geomToPatchMap, numTotalCnts); + granData.toDevice(); + } + + size_t totalUniquePatchPairs = 0; + size_t prim_offset = 0; + + for (size_t i = 0; i < numTypes; i++) { + contact_t thisType = host_unique_types[i]; + contactPairs_t count = host_type_counts[i]; + + if (count == 0) { + continue; + } + + cubDEMSortByKeys( + contactPatchPairs + prim_offset, patchPairs_sorted + prim_offset, + granData->idPrimitiveA + prim_offset, idA_sorted + prim_offset, count, this_stream, + scratchPad); + + cubDEMSortByKeys( + contactPatchPairs + prim_offset, patchPairs_sorted + prim_offset, + granData->idPrimitiveB + prim_offset, idB_sorted + prim_offset, count, this_stream, + scratchPad); + + cubDEMSortByKeys( + contactPatchPairs + prim_offset, patchPairs_sorted + prim_offset, + granData->contactTypePrimitive + prim_offset, contactType_sorted + prim_offset, count, + this_stream, scratchPad); + cubDEMSortByKeys( + contactPatchPairs + prim_offset, patchPairs_sorted + prim_offset, + granData->contactPersistency + prim_offset, contactPersistency_sorted + prim_offset, count, + this_stream, scratchPad); + + cubDEMRunLengthEncode( + patchPairs_sorted + prim_offset, unique_patch_pairs, patch_pair_counts, + scratchPad.getDualStructDevice("numUniquePatchPairs"), count, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniquePatchPairs"); + size_t numUniqueInSegment = *scratchPad.getDualStructHost("numUniquePatchPairs"); + + size_t newTotalSize = totalUniquePatchPairs + numUniqueInSegment; + if (newTotalSize > idPatchA.size()) { + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPatchA, newTotalSize); + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPatchB, newTotalSize); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactTypePatch, newTotalSize); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactPatchIsland, newTotalSize); + granData.toDevice(); + } + + if (numUniqueInSegment > 0) { + typeStartCountPatchMap_thisStep[thisType] = {totalUniquePatchPairs, numUniqueInSegment}; + + size_t blocks_needed_for_decode = + (numUniqueInSegment + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + decodePatchPairsToSeparateArrays<<>>( + unique_patch_pairs, granData->idPatchA + totalUniquePatchPairs, + granData->idPatchB + totalUniquePatchPairs, numUniqueInSegment); + DEME_GPU_DEBUG_SYNC(this_stream); + + fillContactTypeArray<<>>(granData->contactTypePatch + totalUniquePatchPairs, + thisType, numUniqueInSegment); + DEME_GPU_DEBUG_SYNC(this_stream); + + // Simple patch combination has exactly one patch contact per (type, patchA, patchB). + // Its history key must therefore not depend on whichever primitive happens to be first in + // this patch-pair group; that representative can change between kT passes and make dT lose + // contact history. Use one deterministic island label for all simple-mode patch contacts. + DEME_GPU_CALL_ASYNC(cudaMemsetAsync(granData->contactPatchIsland + totalUniquePatchPairs, 0, + numUniqueInSegment * sizeof(bodyID_t), this_stream), + this_stream); + } + + contactPairs_t* isNewGroup = (contactPairs_t*)scratchPad.allocateTempVector( + "isNewGroup", count * sizeof(contactPairs_t)); + + size_t blocks_needed_for_mark = + (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_mark > 0) { + markNewPatchPairGroups<<>>(patchPairs_sorted + prim_offset, isNewGroup, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + cubDEMInclusiveScan( + isNewGroup, granData->geomToPatchMap + prim_offset, count, this_stream, scratchPad); + + if (count > 0 && totalUniquePatchPairs > 0) { + addOffsetToArray<<>>(granData->geomToPatchMap + prim_offset, + (contactPairs_t)totalUniquePatchPairs, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } + scratchPad.finishUsingTempVector("isNewGroup"); + + totalUniquePatchPairs += numUniqueInSegment; + prim_offset += count; + } + + *scratchPad.numContacts = totalUniquePatchPairs; + + scratchPad.finishUsingTempVector("unique_patch_pairs"); + scratchPad.finishUsingTempVector("patch_pair_counts"); + scratchPad.finishUsingDualStruct("numUniquePatchPairs"); + + DEME_GPU_CALL(cudaMemcpy(granData->idPrimitiveA, idA_sorted, numTotalCnts * sizeof(bodyID_t), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->idPrimitiveB, idB_sorted, numTotalCnts * sizeof(bodyID_t), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactTypePrimitive, contactType_sorted, + numTotalCnts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactPersistency, contactPersistency_sorted, + numTotalCnts * sizeof(notStupidBool_t), cudaMemcpyDeviceToDevice)); + + scratchPad.finishUsingTempVector("patchPairs_sorted"); + scratchPad.finishUsingTempVector("idA_sorted"); + scratchPad.finishUsingTempVector("idB_sorted"); + scratchPad.finishUsingTempVector("contactType_sorted"); + scratchPad.finishUsingTempVector("contactPersistency_sorted"); + + delete[] host_type_counts; + delete[] host_unique_types; + } + + scratchPad.finishUsingTempVector("unique_types"); + scratchPad.finishUsingTempVector("type_counts"); + scratchPad.finishUsingTempVector("contactPatchPairs"); + scratchPad.finishUsingDualStruct("numUniqueTypes"); + + // =============================================================================================== + // END SIMPLE PATCH ROUTE + // =============================================================================================== + } else { + // =============================================================================================== + // BEGIN COMPLEX ISLAND-FLOODING PATCH ROUTE (useSimplePatchCombination == false) + // + // This entire else-body, through the matching END marker after shared cleanup below, builds the + // RefBranch-style type/patch-pair segments first, then splits each patch pair into connected + // patch-islands via flooding. This path is for preserving multiple disconnected mesh-contact islands + // inside the same patch pair. + // =============================================================================================== + + size_t total_ids_bytes = numTotalCnts * sizeof(bodyID_t); + size_t total_persistency_bytes = numTotalCnts * sizeof(notStupidBool_t); + size_t patch_arr_bytes = numTotalCnts * sizeof(patchIDPair_t); + size_t type_arr_bytes = numTotalCnts * sizeof(contact_t); + size_t idx_arr_bytes = numTotalCnts * sizeof(contactPairs_t); + + patchIDPair_t* contactPatchPairs = + (patchIDPair_t*)scratchPad.allocateTempVector("contactPatchPairs", patch_arr_bytes); + patchIDPair_t* patchPairs_sorted = + (patchIDPair_t*)scratchPad.allocateTempVector("patchPairs_sorted", patch_arr_bytes); + contactPairs_t* sort_indices = + (contactPairs_t*)scratchPad.allocateTempVector("contactSortIndices", idx_arr_bytes); + contactPairs_t* sort_indices_sorted = + (contactPairs_t*)scratchPad.allocateTempVector("contactSortIndices_sorted", idx_arr_bytes); + bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", total_ids_bytes); + bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", total_ids_bytes); + contact_t* contactType_sorted = + (contact_t*)scratchPad.allocateTempVector("contactType_sorted", type_arr_bytes); + notStupidBool_t* contactPersistency_sorted = (notStupidBool_t*)scratchPad.allocateTempVector( + "contactPersistency_sorted", total_persistency_bytes); + contactPairs_t* type_indices = + (contactPairs_t*)scratchPad.allocateTempVector("contactTypeIndices", idx_arr_bytes); + contactPairs_t* type_indices_sorted = + (contactPairs_t*)scratchPad.allocateTempVector("contactTypeIndices_sorted", idx_arr_bytes); + + size_t blocks_needed_for_patch_ids = + (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_patch_ids > 0) { + extractPatchInvolvedContactPatchIDPairs<<>>( + contactPatchPairs, granData->contactTypePrimitive, granData->idPrimitiveA, + granData->idPrimitiveB, granData->triPatchID, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + lineNumbers<<>>(sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Sort by patch-pair first, then gather arrays into patch-pair order. + cubDEMSortByKeys(contactPatchPairs, patchPairs_sorted, sort_indices, + sort_indices_sorted, numTotalCnts, this_stream, + scratchPad); + + if (blocks_needed_for_patch_ids > 0) { + gatherByIndex + <<>>( + granData->idPrimitiveA, idA_sorted, sort_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->idPrimitiveB, idB_sorted, sort_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactTypePrimitive, contactType_sorted, sort_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactPersistency, contactPersistency_sorted, sort_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + if (blocks_needed_for_patch_ids > 0) { + lineNumbers<<>>(type_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Stable sort by contact type to preserve patch-pair order within each type segment. + cubDEMSortByKeys(contactType_sorted, granData->contactTypePrimitive, + type_indices, type_indices_sorted, numTotalCnts, + this_stream, scratchPad); + + if (blocks_needed_for_patch_ids > 0) { + gatherByIndex + <<>>( + idA_sorted, granData->idPrimitiveA, type_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + idB_sorted, granData->idPrimitiveB, type_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + contactPersistency_sorted, granData->contactPersistency, type_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + patchPairs_sorted, contactPatchPairs, type_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Preserve the RefBranch invariant that primitive contacts are sorted by patch pair within each + // physical contact type segment before patch grouping. The flooded path builds extra island labels on + // top of these base groups, but dT still assumes each type segment has dense, ordered patch-contact + // IDs. + const size_t segment_type_buf_len = DEME_MAX((size_t)1, numTotalCnts); + contact_t* segment_unique_types = (contact_t*)scratchPad.allocateTempVector( + "segment_unique_types", segment_type_buf_len * sizeof(contact_t)); + contactPairs_t* segment_type_counts = (contactPairs_t*)scratchPad.allocateTempVector( + "segment_type_counts", segment_type_buf_len * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numSegmentTypes"); + cubDEMRunLengthEncode( + granData->contactTypePrimitive, segment_unique_types, segment_type_counts, + scratchPad.getDualStructDevice("numSegmentTypes"), numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numSegmentTypes"); + size_t numSegmentTypes = *scratchPad.getDualStructHost("numSegmentTypes"); + if (numSegmentTypes > 0) { + contactPairs_t* host_segment_counts = new contactPairs_t[numSegmentTypes]; + DEME_GPU_CALL(cudaMemcpy(host_segment_counts, segment_type_counts, + numSegmentTypes * sizeof(contactPairs_t), cudaMemcpyDeviceToHost)); + + contactPairs_t segmentOffset = 0; + for (size_t seg = 0; seg < numSegmentTypes; seg++) { + contactPairs_t segCount = host_segment_counts[seg]; + if (segCount == 0) { + continue; + } + size_t blocks_needed_for_segment = + (segCount + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_segment > 0) { + lineNumbers<<>>(sort_indices + segmentOffset, segCount); + DEME_GPU_DEBUG_SYNC(this_stream); + } + cubDEMSortByKeys( + contactPatchPairs + segmentOffset, patchPairs_sorted + segmentOffset, + sort_indices + segmentOffset, sort_indices_sorted + segmentOffset, segCount, this_stream, + scratchPad); + if (blocks_needed_for_segment > 0) { + gatherByIndex + <<>>( + granData->idPrimitiveA + segmentOffset, idA_sorted + segmentOffset, + sort_indices_sorted + segmentOffset, segCount); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->idPrimitiveB + segmentOffset, idB_sorted + segmentOffset, + sort_indices_sorted + segmentOffset, segCount); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactTypePrimitive + segmentOffset, contactType_sorted + segmentOffset, + sort_indices_sorted + segmentOffset, segCount); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactPersistency + segmentOffset, + contactPersistency_sorted + segmentOffset, sort_indices_sorted + segmentOffset, + segCount); + DEME_GPU_DEBUG_SYNC(this_stream); + } + segmentOffset += segCount; + } + + DEME_GPU_CALL( + cudaMemcpy(granData->idPrimitiveA, idA_sorted, total_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL( + cudaMemcpy(granData->idPrimitiveB, idB_sorted, total_ids_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactTypePrimitive, contactType_sorted, type_arr_bytes, + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->contactPersistency, contactPersistency_sorted, + total_persistency_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL( + cudaMemcpy(contactPatchPairs, patchPairs_sorted, patch_arr_bytes, cudaMemcpyDeviceToDevice)); + + delete[] host_segment_counts; + } + scratchPad.finishUsingTempVector("segment_unique_types"); + scratchPad.finishUsingTempVector("segment_type_counts"); + scratchPad.finishUsingDualStruct("numSegmentTypes"); + + primitiveContactArraysAreSortedByType = true; + + // Ensure geomToPatchMap is sized to numPrimitiveContacts before building it. + if (numTotalCnts > geomToPatchMap.size()) { + DEME_DUAL_ARRAY_RESIZE_NOVAL(geomToPatchMap, numTotalCnts); + granData.toDevice(); + } + + contactPairs_t* isNewGroup = + (contactPairs_t*)scratchPad.allocateTempVector("isNewGroup", numTotalCnts * sizeof(contactPairs_t)); + size_t blocks_needed_for_mark = + (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_mark > 0) { + markNewPatchPairGroupsByType<<>>(contactPatchPairs, granData->contactTypePrimitive, + isNewGroup, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Prefix scan gives 0-based group indices for each primitive contact (grouped by type + patch pair). + contactPairs_t* groupIndex = + (contactPairs_t*)scratchPad.allocateTempVector("groupIndex", numTotalCnts * sizeof(contactPairs_t)); + cubDEMInclusiveScan(isNewGroup, groupIndex, numTotalCnts, this_stream, + scratchPad); + + // Flip the first element to 1 so it can be used for selection flags. + setFirstFlagToOne<<<1, 1, 0, this_stream>>>(isNewGroup, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + + scratchPad.allocateDualStruct("numUniqueGroups"); + cubDEMSum(isNewGroup, scratchPad.getDualStructDevice("numUniqueGroups"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueGroups"); + size_t numGroups = *scratchPad.getDualStructHost("numUniqueGroups"); + + // This branch is already under useSimplePatchCombination == false. Keep the flooding path explicit + // here so the simple-mode patch identity rules above remain the only simple path in the code. + { + // ---- BEGIN ISLAND-FLOODING CORE ---- + contact_t* groupContactTypes = nullptr; + if (numGroups > 0) { + groupContactTypes = (contact_t*)scratchPad.allocateTempVector("groupContactTypes", + numGroups * sizeof(contact_t)); + cubDEMSelectFlagged( + granData->contactTypePrimitive, groupContactTypes, isNewGroup, + scratchPad.getDualStructDevice("numUniqueGroups"), numTotalCnts, this_stream, scratchPad); + } + + bodyID_t* groupPrimA = nullptr; + bodyID_t* groupPrimB = nullptr; + if (numGroups > 0) { + groupPrimA = + (bodyID_t*)scratchPad.allocateTempVector("groupPrimA", numGroups * sizeof(bodyID_t)); + groupPrimB = + (bodyID_t*)scratchPad.allocateTempVector("groupPrimB", numGroups * sizeof(bodyID_t)); + cubDEMSelectFlagged(granData->idPrimitiveA, groupPrimA, isNewGroup, + scratchPad.getDualStructDevice("numUniqueGroups"), + numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged(granData->idPrimitiveB, groupPrimB, isNewGroup, + scratchPad.getDualStructDevice("numUniqueGroups"), + numTotalCnts, this_stream, scratchPad); + } + + contactPairs_t* groupUniqueCountA = (contactPairs_t*)scratchPad.allocateTempVector( + "groupUniqueCountA", numGroups * sizeof(contactPairs_t)); + contactPairs_t* groupUniqueCountB = (contactPairs_t*)scratchPad.allocateTempVector( + "groupUniqueCountB", numGroups * sizeof(contactPairs_t)); + if (numGroups > 0) { + DEME_GPU_CALL_ASYNC( + cudaMemsetAsync(groupUniqueCountA, 0, numGroups * sizeof(contactPairs_t), this_stream), + this_stream); + DEME_GPU_CALL_ASYNC( + cudaMemsetAsync(groupUniqueCountB, 0, numGroups * sizeof(contactPairs_t), this_stream), + this_stream); + } + + uint64_t* keyA = + (uint64_t*)scratchPad.allocateTempVector("groupPrimKeyA", numTotalCnts * sizeof(uint64_t)); + uint64_t* keyA_sorted = (uint64_t*)scratchPad.allocateTempVector("groupPrimKeyA_sorted", + numTotalCnts * sizeof(uint64_t)); + if (blocks_needed_for_patch_ids > 0) { + buildGroupPrimitiveKeys<<>>(groupIndex, granData->idPrimitiveA, keyA, + numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + cubDEMSortKeys(keyA, keyA_sorted, numTotalCnts, this_stream, scratchPad); + + uint64_t* uniqueKeyA = + (uint64_t*)scratchPad.allocateTempVector("uniqueKeyA", numTotalCnts * sizeof(uint64_t)); + scratchPad.allocateDualStruct("numUniqueKeyA"); + cubDEMUnique(keyA_sorted, uniqueKeyA, scratchPad.getDualStructDevice("numUniqueKeyA"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueKeyA"); + size_t numUniqueKeyA = *scratchPad.getDualStructHost("numUniqueKeyA"); + if (numUniqueKeyA > 0) { + contactPairs_t* uniqueGroupA = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueGroupA", numUniqueKeyA * sizeof(contactPairs_t)); + size_t blocks_needed_unique = + (numUniqueKeyA + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + extractGroupIndexFromKey<<>>(uniqueKeyA, uniqueGroupA, numUniqueKeyA); + DEME_GPU_DEBUG_SYNC(this_stream); + + contactPairs_t* uniqueGroupsA = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueGroupsA", numUniqueKeyA * sizeof(contactPairs_t)); + contactPairs_t* countsA = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueCountsA", numUniqueKeyA * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numGroupsA"); + cubDEMRunLengthEncode( + uniqueGroupA, uniqueGroupsA, countsA, scratchPad.getDualStructDevice("numGroupsA"), + numUniqueKeyA, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numGroupsA"); + size_t numGroupsA = *scratchPad.getDualStructHost("numGroupsA"); + if (numGroupsA > 0) { + size_t blocks_needed_groups = + (numGroupsA + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + scatterGroupCounts<<>>(uniqueGroupsA, countsA, groupUniqueCountA, numGroupsA); + DEME_GPU_DEBUG_SYNC(this_stream); + } + scratchPad.finishUsingTempVector("uniqueGroupsA"); + scratchPad.finishUsingTempVector("uniqueCountsA"); + scratchPad.finishUsingDualStruct("numGroupsA"); + scratchPad.finishUsingTempVector("uniqueGroupA"); + } + scratchPad.finishUsingTempVector("uniqueKeyA"); + scratchPad.finishUsingDualStruct("numUniqueKeyA"); + scratchPad.finishUsingTempVector("groupPrimKeyA_sorted"); + scratchPad.finishUsingTempVector("groupPrimKeyA"); + + uint64_t* keyB = + (uint64_t*)scratchPad.allocateTempVector("groupPrimKeyB", numTotalCnts * sizeof(uint64_t)); + uint64_t* keyB_sorted = (uint64_t*)scratchPad.allocateTempVector("groupPrimKeyB_sorted", + numTotalCnts * sizeof(uint64_t)); + if (blocks_needed_for_patch_ids > 0) { + buildGroupPrimitiveKeys<<>>(groupIndex, granData->idPrimitiveB, keyB, + numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + cubDEMSortKeys(keyB, keyB_sorted, numTotalCnts, this_stream, scratchPad); + + uint64_t* uniqueKeyB = + (uint64_t*)scratchPad.allocateTempVector("uniqueKeyB", numTotalCnts * sizeof(uint64_t)); + scratchPad.allocateDualStruct("numUniqueKeyB"); + cubDEMUnique(keyB_sorted, uniqueKeyB, scratchPad.getDualStructDevice("numUniqueKeyB"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueKeyB"); + size_t numUniqueKeyB = *scratchPad.getDualStructHost("numUniqueKeyB"); + if (numUniqueKeyB > 0) { + contactPairs_t* uniqueGroupB = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueGroupB", numUniqueKeyB * sizeof(contactPairs_t)); + size_t blocks_needed_unique = + (numUniqueKeyB + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + extractGroupIndexFromKey<<>>(uniqueKeyB, uniqueGroupB, numUniqueKeyB); + DEME_GPU_DEBUG_SYNC(this_stream); + + contactPairs_t* uniqueGroupsB = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueGroupsB", numUniqueKeyB * sizeof(contactPairs_t)); + contactPairs_t* countsB = (contactPairs_t*)scratchPad.allocateTempVector( + "uniqueCountsB", numUniqueKeyB * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numGroupsB"); + cubDEMRunLengthEncode( + uniqueGroupB, uniqueGroupsB, countsB, scratchPad.getDualStructDevice("numGroupsB"), + numUniqueKeyB, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numGroupsB"); + size_t numGroupsB = *scratchPad.getDualStructHost("numGroupsB"); + if (numGroupsB > 0) { + size_t blocks_needed_groups = + (numGroupsB + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + scatterGroupCounts<<>>(uniqueGroupsB, countsB, groupUniqueCountB, numGroupsB); + DEME_GPU_DEBUG_SYNC(this_stream); + } + scratchPad.finishUsingTempVector("uniqueGroupsB"); + scratchPad.finishUsingTempVector("uniqueCountsB"); + scratchPad.finishUsingDualStruct("numGroupsB"); + scratchPad.finishUsingTempVector("uniqueGroupB"); + } + scratchPad.finishUsingTempVector("uniqueKeyB"); + scratchPad.finishUsingDualStruct("numUniqueKeyB"); + scratchPad.finishUsingTempVector("groupPrimKeyB_sorted"); + scratchPad.finishUsingTempVector("groupPrimKeyB"); + + notStupidBool_t* groupWinnerIsA = (notStupidBool_t*)scratchPad.allocateTempVector( + "groupWinnerIsA", numGroups * sizeof(notStupidBool_t)); + notStupidBool_t* groupWinnerIsTri = (notStupidBool_t*)scratchPad.allocateTempVector( + "groupWinnerIsTri", numGroups * sizeof(notStupidBool_t)); + notStupidBool_t* groupForceSingleIsland = (notStupidBool_t*)scratchPad.allocateTempVector( + "groupForceSingleIsland", numGroups * sizeof(notStupidBool_t)); + if (numGroups > 0) { + size_t blocks_needed_groups = + (numGroups + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + computeGroupWinners<<>>( + groupContactTypes, groupPrimA, groupPrimB, groupUniqueCountA, groupUniqueCountB, + granData->ownerTriMesh, granData->ownerMeshConvex, granData->ownerMeshNeverWinner, + groupWinnerIsA, groupWinnerIsTri, groupForceSingleIsland, numGroups); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + bodyID_t* winnerPrimitive = + (bodyID_t*)scratchPad.allocateTempVector("winnerPrimitive", numTotalCnts * sizeof(bodyID_t)); + notStupidBool_t* winnerIsTri = (notStupidBool_t*)scratchPad.allocateTempVector( + "winnerIsTri", numTotalCnts * sizeof(notStupidBool_t)); + if (blocks_needed_for_patch_ids > 0) { + selectWinnerPrimitive<<>>( + groupIndex, granData->idPrimitiveA, granData->idPrimitiveB, groupWinnerIsA, + groupWinnerIsTri, groupForceSingleIsland, winnerPrimitive, winnerIsTri, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + uint64_t* activeTriKeysAll = + (uint64_t*)scratchPad.allocateTempVector("activeTriKeysAll", numTotalCnts * sizeof(uint64_t)); + notStupidBool_t* activeTriFlags = winnerIsTri; + if (blocks_needed_for_patch_ids > 0) { + buildActiveTriKeys<<>>(groupIndex, winnerPrimitive, activeTriFlags, + activeTriKeysAll, activeTriFlags, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + uint64_t* activeTriKeys = + (uint64_t*)scratchPad.allocateTempVector("activeTriKeys", numTotalCnts * sizeof(uint64_t)); + scratchPad.allocateDualStruct("numActiveTriKeys"); + cubDEMSelectFlagged(activeTriKeysAll, activeTriKeys, activeTriFlags, + scratchPad.getDualStructDevice("numActiveTriKeys"), + numTotalCnts, this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numActiveTriKeys"); + size_t numActiveTriKeys = *scratchPad.getDualStructHost("numActiveTriKeys"); + + uint64_t* activeTriKeysUnique = nullptr; + bodyID_t* activeLabelsA = nullptr; + bodyID_t* activeLabelsB = nullptr; + contactPairs_t* groupActiveCount = nullptr; + contactPairs_t* groupActiveStart = nullptr; + bodyID_t* finalActiveLabels = nullptr; + size_t numUniqueActiveTri = 0; + + if (numActiveTriKeys > 0) { + uint64_t* activeTriKeys_sorted = (uint64_t*)scratchPad.allocateTempVector( + "activeTriKeys_sorted", numActiveTriKeys * sizeof(uint64_t)); + cubDEMSortKeys(activeTriKeys, activeTriKeys_sorted, numActiveTriKeys, this_stream, + scratchPad); + + activeTriKeysUnique = (uint64_t*)scratchPad.allocateTempVector( + "activeTriKeys_unique", numActiveTriKeys * sizeof(uint64_t)); + scratchPad.allocateDualStruct("numUniqueActiveTri"); + cubDEMUnique(activeTriKeys_sorted, activeTriKeysUnique, + scratchPad.getDualStructDevice("numUniqueActiveTri"), numActiveTriKeys, + this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueActiveTri"); + numUniqueActiveTri = *scratchPad.getDualStructHost("numUniqueActiveTri"); + + if (numUniqueActiveTri > 0) { + activeLabelsA = (bodyID_t*)scratchPad.allocateTempVector( + "activeTriLabelsA", numUniqueActiveTri * sizeof(bodyID_t)); + activeLabelsB = (bodyID_t*)scratchPad.allocateTempVector( + "activeTriLabelsB", numUniqueActiveTri * sizeof(bodyID_t)); + size_t blocks_needed_active = + (numUniqueActiveTri + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + initActiveTriLabels<<>>(activeTriKeysUnique, activeLabelsA, + numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + initActiveTriLabels<<>>(activeTriKeysUnique, activeLabelsB, + numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + + groupActiveCount = (contactPairs_t*)scratchPad.allocateTempVector( + "groupActiveCount", numGroups * sizeof(contactPairs_t)); + DEME_GPU_CALL_ASYNC( + cudaMemsetAsync(groupActiveCount, 0, numGroups * sizeof(contactPairs_t), this_stream), + this_stream); + countActiveTriPerGroup<<>>(activeTriKeysUnique, groupActiveCount, + numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + + groupActiveStart = (contactPairs_t*)scratchPad.allocateTempVector( + "groupActiveStart", numGroups * sizeof(contactPairs_t)); + if (numGroups > 0) { + cubDEMPrefixScan(groupActiveCount, groupActiveStart, + numGroups, this_stream, scratchPad); + } + + contactPairs_t* activeTriNeighborPos = (contactPairs_t*)scratchPad.allocateTempVector( + "activeTriNeighborPos", numUniqueActiveTri * 3 * sizeof(contactPairs_t)); + buildActiveTriNeighborPos<<>>( + activeTriKeysUnique, groupActiveStart, groupActiveCount, granData->triNeighborIndex, + granData->triNeighbor1, granData->triNeighbor2, granData->triNeighbor3, + activeTriNeighborPos, numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + + const int kCheckEvery = 4; + const int kMaxLabelItersCap = 512; + int maxIters = (int)numUniqueActiveTri; + if (maxIters > kMaxLabelItersCap) { + maxIters = kMaxLabelItersCap; + } + scratchPad.allocateDualArray("activeTriLabelChanged", sizeof(contactPairs_t)); + auto* changedHostRaw = scratchPad.getDualArrayHost("activeTriLabelChanged"); + auto* changedDevRaw = scratchPad.getDualArrayDevice("activeTriLabelChanged"); + contactPairs_t* changedDev = reinterpret_cast(changedDevRaw); + + bodyID_t* labelsIn = activeLabelsA; + bodyID_t* labelsOut = activeLabelsB; + int iter = 0; + while (iter < maxIters) { + int remaining = maxIters - iter; + int batch = remaining < kCheckEvery ? remaining : kCheckEvery; + + for (int b = 0; b < batch - 1; ++b) { + propagateActiveTriLabelsFromNeighborPos<<< + dim3(blocks_needed_active), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream>>>( + labelsIn, labelsOut, activeTriNeighborPos, nullptr, numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + bodyID_t* tmp = labelsIn; + labelsIn = labelsOut; + labelsOut = tmp; + ++iter; + } + + DEME_GPU_CALL_ASYNC(cudaMemsetAsync(changedDev, 0, sizeof(contactPairs_t), this_stream), + this_stream); + propagateActiveTriLabelsFromNeighborPos<<< + dim3(blocks_needed_active), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream>>>( + labelsIn, labelsOut, activeTriNeighborPos, changedDev, numUniqueActiveTri); + DEME_GPU_DEBUG_SYNC(this_stream); + bodyID_t* tmp = labelsIn; + labelsIn = labelsOut; + labelsOut = tmp; + ++iter; + + scratchPad.syncDualArrayDeviceToHost("activeTriLabelChanged"); + const contactPairs_t changed = *reinterpret_cast(changedHostRaw); + if (changed == 0) { + break; + } + } + finalActiveLabels = labelsIn; + + scratchPad.finishUsingDualArray("activeTriLabelChanged"); + scratchPad.finishUsingTempVector("activeTriNeighborPos"); + } + + scratchPad.finishUsingTempVector("activeTriKeys_sorted"); + scratchPad.finishUsingDualStruct("numUniqueActiveTri"); + } + + bodyID_t* contactIslandLabel = + (bodyID_t*)scratchPad.allocateTempVector("contactIslandLabel", numTotalCnts * sizeof(bodyID_t)); + if (numUniqueActiveTri > 0) { + size_t blocks_needed_labels = + (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + assignContactIslandLabel<<>>( + groupIndex, winnerPrimitive, winnerIsTri, activeTriKeysUnique, finalActiveLabels, + groupActiveStart, groupActiveCount, contactIslandLabel, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } else { + size_t blocks_needed_labels = + (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_labels > 0) { + copyBodyIDArray<<>>(winnerPrimitive, contactIslandLabel, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + } + + uint64_t* islandKeyHi = + (uint64_t*)scratchPad.allocateTempVector("islandKeyHi", numTotalCnts * sizeof(uint64_t)); + uint64_t* islandKeyLo = + (uint64_t*)scratchPad.allocateTempVector("islandKeyLo", numTotalCnts * sizeof(uint64_t)); + if (blocks_needed_for_patch_ids > 0) { + buildIslandCompositeKeyParts<<>>( + contactPatchPairs, granData->contactTypePrimitive, contactIslandLabel, islandKeyHi, + islandKeyLo, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + contactPairs_t* island_sort_indices = + (contactPairs_t*)scratchPad.allocateTempVector("islandSortIndices", idx_arr_bytes); + contactPairs_t* island_sort_indices_sorted = + (contactPairs_t*)scratchPad.allocateTempVector("islandSortIndices_sorted", idx_arr_bytes); + if (blocks_needed_for_patch_ids > 0) { + lineNumbers<<>>(island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + uint64_t* islandKeyLo_sorted = + (uint64_t*)scratchPad.allocateTempVector("islandKeyLo_sorted", numTotalCnts * sizeof(uint64_t)); + cubDEMSortByKeys(islandKeyLo, islandKeyLo_sorted, island_sort_indices, + island_sort_indices_sorted, numTotalCnts, this_stream, + scratchPad); + + uint64_t* islandKeyHi_by_lo = + (uint64_t*)scratchPad.allocateTempVector("islandKeyHi_by_lo", numTotalCnts * sizeof(uint64_t)); + if (blocks_needed_for_patch_ids > 0) { + gatherByIndex + <<>>( + islandKeyHi, islandKeyHi_by_lo, island_sort_indices_sorted, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + uint64_t* islandKeyHi_sorted = + (uint64_t*)scratchPad.allocateTempVector("islandKeyHi_sorted", numTotalCnts * sizeof(uint64_t)); + cubDEMSortByKeys(islandKeyHi_by_lo, islandKeyHi_sorted, + island_sort_indices_sorted, island_sort_indices, + numTotalCnts, this_stream, scratchPad); + + if (blocks_needed_for_patch_ids > 0) { + gatherByIndex + <<>>( + granData->idPrimitiveA, idA_sorted, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->idPrimitiveB, idB_sorted, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactTypePrimitive, contactType_sorted, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + granData->contactPersistency, contactPersistency_sorted, island_sort_indices, + numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + gatherByIndex + <<>>( + contactPatchPairs, patchPairs_sorted, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + bodyID_t* contactIslandLabel_sorted = (bodyID_t*)scratchPad.allocateTempVector( + "contactIslandLabel_sorted", numTotalCnts * sizeof(bodyID_t)); + if (blocks_needed_for_patch_ids > 0) { + gatherByIndex + <<>>( + contactIslandLabel, contactIslandLabel_sorted, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->idPrimitiveA, idA_sorted, total_ids_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->idPrimitiveB, idB_sorted, total_ids_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactTypePrimitive, contactType_sorted, + type_arr_bytes, cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(granData->contactPersistency, contactPersistency_sorted, + total_persistency_bytes, cudaMemcpyDeviceToDevice, this_stream), + this_stream); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(contactPatchPairs, patchPairs_sorted, patch_arr_bytes, + cudaMemcpyDeviceToDevice, this_stream), + this_stream); + + contactPairs_t* isNewIslandGroup = (contactPairs_t*)scratchPad.allocateTempVector( + "isNewIslandGroup", numTotalCnts * sizeof(contactPairs_t)); + if (blocks_needed_for_patch_ids > 0) { + uint64_t* islandKeyLo_sorted_by_hi = (uint64_t*)scratchPad.allocateTempVector( + "islandKeyLo_sorted_by_hi", numTotalCnts * sizeof(uint64_t)); + gatherByIndex + <<>>( + islandKeyLo, islandKeyLo_sorted_by_hi, island_sort_indices, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + markNewCompositeGroups64<<>>(islandKeyHi_sorted, islandKeyLo_sorted_by_hi, + isNewIslandGroup, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + scratchPad.finishUsingTempVector("islandKeyLo_sorted_by_hi"); + } + cubDEMInclusiveScan(isNewIslandGroup, granData->geomToPatchMap, + numTotalCnts, this_stream, scratchPad); + setFirstFlagToOne<<<1, 1, 0, this_stream>>>(isNewIslandGroup, numTotalCnts); + DEME_GPU_DEBUG_SYNC(this_stream); + + scratchPad.allocateDualStruct("numUniqueIslands"); + cubDEMSum(isNewIslandGroup, + scratchPad.getDualStructDevice("numUniqueIslands"), numTotalCnts, + this_stream, scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueIslands"); + size_t numUniqueIslands = *scratchPad.getDualStructHost("numUniqueIslands"); + + if (numUniqueIslands > idPatchA.size()) { + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPatchA, numUniqueIslands); + DEME_DUAL_ARRAY_RESIZE_NOVAL(idPatchB, numUniqueIslands); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactTypePatch, numUniqueIslands); + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactPatchIsland, numUniqueIslands); + granData.toDevice(); + } + + patchIDPair_t* unique_patch_pairs = nullptr; + if (numUniqueIslands > 0) { + unique_patch_pairs = (patchIDPair_t*)scratchPad.allocateTempVector( + "unique_patch_pairs", numUniqueIslands * sizeof(patchIDPair_t)); + cubDEMSelectFlagged( + contactPatchPairs, unique_patch_pairs, isNewIslandGroup, + scratchPad.getDualStructDevice("numUniqueIslands"), numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged( + granData->contactTypePrimitive, granData->contactTypePatch, isNewIslandGroup, + scratchPad.getDualStructDevice("numUniqueIslands"), numTotalCnts, this_stream, scratchPad); + cubDEMSelectFlagged( + contactIslandLabel_sorted, contactPatchIsland.data(), isNewIslandGroup, + scratchPad.getDualStructDevice("numUniqueIslands"), numTotalCnts, this_stream, scratchPad); + size_t blocks_needed_for_decode = + (numUniqueIslands + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + decodePatchPairsToSeparateArrays<<>>( + unique_patch_pairs, granData->idPatchA, granData->idPatchB, numUniqueIslands); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + *scratchPad.numContacts = numUniqueIslands; + + if (solverFlags.useStablePatchIslandIDs) { + stabilizeFloodedPatchIslandIDs(granData, previous_primitivePatchIsland, numTotalCnts, + numUniqueIslands, *scratchPad.numPrevPrimitiveContacts, + this_stream, scratchPad); + } + + typeStartCountPatchMap_thisStep.SetAll({0, 0}); + if (numUniqueIslands > 0) { + const size_t type_buf_len = DEME_MAX((size_t)1, numUniqueIslands); + contact_t* unique_types = + (contact_t*)scratchPad.allocateTempVector("unique_types", type_buf_len * sizeof(contact_t)); + contactPairs_t* type_counts = (contactPairs_t*)scratchPad.allocateTempVector( + "type_counts", type_buf_len * sizeof(contactPairs_t)); + scratchPad.allocateDualStruct("numUniqueTypes"); + + cubDEMRunLengthEncode( + granData->contactTypePatch, unique_types, type_counts, + scratchPad.getDualStructDevice("numUniqueTypes"), numUniqueIslands, this_stream, + scratchPad); + scratchPad.syncDualStructDeviceToHost("numUniqueTypes"); + size_t numTypes = *scratchPad.getDualStructHost("numUniqueTypes"); + if (numTypes > type_buf_len) { + DEME_ERROR("Patch contact type run-length output (%zu) exceeds allocated buffer (%zu).", + numTypes, type_buf_len); + } + + if (numTypes > 0) { + contact_t* host_unique_types = new contact_t[numTypes]; + contactPairs_t* host_type_counts = new contactPairs_t[numTypes]; + DEME_GPU_CALL(cudaMemcpy(host_unique_types, unique_types, numTypes * sizeof(contact_t), + cudaMemcpyDeviceToHost)); + DEME_GPU_CALL(cudaMemcpy(host_type_counts, type_counts, numTypes * sizeof(contactPairs_t), + cudaMemcpyDeviceToHost)); + + contactPairs_t offset = 0; + for (size_t i = 0; i < numTypes; i++) { + const contact_t type = host_unique_types[i]; + const contactPairs_t cnt = host_type_counts[i]; + if (isSupportedContactType(type) && cnt > 0) { + typeStartCountPatchMap_thisStep[type] = {offset, cnt}; + } else if (cnt > 0) { + DEME_ERROR("Unsupported patch contact type %d survived kT patch derivation.", type); + } + offset += host_type_counts[i]; + } + delete[] host_unique_types; + delete[] host_type_counts; + } + + scratchPad.finishUsingTempVector("unique_types"); + scratchPad.finishUsingTempVector("type_counts"); + scratchPad.finishUsingDualStruct("numUniqueTypes"); + } + + if (unique_patch_pairs) { + scratchPad.finishUsingTempVector("unique_patch_pairs"); + } + scratchPad.finishUsingTempVector("contactIslandLabel_sorted"); + if (groupContactTypes) { + scratchPad.finishUsingTempVector("groupContactTypes"); + } + if (groupPrimA) { + scratchPad.finishUsingTempVector("groupPrimA"); + } + if (groupPrimB) { + scratchPad.finishUsingTempVector("groupPrimB"); + } + scratchPad.finishUsingTempVector("groupUniqueCountA"); + scratchPad.finishUsingTempVector("groupUniqueCountB"); + scratchPad.finishUsingTempVector("groupWinnerIsA"); + scratchPad.finishUsingTempVector("groupWinnerIsTri"); + scratchPad.finishUsingTempVector("groupForceSingleIsland"); + scratchPad.finishUsingTempVector("winnerPrimitive"); + scratchPad.finishUsingTempVector("winnerIsTri"); + scratchPad.finishUsingTempVector("activeTriKeysAll"); + scratchPad.finishUsingTempVector("activeTriKeys"); + scratchPad.finishUsingDualStruct("numActiveTriKeys"); + if (activeTriKeysUnique) { + scratchPad.finishUsingTempVector("activeTriKeys_unique"); + } + if (activeLabelsA) { + scratchPad.finishUsingTempVector("activeTriLabelsA"); + } + if (activeLabelsB) { + scratchPad.finishUsingTempVector("activeTriLabelsB"); + } + if (groupActiveCount) { + scratchPad.finishUsingTempVector("groupActiveCount"); + } + if (groupActiveStart) { + scratchPad.finishUsingTempVector("groupActiveStart"); + } + scratchPad.finishUsingTempVector("contactIslandLabel"); + scratchPad.finishUsingTempVector("islandKeyHi"); + scratchPad.finishUsingTempVector("islandKeyLo"); + scratchPad.finishUsingTempVector("islandKeyLo_sorted"); + scratchPad.finishUsingTempVector("islandKeyHi_by_lo"); + scratchPad.finishUsingTempVector("islandKeyHi_sorted"); + scratchPad.finishUsingTempVector("islandSortIndices"); + scratchPad.finishUsingTempVector("islandSortIndices_sorted"); + scratchPad.finishUsingTempVector("isNewIslandGroup"); + scratchPad.finishUsingDualStruct("numUniqueIslands"); + // ---- END ISLAND-FLOODING CORE ---- + } + + scratchPad.finishUsingTempVector("groupIndex"); + scratchPad.finishUsingTempVector("isNewGroup"); + scratchPad.finishUsingDualStruct("numUniqueGroups"); + scratchPad.finishUsingTempVector("contactPatchPairs"); + scratchPad.finishUsingTempVector("patchPairs_sorted"); + scratchPad.finishUsingTempVector("contactSortIndices"); + scratchPad.finishUsingTempVector("contactSortIndices_sorted"); + scratchPad.finishUsingTempVector("idA_sorted"); + scratchPad.finishUsingTempVector("idB_sorted"); + scratchPad.finishUsingTempVector("contactType_sorted"); + scratchPad.finishUsingTempVector("contactPersistency_sorted"); + scratchPad.finishUsingTempVector("contactTypeIndices"); + scratchPad.finishUsingTempVector("contactTypeIndices_sorted"); + + // =============================================================================================== + // END COMPLEX ISLAND-FLOODING PATCH ROUTE + // =============================================================================================== + } + // std::cout << "Patch contacts:" << std::endl; + // displayDeviceArray(granData->idPatchA, *scratchPad.numContacts); + // displayDeviceArray(granData->idPatchB, *scratchPad.numContacts); + // displayDeviceArray(granData->contactTypePatch, *scratchPad.numContacts); + // displayDeviceArray(granData->geomToPatchMap, *scratchPad.numPrimitiveContacts); + } + + timers.GetTimer("Find contact pairs").stop(); + + } // End of contact pairs construction of this CD step + + // ----------------------------------------------------------------------------------------------------------- + // Constructing contact history (patch-based) + // The contact mapping is now being built between previous and current patch contacts. + // We have numContacts elements to work with (patch-based contacts), not numPrimitiveContacts. + // The physics-facing patch arrays keep their original A/B order. For mesh-related history migration, we build + // temporary history keys; MM keys canonicalize the patch pair, so swapped A/B emission across kT passes does not + // make a physical contact look new. + // ----------------------------------------------------------------------------------------------------------- + + timers.GetTimer("Build history map").start(); + // Now, identify enduring contacts in history-based models. + if (*scratchPad.numContacts > 0) { + // For history-based models, construct the enduring contact map at the patch level. + // Only need to actually create the mapping if the force model has history + if (!solverFlags.isHistoryless) { + // Resize contactMapping to hold numContacts elements (patch-based) + if (*scratchPad.numContacts > contactMapping.size()) { + DEME_DUAL_ARRAY_RESIZE_NOVAL(contactMapping, *scratchPad.numContacts); + granData.toDevice(); + } + + // Build patch-based contact mapping using physical contact types only. NOT_A_CONTACT sentinels must have + // been compacted before patch-contact derivation; if one reaches this map, dT should fail when dispatching. + for (size_t type_idx = 0; type_idx < NUM_SUPPORTED_CONTACT_TYPES; type_idx++) { + contact_t thisType = ALL_CONTACT_TYPES[type_idx]; + + // Get start/count for this type in the current and previous steps. + const auto& curr_info = typeStartCountPatchMap_thisStep[thisType]; + const auto& prev_info = typeStartCountPatchMap[thisType]; + + contactPairs_t curr_start = curr_info.first; + contactPairs_t curr_count = curr_info.second; + contactPairs_t prev_start = prev_info.first; + contactPairs_t prev_count = prev_info.second; + + // Skip if no contacts of this type in current step + if (curr_count == 0) { + continue; + } + + // Launch appropriate kernel based on whether previous step had this type + size_t blocks_needed = (curr_count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + + if (prev_count == 0) { + // Previous step has no contacts of this type - set all to NULL_MAPPING_PARTNER + setNullMappingForType<<>>( + granData->contactMapping, curr_start, curr_count); + DEME_GPU_DEBUG_SYNC(this_stream); + } else { + // Both steps have contacts of this type - perform mapping. + if (usesStablePatchIslandHistory(thisType)) { + // Mesh-related contacts use a history-only identity that is independent of force-kernel A/B + // ordering. MM contacts canonicalize the patch pair here, so a physical contact emitted as + // (patchA, patchB) on one kT pass and (patchB, patchA) on another still migrates history. + // idPatchA/idPatchB themselves are not modified, preserving force/material/owner semantics. + const size_t prev_key_bytes = prev_count * sizeof(uint64_t); + const size_t prev_idx_bytes = prev_count * sizeof(contactPairs_t); + uint64_t* prev_history_keys = + (uint64_t*)scratchPad.allocateTempVector("prevPatchHistoryKeys", prev_key_bytes); + uint64_t* prev_history_keys_sorted = + (uint64_t*)scratchPad.allocateTempVector("prevPatchHistoryKeys_sorted", prev_key_bytes); + contactPairs_t* prev_history_indices = + (contactPairs_t*)scratchPad.allocateTempVector("prevPatchHistoryIndices", prev_idx_bytes); + contactPairs_t* prev_history_indices_sorted = (contactPairs_t*)scratchPad.allocateTempVector( + "prevPatchHistoryIndices_sorted", prev_idx_bytes); + + const size_t prev_blocks_needed = + (prev_count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (prev_blocks_needed > 0) { + buildPatchHistoryPairKeys<<>>( + granData->previous_idPatchA, granData->previous_idPatchB, prev_history_keys, + prev_history_indices, prev_start, prev_count, thisType); + DEME_GPU_DEBUG_SYNC(this_stream); + } + cubDEMSortByKeys(prev_history_keys, prev_history_keys_sorted, + prev_history_indices, prev_history_indices_sorted, + prev_count, this_stream, scratchPad); + + buildPatchContactMappingByHistoryKeys<<>>( + granData->idPatchA, granData->idPatchB, granData->contactPatchIsland, + granData->previous_contactPatchIsland, prev_history_keys_sorted, + prev_history_indices_sorted, granData->contactMapping, curr_start, curr_count, prev_start, + prev_count, thisType); + DEME_GPU_DEBUG_SYNC(this_stream); + + scratchPad.finishUsingTempVector("prevPatchHistoryKeys"); + scratchPad.finishUsingTempVector("prevPatchHistoryKeys_sorted"); + scratchPad.finishUsingTempVector("prevPatchHistoryIndices"); + scratchPad.finishUsingTempVector("prevPatchHistoryIndices_sorted"); + } else { + buildPatchContactMappingForType<<>>( + granData->idPatchA, granData->idPatchB, granData->contactPatchIsland, + granData->previous_idPatchA, granData->previous_idPatchB, + granData->previous_contactPatchIsland, granData->contactMapping, curr_start, curr_count, + prev_start, prev_count); + DEME_GPU_DEBUG_SYNC(this_stream); + } + } + } + // std::cout << "Patch contact mapping:" << std::endl; + // displayDeviceArray(granData->contactMapping, *scratchPad.numContacts); + + // Copy current patch arrays to previous arrays for the next iteration + size_t patch_id_arr_bytes = (*scratchPad.numContacts) * sizeof(bodyID_t); + size_t patch_type_arr_bytes = (*scratchPad.numContacts) * sizeof(contact_t); + if (*scratchPad.numContacts > previous_idPatchA.size()) { + patchArraysResize(*scratchPad.numContacts, previous_idPatchA, previous_idPatchB, + previous_contactTypePatch, previous_contactPatchIsland, granData); + } + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPatchA, granData->idPatchA, patch_id_arr_bytes, + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPatchB, granData->idPatchB, patch_id_arr_bytes, + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_contactTypePatch, granData->contactTypePatch, + patch_type_arr_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_contactPatchIsland, granData->contactPatchIsland, + patch_id_arr_bytes, cudaMemcpyDeviceToDevice)); + + // Currently only when using persistent contacts we need to store enduring primitive contact info + if (solverFlags.hasPersistentContacts) { + // Resize previous primitive contact arrays if needed + if (*scratchPad.numPrimitiveContacts > previous_idPrimitiveA.size()) { + // Note persistency should not change size here even if called + primitiveContactArraysResize(*scratchPad.numPrimitiveContacts, previous_idPrimitiveA, + previous_idPrimitiveB, previous_contactTypePrimitive, + contactPersistency, granData); + } + // Copy current primitive contact arrays to previous arrays for the next iteration + size_t primitive_id_arr_bytes = (*scratchPad.numPrimitiveContacts) * sizeof(bodyID_t); + size_t primitive_type_arr_bytes = (*scratchPad.numPrimitiveContacts) * sizeof(contact_t); + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPrimitiveA, granData->idPrimitiveA, + primitive_id_arr_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPrimitiveB, granData->idPrimitiveB, + primitive_id_arr_bytes, cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_contactTypePrimitive, granData->contactTypePrimitive, + primitive_type_arr_bytes, cudaMemcpyDeviceToDevice)); + } + + // Update the patch contact map for the next iteration + typeStartCountPatchMap = typeStartCountPatchMap_thisStep; + + } // End of history-based model mapping + + } // End of contact sorting--mapping subroutine + timers.GetTimer("Build history map").stop(); + + // std::cout << "Patch Contacts: " << std::endl; + // displayDeviceArray(granData->idPatchA, *scratchPad.numContacts); + // displayDeviceArray(granData->idPatchB, *scratchPad.numContacts); + // displayDeviceArray(granData->contactTypePatch, *scratchPad.numContacts); + + if (solverFlags.useStablePatchIslandIDs && !solverFlags.useSimplePatchCombination && + *scratchPad.numPrimitiveContacts > 0) { + // Snapshot current primitive contacts and their final stable patch-island label for the next kT step. This is + // only active once the flooding path is enabled; simple patch grouping remains behavior-neutral for now. + if (*scratchPad.numPrimitiveContacts > previous_idPrimitiveA.size() || + *scratchPad.numPrimitiveContacts > previous_primitivePatchIsland.size()) { + primitiveHistoryArraysResize(*scratchPad.numPrimitiveContacts, previous_idPrimitiveA, previous_idPrimitiveB, + previous_contactTypePrimitive, previous_primitivePatchIsland, granData); + } + + const size_t primitive_id_arr_bytes = (*scratchPad.numPrimitiveContacts) * sizeof(bodyID_t); + const size_t primitive_type_arr_bytes = (*scratchPad.numPrimitiveContacts) * sizeof(contact_t); + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPrimitiveA, granData->idPrimitiveA, primitive_id_arr_bytes, + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_idPrimitiveB, granData->idPrimitiveB, primitive_id_arr_bytes, + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(granData->previous_contactTypePrimitive, granData->contactTypePrimitive, + primitive_type_arr_bytes, cudaMemcpyDeviceToDevice)); + + const size_t blocks_needed = + (*scratchPad.numPrimitiveContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + fillPrimitivePatchIslandLabels<<>>( + granData->geomToPatchMap, granData->contactPatchIsland, granData->contactTypePrimitive, + previous_primitivePatchIsland.data(), *scratchPad.numPrimitiveContacts); + DEME_GPU_DEBUG_SYNC(this_stream); + } + + // Finally, don't forget to store the number of contacts for the next iteration, even if there is 0 contacts (in + // that case, mapping will not be constructed, but we don't have to worry b/c in the next iteration, simply no work + // will be done for the old array and every contact will be new) + *scratchPad.numPrevContacts = *scratchPad.numContacts; + *scratchPad.numPrevPrimitiveContacts = *scratchPad.numPrimitiveContacts; + *scratchPad.numPrevSpheres = simParams->nSpheresGM; + *scratchPad.numPrevTriangles = simParams->nTriGM; + *scratchPad.numPrevMeshPatches = simParams->nMeshPatches; + + // dT kT may send these numbers to each other from device + scratchPad.numContacts.toDevice(); + scratchPad.numPrevContacts.toDevice(); + scratchPad.numPrimitiveContacts.toDevice(); + scratchPad.numPrevPrimitiveContacts.toDevice(); + scratchPad.numPrevSpheres.toDevice(); + scratchPad.numPrevTriangles.toDevice(); + scratchPad.numPrevMeshPatches.toDevice(); +} + +void overwritePrevContactArrays(DualStruct& kT_data, + DualStruct& dT_data, + DualArray& previous_idPatchA, + DualArray& previous_idPatchB, + DualArray& previous_contactTypePatch, + DualArray& previous_contactPatchIsland, + ContactTypeMap>& typeStartCountPatchMap, + DualStruct& simParams, + DEMSolverScratchData& scratchPad, + cudaStream_t& this_stream, + size_t nContacts) { + // Make sure the storage is large enough + if (nContacts > previous_idPatchA.size()) { + patchArraysResize(nContacts, previous_idPatchA, previous_idPatchB, previous_contactTypePatch, + previous_contactPatchIsland, kT_data); + } + + // No sort, copy over + DEME_GPU_CALL(cudaMemcpy(kT_data->previous_idPatchA, dT_data->idPatchA, nContacts * sizeof(bodyID_t), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(kT_data->previous_idPatchB, dT_data->idPatchB, nContacts * sizeof(bodyID_t), + cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(kT_data->previous_contactTypePatch, dT_data->contactTypePatch, + nContacts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); + DEME_GPU_CALL(cudaMemcpy(kT_data->previous_contactPatchIsland, dT_data->contactPatchIsland, + nContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); + + // Derive typeStartCountPatchMap from the loaded contact arrays + // This is necessary for the persistent mapping process in the next contact detection step + typeStartCountPatchMap.SetAll({0, 0}); + if (nContacts > 0) { + // Use run-length encoding to identify contact type boundaries + size_t type_buf_len = DEME_MAX((size_t)1, nContacts); + size_t typeCounts_bytes = type_buf_len * sizeof(contactPairs_t); + contactPairs_t* typeCounts = (contactPairs_t*)scratchPad.allocateTempVector("typeCounts", typeCounts_bytes); + + size_t existingTypes_bytes = type_buf_len * sizeof(contact_t); + contact_t* existingTypes = (contact_t*)scratchPad.allocateTempVector("existingTypes", existingTypes_bytes); + + scratchPad.allocateDualStruct("numExistingTypes"); + + // Run-length encode the contact types to find boundaries + cubDEMRunLengthEncode(kT_data->previous_contactTypePatch, existingTypes, typeCounts, + scratchPad.getDualStructDevice("numExistingTypes"), nContacts, + this_stream, scratchPad); + + scratchPad.syncDualStructDeviceToHost("numExistingTypes"); + size_t numExistingTypes = *scratchPad.getDualStructHost("numExistingTypes"); + if (numExistingTypes > type_buf_len) { + DEME_ERROR("Prev patch contact type run-length output (%zu) exceeds allocated buffer (%zu).", + numExistingTypes, type_buf_len); + } + + // Prefix scan to get start offsets + size_t typeOffsets_bytes = type_buf_len * sizeof(contactPairs_t); + contactPairs_t* typeOffsets = (contactPairs_t*)scratchPad.allocateTempVector("typeOffsets", typeOffsets_bytes); + + cubDEMPrefixScan(typeCounts, typeOffsets, numExistingTypes, this_stream, + scratchPad); + + // Copy results to host to populate the map + contact_t* host_existingTypes = new contact_t[numExistingTypes]; + contactPairs_t* host_typeOffsets = new contactPairs_t[numExistingTypes]; + + DEME_GPU_CALL(cudaMemcpy(host_existingTypes, existingTypes, numExistingTypes * sizeof(contact_t), + cudaMemcpyDeviceToHost)); + DEME_GPU_CALL(cudaMemcpy(host_typeOffsets, typeOffsets, numExistingTypes * sizeof(contactPairs_t), + cudaMemcpyDeviceToHost)); + + // Build the typeStartCountPatchMap + typeStartCountPatchMap.SetAll({0, 0}); + for (size_t i = 0; i < numExistingTypes; i++) { + const contact_t type = host_existingTypes[i]; + contactPairs_t startOffset = host_typeOffsets[i]; + contactPairs_t count = + (i + 1 < numExistingTypes ? host_typeOffsets[i + 1] : (contactPairs_t)nContacts) - startOffset; + if (isSupportedContactType(type) && count > 0) { + typeStartCountPatchMap[type] = {startOffset, count}; + } + } + + delete[] host_existingTypes; + delete[] host_typeOffsets; + + scratchPad.finishUsingTempVector("typeCounts"); + scratchPad.finishUsingTempVector("existingTypes"); + scratchPad.finishUsingTempVector("typeOffsets"); + scratchPad.finishUsingDualStruct("numExistingTypes"); + } + + *scratchPad.numPrevContacts = nContacts; + // If nSpheresGM is updated, then it should have been taken care of in the init/populate array phase and in kT's + // simParams now + *scratchPad.numPrevSpheres = simParams->nSpheresGM; + *scratchPad.numPrevTriangles = simParams->nTriGM; + // dT kT may send these numbers to each other from device + scratchPad.numPrevContacts.toDevice(); + scratchPad.numPrevSpheres.toDevice(); + scratchPad.numPrevTriangles.toDevice(); +} + +} // namespace deme diff --git a/src/algorithms/DEMContactDetectionKernels.cuh b/src/algorithms/DEMContactDetectionKernels.cuh new file mode 100644 index 00000000..fc021bd5 --- /dev/null +++ b/src/algorithms/DEMContactDetectionKernels.cuh @@ -0,0 +1,1066 @@ +// DEM history mapping related custom kernels + +#ifndef DEME_CD_KERNELS_CUH +#define DEME_CD_KERNELS_CUH + +#include +#include + +#include + +namespace deme { + +// Kernel to add a constant offset to each element of an array +__global__ void addOffsetToArray(contactPairs_t* arr, contactPairs_t offset, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + arr[myID] += offset; + } +} + +// Kernel to fill a contact_t array with a constant value +__global__ void fillContactTypeArray(contact_t* arr, contact_t val, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + arr[myID] = val; + } +} + +__global__ void markBoolIf(notStupidBool_t* bool_arr, notStupidBool_t* value_arr, notStupidBool_t val, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + notStupidBool_t my_val = value_arr[myID]; + if (my_val == val) { + bool_arr[myID] = 1; + } else { + bool_arr[myID] = 0; + } + } +} + +// Mark primitive contacts that should survive into patch-contact derivation. NOT_A_CONTACT entries are temporary +// sentinels emitted by CD kernels; they do not carry valid patch identity or force/history state. +__global__ void markPhysicalPrimitiveContacts(notStupidBool_t* keep_flags, const contact_t* contact_types, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + keep_flags[myID] = (contact_types[myID] == NOT_A_CONTACT) ? 0 : 1; + } +} + +__global__ void setArr(notStupidBool_t* arr, size_t n, notStupidBool_t val) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + arr[myID] = val; + } +} + +__global__ void markDuplicateContacts(primitivesPrimTouches_t* idA_runlength, + contactPairs_t* idA_scanned_runlength, + bodyID_t* idB, + contact_t* contactTypePrimitive, + notStupidBool_t* persistency, + notStupidBool_t* retain_list, + size_t n, + bool persistency_affect) { + bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + primitivesPrimTouches_t cnt_count = idA_runlength[myID]; + // If this idA has non-zero runlength in new: a potential removal needed + if (cnt_count > 0) { + // Where should I start looking? Grab the offset. + contactPairs_t cnt_offset = idA_scanned_runlength[myID]; + for (primitivesPrimTouches_t i = 0; i < cnt_count - 1; i++) { + for (primitivesPrimTouches_t j = i + 1; j < cnt_count; j++) { + // Current contact numbers we are inspecting + contactPairs_t contactA = cnt_offset + i; + contactPairs_t contactB = cnt_offset + j; + bodyID_t contactA_idB = idB[contactA]; + contact_t contactA_cntType = contactTypePrimitive[contactA]; + bodyID_t contactB_idB = idB[contactB]; + contact_t contactB_cntType = contactTypePrimitive[contactB]; + notStupidBool_t contactA_persistency, contactB_persistency; + if (persistency_affect) { + contactA_persistency = persistency[contactA]; + contactB_persistency = persistency[contactB]; + } + + if (contactA_idB == contactB_idB && contactA_cntType == contactB_cntType) { + // If so, they are the same contact, we have to remove one + if (!persistency_affect) { + // If persistency does not matter, we remove the one with bigger index, in case there are + // 3-way duplicates + retain_list[contactB] = 0; + } else { + if (contactA_persistency == contactB_persistency) { + // If both are persistent or both are non-persistent, we remove the one with bigger + // index, in case there are 3-way duplicates + retain_list[contactB] = 0; + } else if (contactB_persistency == CONTACT_NOT_PERSISTENT) { + retain_list[contactB] = 0; + } else { + retain_list[contactA] = 0; + } + } + } + } + } + } + } +} + +__global__ void extractPatchInvolvedContactPatchIDPairs(patchIDPair_t* contactPatchPairs, + contact_t* contactTypePrimitive, + bodyID_t* idPrimitiveA, + bodyID_t* idPrimitiveB, + bodyID_t* triPatchID, + size_t nContacts) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nContacts) { + bodyID_t bodyA = idPrimitiveA[myID]; + bodyID_t bodyB = idPrimitiveB[myID]; + switch (contactTypePrimitive[myID]) { + case SPHERE_TRIANGLE_CONTACT: { + bodyID_t patchB = triPatchID[bodyB]; + // For sphere-triangle contact: triangle has patch ID, sphere does not but its geoID serves the same + // purpose. We ensure sphere is A. + contactPatchPairs[myID] = + encodeType(bodyA, patchB); // Input bodyID_t, return patchIDPair_t + break; + } + case TRIANGLE_ANALYTICAL_CONTACT: { + bodyID_t patchA = triPatchID[bodyA]; + // For mesh-analytical contact: mesh has patch ID, analytical object does not but its geoID serves the + // same purpose. We ensure triangle is A. + contactPatchPairs[myID] = encodeType(patchA, bodyB); + break; + } + case TRIANGLE_TRIANGLE_CONTACT: { + bodyID_t patchA = triPatchID[bodyA]; + bodyID_t patchB = triPatchID[bodyB]; + // For triangle-triangle contact: both triangles have patch IDs. Keeps original order. + contactPatchPairs[myID] = encodeType(patchA, patchB); + break; + } + default: + // In other sph-sph and sph-anal, for now, we just use geoID as patchIDs. Keeps original order. + contactPatchPairs[myID] = encodeType(bodyA, bodyB); + break; + } + } +} + +// Decode unique patch pairs into separate idPatchA/idPatchB arrays +__global__ void decodePatchPairsToSeparateArrays(patchIDPair_t* uniquePatchPairs, + bodyID_t* idPatchA, + bodyID_t* idPatchB, + size_t numUnique) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < numUnique) { + patchIDPair_t patchPair = uniquePatchPairs[myID]; + // decodeTypeA returns the value stored in high bits, and decodeTypeB returns the value stored in low bits. This + // decoding does not change the order as they were encoded in encodeType. + idPatchA[myID] = decodeTypeA(patchPair); + idPatchB[myID] = decodeTypeB(patchPair); + } +} + +// Build geomToPatchMap by detecting boundaries of unique patch pairs in sorted array +// and using prefix scan on "is first of group" flags. +// For sorted patch pairs, mark 1 at each position where a new unique value starts, then prefix scan. +__global__ void markNewPatchPairGroups(patchIDPair_t* sortedPatchPairs, contactPairs_t* isNewGroup, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + if (myID == 0) { + isNewGroup[myID] = 0; + } else { + // Compare with previous element - if different, it's a new group + isNewGroup[myID] = (sortedPatchPairs[myID] != sortedPatchPairs[myID - 1]) ? 1 : 0; + } + } +} + +__global__ void markNewPatchPairGroupsByType(const patchIDPair_t* sortedPatchPairs, + const contact_t* sortedContactTypes, + contactPairs_t* isNewGroup, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + if (myID == 0) { + isNewGroup[myID] = 0; + } else { + const bool new_pair = sortedPatchPairs[myID] != sortedPatchPairs[myID - 1]; + const bool new_type = sortedContactTypes[myID] != sortedContactTypes[myID - 1]; + isNewGroup[myID] = (new_pair || new_type) ? 1 : 0; + } + } +} + +__global__ void setFirstFlagToOne(contactPairs_t* flags, size_t n) { + if (blockIdx.x == 0 && threadIdx.x == 0 && n > 0) { + flags[0] = 1; + } +} + +__global__ void lineNumbers(contactPairs_t* arr, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + arr[myID] = myID; + } +} + +template +__global__ void gatherByIndex(const T* in, T* out, const contactPairs_t* idx, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + out[myID] = in[idx[myID]]; + } +} + +__global__ void buildGroupPrimitiveKeys(const contactPairs_t* groupIndex, + const bodyID_t* primitiveIDs, + uint64_t* keys, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const bodyID_t phys_id = primitiveIDs[myID]; + keys[myID] = (static_cast(groupIndex[myID]) << 32) | static_cast(phys_id); + } +} + +__global__ void extractGroupIndexFromKey(const uint64_t* keys, contactPairs_t* groupIndex, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + groupIndex[myID] = static_cast(keys[myID] >> 32); + } +} + +__global__ void scatterGroupCounts(const contactPairs_t* groupIDs, + const contactPairs_t* counts, + contactPairs_t* groupCounts, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + groupCounts[groupIDs[myID]] = counts[myID]; + } +} + +__global__ void computeGroupWinners(const contact_t* groupTypes, + const bodyID_t* groupPrimA, + const bodyID_t* groupPrimB, + const contactPairs_t* countA, + const contactPairs_t* countB, + const bodyID_t* ownerTriMesh, + const notStupidBool_t* ownerMeshConvex, + const notStupidBool_t* ownerMeshNeverWinner, + notStupidBool_t* winnerIsA, + notStupidBool_t* winnerIsTri, + notStupidBool_t* forceSingleIsland, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const contact_t ctype = groupTypes[myID]; + const geoType_t typeA = decodeTypeA(ctype); + const geoType_t typeB = decodeTypeB(ctype); + const contactPairs_t nA = countA[myID]; + const contactPairs_t nB = countB[myID]; + const bool A_is_tri = (typeA == GEO_T_TRIANGLE); + const bool B_is_tri = (typeB == GEO_T_TRIANGLE); + bool A_convex = false; + bool B_convex = false; + bool A_never = false; + bool B_never = false; + if (A_is_tri) { + const bodyID_t triA = groupPrimA[myID]; + const bodyID_t ownerA = ownerTriMesh[triA]; + if (ownerA != NULL_BODYID) { + A_convex = (ownerMeshConvex[ownerA] != 0); + A_never = (ownerMeshNeverWinner[ownerA] != 0); + } + } + if (B_is_tri) { + const bodyID_t triB = groupPrimB[myID]; + const bodyID_t ownerB = ownerTriMesh[triB]; + if (ownerB != NULL_BODYID) { + B_convex = (ownerMeshConvex[ownerB] != 0); + B_never = (ownerMeshNeverWinner[ownerB] != 0); + } + } + const bool single_island = (A_is_tri && B_is_tri && A_convex && B_convex); + forceSingleIsland[myID] = single_island ? 1 : 0; + + notStupidBool_t pickA = 0; + if (ctype == SPHERE_TRIANGLE_CONTACT) { + pickA = 0; + } else if (A_never && B_never) { + pickA = 0; + } else if (A_never && !B_never) { + pickA = 0; + } else if (B_never && !A_never) { + pickA = 1; + } else if (nA > nB) { + pickA = 1; + } else if (nA < nB) { + pickA = 0; + } else { + if (A_is_tri && B_is_tri) { + if (A_convex != B_convex) { + pickA = A_convex ? 0 : 1; + } else { + pickA = 0; + } + } else if (A_is_tri && !B_is_tri) { + pickA = 1; + } else if (B_is_tri && !A_is_tri) { + pickA = 0; + } else { + pickA = 0; + } + } + winnerIsA[myID] = pickA; + if (single_island) { + winnerIsTri[myID] = 0; + } else { + const geoType_t winnerType = (pickA ? typeA : typeB); + winnerIsTri[myID] = (winnerType == GEO_T_TRIANGLE) ? 1 : 0; + } + } +} + +__global__ void selectWinnerPrimitive(const contactPairs_t* groupIndex, + const bodyID_t* idA, + const bodyID_t* idB, + const notStupidBool_t* groupWinnerIsA, + const notStupidBool_t* groupWinnerIsTri, + const notStupidBool_t* groupForceSingleIsland, + bodyID_t* winnerID, + notStupidBool_t* winnerIsTri, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const contactPairs_t grp = groupIndex[myID]; + if (groupForceSingleIsland[grp] != 0) { + winnerID[myID] = 0; + winnerIsTri[myID] = 0; + return; + } + const bool pickA = (groupWinnerIsA[grp] != 0); + winnerID[myID] = pickA ? idA[myID] : idB[myID]; + winnerIsTri[myID] = groupWinnerIsTri[grp]; + } +} + +__global__ void buildActiveTriKeys(const contactPairs_t* groupIndex, + const bodyID_t* winnerID, + const notStupidBool_t* winnerIsTri, + uint64_t* keys, + notStupidBool_t* flags, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const notStupidBool_t is_tri = winnerIsTri[myID]; + flags[myID] = is_tri; + if (is_tri) { + keys[myID] = (static_cast(groupIndex[myID]) << 32) | static_cast(winnerID[myID]); + } else { + keys[myID] = 0; + } + } +} + +__global__ void initActiveTriLabels(const uint64_t* keys, bodyID_t* labels, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + labels[myID] = static_cast(keys[myID] & 0xffffffffull); + } +} + +__global__ void countActiveTriPerGroup(const uint64_t* keys, contactPairs_t* groupCounts, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const contactPairs_t grp = static_cast(keys[myID] >> 32); + atomicAdd(&groupCounts[grp], (contactPairs_t)1); + } +} + +constexpr contactPairs_t DEME_INVALID_ACTIVE_TRI_POS = 0xffffffffu; + +__global__ void buildActiveTriNeighborPos(const uint64_t* keys, + const contactPairs_t* groupStart, + const contactPairs_t* groupCount, + const bodyID_t* triNeighborIndex, + const bodyID_t* triNeighbor1, + const bodyID_t* triNeighbor2, + const bodyID_t* triNeighbor3, + contactPairs_t* outNeighborPos, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const uint64_t key = keys[myID]; + const contactPairs_t grp = static_cast(key >> 32); + const bodyID_t triID = static_cast(key & 0xffffffffull); + const contactPairs_t start = groupStart[grp]; + const contactPairs_t count = groupCount[grp]; + + outNeighborPos[3 * myID + 0] = DEME_INVALID_ACTIVE_TRI_POS; + outNeighborPos[3 * myID + 1] = DEME_INVALID_ACTIVE_TRI_POS; + outNeighborPos[3 * myID + 2] = DEME_INVALID_ACTIVE_TRI_POS; + + const bodyID_t nb_idx = triNeighborIndex[triID]; + if (nb_idx == NULL_BODYID || count == 0) { + return; + } + + const bodyID_t nbs[3] = {triNeighbor1[nb_idx], triNeighbor2[nb_idx], triNeighbor3[nb_idx]}; + for (int e = 0; e < 3; ++e) { + const bodyID_t nb = nbs[e]; + if (nb == NULL_BODYID) { + continue; + } + const uint64_t target = (static_cast(grp) << 32) | static_cast(nb); + contactPairs_t left = 0; + contactPairs_t right = count; + while (left < right) { + const contactPairs_t mid = left + (right - left) / 2; + if (keys[start + mid] < target) { + left = mid + 1; + } else { + right = mid; + } + } + if (left < count && keys[start + left] == target) { + outNeighborPos[3 * myID + e] = start + left; + } + } + } +} + +__global__ void propagateActiveTriLabelsFromNeighborPos(const bodyID_t* labelsIn, + bodyID_t* labelsOut, + const contactPairs_t* neighborPos, + contactPairs_t* changedFlag, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + __shared__ int blockChanged; + if (threadIdx.x == 0) { + blockChanged = 0; + } + __syncthreads(); + + if (myID < n) { + const bodyID_t oldLabel = labelsIn[myID]; + bodyID_t label = oldLabel; + const contactPairs_t p0 = neighborPos[3 * myID + 0]; + const contactPairs_t p1 = neighborPos[3 * myID + 1]; + const contactPairs_t p2 = neighborPos[3 * myID + 2]; + + if (p0 != DEME_INVALID_ACTIVE_TRI_POS && labelsIn[p0] < label) { + label = labelsIn[p0]; + } + if (p1 != DEME_INVALID_ACTIVE_TRI_POS && labelsIn[p1] < label) { + label = labelsIn[p1]; + } + if (p2 != DEME_INVALID_ACTIVE_TRI_POS && labelsIn[p2] < label) { + label = labelsIn[p2]; + } + + labelsOut[myID] = label; + if (changedFlag != nullptr && label != oldLabel) { + atomicExch(&blockChanged, 1); + } + } + + __syncthreads(); + if (changedFlag != nullptr && threadIdx.x == 0 && blockChanged) { + atomicExch(changedFlag, (contactPairs_t)1); + } +} + +__global__ void assignContactIslandLabel(const contactPairs_t* groupIndex, + const bodyID_t* winnerID, + const notStupidBool_t* winnerIsTri, + const uint64_t* activeKeys, + const bodyID_t* activeLabels, + const contactPairs_t* groupStart, + const contactPairs_t* groupCount, + bodyID_t* outLabels, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const bodyID_t prim = winnerID[myID]; + if (winnerIsTri[myID] == 0) { + outLabels[myID] = prim; + return; + } + const contactPairs_t grp = groupIndex[myID]; + const contactPairs_t start = groupStart[grp]; + const contactPairs_t count = groupCount[grp]; + if (count == 0) { + outLabels[myID] = prim; + return; + } + const uint64_t target = (static_cast(grp) << 32) | static_cast(prim); + contactPairs_t left = 0; + contactPairs_t right = count; + while (left < right) { + const contactPairs_t mid = left + (right - left) / 2; + if (activeKeys[start + mid] < target) { + left = mid + 1; + } else { + right = mid; + } + } + outLabels[myID] = (left < count && activeKeys[start + left] == target) ? activeLabels[start + left] : prim; + } +} + +__global__ void copyBodyIDArray(const bodyID_t* in, bodyID_t* out, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + out[myID] = in[myID]; + } +} + +__global__ void buildIslandCompositeKeyParts(const patchIDPair_t* patchPairs, + const contact_t* contactTypes, + const bodyID_t* labels, + uint64_t* key_hi, + uint64_t* key_lo, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const patchIDPair_t pair = patchPairs[myID]; + const uint64_t hi = static_cast(pair >> 32); + const uint64_t lo = static_cast(pair & 0xffffffffull); + key_hi[myID] = (static_cast(contactTypes[myID]) << 32) | hi; + key_lo[myID] = (lo << 32) | static_cast(labels[myID]); + } +} + +__global__ void buildPrimitiveContactKeyParts(const bodyID_t* idA, + const bodyID_t* idB, + const contact_t* contactTypes, + uint64_t* key_hi, + uint64_t* key_lo, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + key_hi[myID] = (static_cast(contactTypes[myID]) << 32) | static_cast(idA[myID]); + key_lo[myID] = static_cast(idB[myID]); + } +} + +__global__ void buildStableIslandVotes(const bodyID_t* curr_idA, + const bodyID_t* curr_idB, + const contact_t* curr_types, + const contactPairs_t* curr_geomToPatchMap, + const uint64_t* prev_key_hi, + const uint64_t* prev_key_lo, + const bodyID_t* prev_primitiveIsland, + uint64_t* voteKeys, + notStupidBool_t* voteFlags, + size_t numCurr, + size_t numPrev) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID >= numCurr) { + return; + } + voteFlags[myID] = 0; + voteKeys[myID] = 0; + if (numPrev == 0 || !usesStablePatchIslandHistory(curr_types[myID])) { + return; + } + + const uint64_t curr_hi = (static_cast(curr_types[myID]) << 32) | static_cast(curr_idA[myID]); + const uint64_t curr_lo = static_cast(curr_idB[myID]); + size_t left = 0; + size_t right = numPrev; + while (left < right) { + const size_t mid = left + (right - left) / 2; + const uint64_t mid_hi = prev_key_hi[mid]; + const uint64_t mid_lo = prev_key_lo[mid]; + if (mid_hi < curr_hi || (mid_hi == curr_hi && mid_lo < curr_lo)) { + left = mid + 1; + } else { + right = mid; + } + } + if (left < numPrev && prev_key_hi[left] == curr_hi && prev_key_lo[left] == curr_lo) { + const bodyID_t prev_label = prev_primitiveIsland[left]; + if (prev_label != NULL_BODYID) { + const contactPairs_t patch_idx = curr_geomToPatchMap[myID]; + voteKeys[myID] = (static_cast(patch_idx) << 32) | static_cast(prev_label); + voteFlags[myID] = 1; + } + } +} + +__global__ void scatterBestStableIslandVote(const uint64_t* uniqueVoteKeys, + const contactPairs_t* counts, + unsigned long long* bestPacked, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const uint64_t vote_key = uniqueVoteKeys[myID]; + const contactPairs_t patch_idx = static_cast(vote_key >> 32); + const bodyID_t label = static_cast(vote_key & 0xffffffffull); + const unsigned long long packed = + (static_cast(counts[myID]) << 32) | + static_cast(0xffffffffull - static_cast(label)); + atomicMax(&bestPacked[patch_idx], packed); + } +} + +__global__ void applyBestStableIslandVotes(const unsigned long long* bestPacked, + bodyID_t* contactPatchIsland, + size_t nPatchContacts) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nPatchContacts) { + const unsigned long long packed = bestPacked[myID]; + if ((packed >> 32) != 0ull) { + contactPatchIsland[myID] = static_cast(0xffffffffull - (packed & 0xffffffffull)); + } + } +} + +__global__ void markNewCompositeGroups64(const uint64_t* key_hi, + const uint64_t* key_lo, + contactPairs_t* isNewGroup, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + if (myID == 0) { + isNewGroup[myID] = 0; + } else { + const bool new_hi = key_hi[myID] != key_hi[myID - 1]; + const bool new_lo = key_lo[myID] != key_lo[myID - 1]; + isNewGroup[myID] = (new_hi || new_lo) ? 1 : 0; + } + } +} + +// Set NULL_MAPPING_PARTNER for contacts of a specific type segment. +// Used when current step has contacts of this type but previous step does not. +// +// Parameters: +// contactMapping: Output mapping array (entire array, not segment) +// curr_start: Starting index in current arrays for this contact type segment +// curr_count: Number of contacts of this type in current step +__global__ void setNullMappingForType(contactPairs_t* contactMapping, + contactPairs_t curr_start, + contactPairs_t curr_count) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < curr_count) { + contactPairs_t curr_idx = curr_start + myID; + contactMapping[curr_idx] = NULL_MAPPING_PARTNER; + } +} + +inline __host__ __device__ uint64_t makePatchHistoryPairKey(bodyID_t patchA, bodyID_t patchB, contact_t type) { + if (type == TRIANGLE_TRIANGLE_CONTACT && patchB < patchA) { + const bodyID_t tmp = patchA; + patchA = patchB; + patchB = tmp; + } + return (static_cast(patchA) << 32) | static_cast(patchB); +} + +__global__ void buildPatchHistoryPairKeys(const bodyID_t* idPatchA, + const bodyID_t* idPatchB, + uint64_t* pairKeys, + contactPairs_t* relativeIndices, + contactPairs_t start, + contactPairs_t count, + contact_t type) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < count) { + const contactPairs_t idx = start + myID; + pairKeys[myID] = makePatchHistoryPairKey(idPatchA[idx], idPatchB[idx], type); + relativeIndices[myID] = myID; + } +} + +__global__ void buildPatchContactMappingByHistoryKeys(bodyID_t* curr_idPatchA, + bodyID_t* curr_idPatchB, + bodyID_t* curr_patchIsland, + bodyID_t* prev_patchIsland, + const uint64_t* prev_pairKeys_sorted, + const contactPairs_t* prev_relativeIndices_sorted, + contactPairs_t* contactMapping, + contactPairs_t curr_start, + contactPairs_t curr_count, + contactPairs_t prev_start, + contactPairs_t prev_count, + contact_t type) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < curr_count) { + const contactPairs_t curr_idx = curr_start + myID; + const uint64_t curr_key = makePatchHistoryPairKey(curr_idPatchA[curr_idx], curr_idPatchB[curr_idx], type); + const bodyID_t curr_label = curr_patchIsland[curr_idx]; + contactPairs_t my_partner = NULL_MAPPING_PARTNER; + + contactPairs_t left = 0; + contactPairs_t right = prev_count; + while (left < right) { + const contactPairs_t mid = left + (right - left) / 2; + if (prev_pairKeys_sorted[mid] < curr_key) { + left = mid + 1; + } else { + right = mid; + } + } + + for (contactPairs_t i = left; i < prev_count && prev_pairKeys_sorted[i] == curr_key; i++) { + const contactPairs_t prev_idx = prev_start + prev_relativeIndices_sorted[i]; + if (prev_patchIsland[prev_idx] == curr_label) { + my_partner = prev_idx; + break; + } + } + + contactMapping[curr_idx] = my_partner; + } +} + +// Build patch-based contact mapping for a single contact type segment. +// This kernel operates on a specific segment of the contact arrays identified by start offsets and counts. +// Each thread processes one current contact and searches for a match in the previous contacts of the same type. +// +// Parameters: +// curr_idPatchA, curr_idPatchB: Current step's patch contact arrays (entire arrays, not segment) +// prev_idPatchA, prev_idPatchB: Previous step's patch contact arrays (entire arrays, not segment) +// contactMapping: Output mapping array (entire array, not segment) +// curr_start: Starting index in current arrays for this contact type segment +// curr_count: Number of contacts of this type in current step +// prev_start: Starting index in previous arrays for this contact type segment +// prev_count: Number of contacts of this type in previous step +__global__ void buildPatchContactMappingForType(bodyID_t* curr_idPatchA, + bodyID_t* curr_idPatchB, + bodyID_t* curr_patchIsland, + bodyID_t* prev_idPatchA, + bodyID_t* prev_idPatchB, + bodyID_t* prev_patchIsland, + contactPairs_t* contactMapping, + contactPairs_t curr_start, + contactPairs_t curr_count, + contactPairs_t prev_start, + contactPairs_t prev_count) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < curr_count) { + // Absolute index in the full contact array + contactPairs_t curr_idx = curr_start + myID; + + bodyID_t curr_A = curr_idPatchA[curr_idx]; + bodyID_t curr_B = curr_idPatchB[curr_idx]; + bodyID_t curr_L = curr_patchIsland[curr_idx]; + + // Default: no match found + contactPairs_t my_partner = NULL_MAPPING_PARTNER; + + // Binary search within the previous type segment for the matching A/B pair + // The segment is sorted by the combined patch ID pair + contactPairs_t left = 0; + contactPairs_t right = prev_count; + while (left < right) { + contactPairs_t mid = left + (right - left) / 2; + contactPairs_t prev_idx = prev_start + mid; + bodyID_t prev_A = prev_idPatchA[prev_idx]; + bodyID_t prev_B = prev_idPatchB[prev_idx]; + + // Compare (A, B, label) triples lexicographically. + if (prev_A < curr_A || + (prev_A == curr_A && (prev_B < curr_B || (prev_B == curr_B && prev_patchIsland[prev_idx] < curr_L)))) { + left = mid + 1; + } else { + right = mid; + } + } + + // Check if we found a match at position left + if (left < prev_count) { + contactPairs_t prev_idx = prev_start + left; + bodyID_t prev_A = prev_idPatchA[prev_idx]; + bodyID_t prev_B = prev_idPatchB[prev_idx]; + bodyID_t prev_L = prev_patchIsland[prev_idx]; + if (prev_A == curr_A && prev_B == curr_B && prev_L == curr_L) { + my_partner = prev_idx; + } + } + + contactMapping[curr_idx] = my_partner; + } +} + +__global__ void buildPatchContactMappingForStableType(bodyID_t* curr_idPatchA, + bodyID_t* curr_idPatchB, + bodyID_t* curr_patchIsland, + bodyID_t* prev_idPatchA, + bodyID_t* prev_idPatchB, + bodyID_t* prev_patchIsland, + contactPairs_t* contactMapping, + contactPairs_t curr_start, + contactPairs_t curr_count, + contactPairs_t prev_start, + contactPairs_t prev_count) { + // Stable-label rewriting preserves the existing (patch A, patch B) ordering but may scramble island labels within + // one patch pair. Binary-search the pair range first, then scan only that usually-small range for the label. + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < curr_count) { + const contactPairs_t curr_idx = curr_start + myID; + const bodyID_t curr_A = curr_idPatchA[curr_idx]; + const bodyID_t curr_B = curr_idPatchB[curr_idx]; + const bodyID_t curr_L = curr_patchIsland[curr_idx]; + contactPairs_t my_partner = NULL_MAPPING_PARTNER; + + contactPairs_t left = 0; + contactPairs_t right = prev_count; + while (left < right) { + const contactPairs_t mid = left + (right - left) / 2; + const contactPairs_t prev_idx = prev_start + mid; + const bodyID_t prev_A = prev_idPatchA[prev_idx]; + const bodyID_t prev_B = prev_idPatchB[prev_idx]; + if (prev_A < curr_A || (prev_A == curr_A && prev_B < curr_B)) { + left = mid + 1; + } else { + right = mid; + } + } + + for (contactPairs_t i = left; i < prev_count; i++) { + const contactPairs_t prev_idx = prev_start + i; + const bodyID_t prev_A = prev_idPatchA[prev_idx]; + const bodyID_t prev_B = prev_idPatchB[prev_idx]; + if (prev_A != curr_A || prev_B != curr_B) { + break; + } + if (prev_patchIsland[prev_idx] == curr_L) { + my_partner = prev_idx; + break; + } + } + contactMapping[curr_idx] = my_partner; + } +} + +// Build patch-based contact mapping between current and previous patch contact arrays. +// Both arrays are sorted by contact type, then by combined patch ID pair within each type. +// For each current contact, we use binary search to find the matching contact in the previous array. +__global__ void buildPatchContactMapping(bodyID_t* curr_idPatchA, + bodyID_t* curr_idPatchB, + contact_t* curr_contactTypePatch, + bodyID_t* prev_idPatchA, + bodyID_t* prev_idPatchB, + contact_t* previous_contactTypePatch, + contactPairs_t* contactMapping, + size_t numCurrContacts, + size_t numPrevContacts) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < numCurrContacts) { + bodyID_t curr_A = curr_idPatchA[myID]; + bodyID_t curr_B = curr_idPatchB[myID]; + contact_t curr_type = curr_contactTypePatch[myID]; + + // Default: no match found + contactPairs_t my_partner = NULL_MAPPING_PARTNER; + + // Find the segment in the previous array with the same contact type + // Since arrays are sorted by type first, we can find the type segment bounds using binary search + + // Find the lower bound (first element with type >= curr_type) + size_t left = 0; + size_t right = numPrevContacts; + while (left < right) { + size_t mid = left + (right - left) / 2; + if (previous_contactTypePatch[mid] < curr_type) { + left = mid + 1; + } else { + right = mid; + } + } + size_t type_start = left; + + // Find the upper bound (first element with type > curr_type) + // Note: we intentionally reuse 'left' from the lower_bound result (= type_start) + // since we know upper_bound >= lower_bound, this is an optimization + right = numPrevContacts; + while (left < right) { + size_t mid = left + (right - left) / 2; + if (previous_contactTypePatch[mid] <= curr_type) { + left = mid + 1; + } else { + right = mid; + } + } + size_t type_end = left; + + // Within this type segment, use binary search to find the matching A/B pair + // The segment is sorted by the combined patch ID pair (A in high bits, B in low bits) + // The encoding ensures that (smaller_A, larger_B) pattern creates a sortable value + left = type_start; + right = type_end; + while (left < right) { + size_t mid = left + (right - left) / 2; + bodyID_t prev_A = prev_idPatchA[mid]; + bodyID_t prev_B = prev_idPatchB[mid]; + + // Compare (A, B) pairs lexicographically + // Since they're sorted by patch ID pair where smaller ID is in high bits + if (prev_A < curr_A || (prev_A == curr_A && prev_B < curr_B)) { + left = mid + 1; + } else { + right = mid; + } + } + + // Check if we found a match at position left + if (left < type_end) { + bodyID_t prev_A = prev_idPatchA[left]; + bodyID_t prev_B = prev_idPatchB[left]; + if (prev_A == curr_A && prev_B == curr_B) { + my_partner = left; + } + } + + contactMapping[myID] = my_partner; + } +} + +// Build a sortable 64-bit key from (idB, contactType, persistency_preference). +// If prefer_persistent is true, persistent contacts sort before non-persistent ones. +__global__ void buildKeyBTypePersist(const bodyID_t* idB, + const contact_t* contactType, + const notStupidBool_t* persistency, + unsigned long long* keys, + size_t n, + bool prefer_persistent) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + const unsigned long long high = (static_cast(idB[myID]) << 32); + const unsigned long long type_part = (static_cast(contactType[myID]) << 1); + const unsigned long long persist_part = + prefer_persistent ? static_cast(persistency[myID] == CONTACT_NOT_PERSISTENT) : 0ull; + keys[myID] = high | type_part | persist_part; + } +} + +// Mark (idA, idB, contactType) duplicates in a lexicographically sorted contact list. +__global__ void markUniqueTriples(const bodyID_t* idA, + const bodyID_t* idB, + const contact_t* contactType, + notStupidBool_t* retain_flags, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + if (myID == 0) { + retain_flags[myID] = (notStupidBool_t)1; + return; + } + const bool is_dup = (idA[myID] == idA[myID - 1]) && (idB[myID] == idB[myID - 1]) && + (contactType[myID] == contactType[myID - 1]); + retain_flags[myID] = is_dup ? (notStupidBool_t)0 : (notStupidBool_t)1; + } +} + +__global__ void fillPrimitivePatchIslandLabels(const contactPairs_t* geomToPatchMap, + const bodyID_t* contactPatchIsland, + const contact_t* primitiveContactTypes, + bodyID_t* primitivePatchIsland, + size_t nPrimitive) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nPrimitive) { + primitivePatchIsland[myID] = usesStablePatchIslandHistory(primitiveContactTypes[myID]) + ? contactPatchIsland[geomToPatchMap[myID]] + : NULL_BODYID; + } +} + +// The rest are old kernels only used in DEME2.x for primitive-primitive contact mapping +/* +__global__ void fillRunLengthArray(primitivesPrimTouches_t* runlength_full, + bodyID_t* unique_ids, + primitivesPrimTouches_t* runlength, + size_t numUnique) { + bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < numUnique) { + bodyID_t i = unique_ids[myID]; + runlength_full[i] = runlength[myID]; + } +} + +__global__ void buildPersistentMap(primitivesPrimTouches_t* new_idA_runlength_full, + primitivesPrimTouches_t* old_idA_runlength_full, + contactPairs_t* new_idA_scanned_runlength, + contactPairs_t* old_idA_scanned_runlength, + contactPairs_t* mapping, + DEMDataKT* granData, + size_t nGeoSafe) { + bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nGeoSafe) { + primitivesPrimTouches_t new_cnt_count = new_idA_runlength_full[myID]; + primitivesPrimTouches_t old_cnt_count = old_idA_runlength_full[myID]; + // If this idA has non-zero runlength in new: a potential enduring sphere + if (new_cnt_count > 0) { + // Where should I start looking? Grab the offset. + contactPairs_t new_cnt_offset = new_idA_scanned_runlength[myID]; + contactPairs_t old_cnt_offset = old_idA_scanned_runlength[myID]; + for (primitivesPrimTouches_t i = 0; i < new_cnt_count; i++) { + // Current contact number we are inspecting + contactPairs_t this_contact = new_cnt_offset + i; + bodyID_t new_idB = granData->idPrimitiveB[this_contact]; + contact_t new_cntType = granData->contactTypePrimitive[this_contact]; + // Mark it as no matching pair found, being a new contact; modify it later + contactPairs_t my_partner = NULL_MAPPING_PARTNER; + // If this is a fake contact, we can move on + if (new_cntType == NOT_A_CONTACT) { + mapping[this_contact] = my_partner; + continue; + } + // Loop through the old idB to see if there is a match + for (primitivesPrimTouches_t j = 0; j < old_cnt_count; j++) { + bodyID_t old_idB = granData->previous_idPrimitiveB[old_cnt_offset + j]; + contact_t old_cntType = granData->previous_contactTypePrimitive[old_cnt_offset + j]; + // If both idB and contact type match, then it is an enduring contact, write it to the mapping + // array + if (new_idB == old_idB && new_cntType == old_cntType) { + my_partner = old_cnt_offset + j; + break; + } + } + // If old_cnt_count == 0, it is automatically NULL_MAPPING_PARTNER + mapping[this_contact] = my_partner; + } + } + } +} + +__global__ void lineNumbers(contactPairs_t* arr, size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + arr[myID] = myID; + } +} + +__global__ void convertToAndFrom(contactPairs_t* old_arr_unsort_to_sort_map, + contactPairs_t* converted_map, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + contactPairs_t map_from = old_arr_unsort_to_sort_map[myID]; + converted_map[map_from] = myID; + } +} + +__global__ void rearrangeMapping(contactPairs_t* map_sorted, + contactPairs_t* old_arr_unsort_to_sort_map, + size_t n) { + contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < n) { + contactPairs_t map_to = map_sorted[myID]; + if (map_to != NULL_MAPPING_PARTNER) + map_sorted[myID] = old_arr_unsort_to_sort_map[map_to]; + } +} +*/ + +} // namespace deme + +#endif diff --git a/src/algorithms/DEMCubContactDetection.cu b/src/algorithms/DEMCubContactDetection.cu deleted file mode 100644 index d6d844ba..00000000 --- a/src/algorithms/DEMCubContactDetection.cu +++ /dev/null @@ -1,1199 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// -// SPDX-License-Identifier: BSD-3-Clause - -#include -#if defined(USE_HIP) - #include -#else - #include -#endif -// #include -#include - -#include -#include -#include - -#include - -#include - -namespace deme { - -inline void contactEventArraysResize(size_t nContactPairs, - DualArray& idGeometryA, - DualArray& idGeometryB, - DualArray& contactType, - DualStruct& granData) { - // Note these resizing are automatically on kT's device - DEME_DUAL_ARRAY_RESIZE_NOVAL(idGeometryA, nContactPairs); - DEME_DUAL_ARRAY_RESIZE_NOVAL(idGeometryB, nContactPairs); - DEME_DUAL_ARRAY_RESIZE_NOVAL(contactType, nContactPairs); - - // Re-packing pointers now is automatic - - // It's safe to toDevice even though kT is working now and dT may write to its buffer - // This is because all buffer arrays are not used in kernels so their pointers are only meaningfully stored on host, - // so writing from host to device won't change the destination where dT writes - granData.toDevice(); -} - -void contactDetection(std::shared_ptr& bin_sphere_kernels, - std::shared_ptr& bin_triangle_kernels, - std::shared_ptr& sphere_contact_kernels, - std::shared_ptr& sphTri_contact_kernels, - std::shared_ptr& history_kernels, - DualStruct& granData, - DualStruct& simParams, - SolverFlags& solverFlags, - VERBOSITY& verbosity, - DualArray& idGeometryA, - DualArray& idGeometryB, - DualArray& contactType, - DualArray& previous_idGeometryA, - DualArray& previous_idGeometryB, - DualArray& previous_contactType, - DualArray& contactPersistency, - DualArray& contactMapping, - cudaStream_t& this_stream, - DEMSolverScratchData& scratchPad, - SolverTimers& timers, - kTStateParams& stateParams) { - // A dumb check - if (simParams->nSpheresGM == 0) { - *(scratchPad.numContacts) = 0; - *scratchPad.numPrevContacts = 0; - *scratchPad.numPrevSpheres = 0; - - scratchPad.numContacts.toDevice(); - scratchPad.numPrevContacts.toDevice(); - scratchPad.numPrevSpheres.toDevice(); - return; - } - // These are needed for the solver to keep tab... But you know, we may have no triangles or no contacts, so - // initializing them is needed. - stateParams.maxSphFoundInBin = 0; - stateParams.maxTriFoundInBin = 0; - stateParams.avgCntsPerSphere = 0; - - // total bytes needed for temp arrays in contact detection - size_t CD_temp_arr_bytes = 0; - - { - timers.GetTimer("Discretize domain").start(); - //////////////////////////////////////////////////////////////////////////////// - // Sphere-related discretization & sphere--analytical contact detection - //////////////////////////////////////////////////////////////////////////////// - - // 1st step: register the number of sphere--bin touching pairs for each sphere for further processing - CD_temp_arr_bytes = simParams->nSpheresGM * sizeof(binsSphereTouches_t); - binsSphereTouches_t* numBinsSphereTouches = - (binsSphereTouches_t*)scratchPad.allocateTempVector("numBinsSphereTouches", CD_temp_arr_bytes); - // This kernel is also tasked to find how many analytical objects each sphere touches - // We'll use a new vector 2 to store this - CD_temp_arr_bytes = simParams->nSpheresGM * sizeof(objID_t); - objID_t* numAnalGeoSphereTouches = - (objID_t*)scratchPad.allocateTempVector("numAnalGeoSphereTouches", CD_temp_arr_bytes); - size_t blocks_needed_for_bodies = - (simParams->nSpheresGM + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - - bin_sphere_kernels->kernel("getNumberOfBinsEachSphereTouches") - .instantiate() - .configure(dim3(blocks_needed_for_bodies), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, numBinsSphereTouches, numAnalGeoSphereTouches); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - // 2nd step: prefix scan sphere--bin touching pairs - // The last element of this scanned array is useful: it can be used to check if the 2 sweeps reach the same - // conclusion on bin--sph touch pairs - CD_temp_arr_bytes = (simParams->nSpheresGM + 1) * sizeof(binSphereTouchPairs_t); - binSphereTouchPairs_t* numBinsSphereTouchesScan = - (binSphereTouchPairs_t*)scratchPad.allocateTempVector("numBinsSphereTouchesScan", CD_temp_arr_bytes); - cubDEMPrefixScan(numBinsSphereTouches, numBinsSphereTouchesScan, - simParams->nSpheresGM, this_stream, scratchPad); - // If there are temp variables that need both device and host copies, we just create DualStruct on-spot - scratchPad.allocateDualStruct("numBinSphereTouchPairs"); - size_t* pNumBinSphereTouchPairs = scratchPad.getDualStructDevice("numBinSphereTouchPairs"); - deviceAdd( - pNumBinSphereTouchPairs, &(numBinsSphereTouchesScan[simParams->nSpheresGM - 1]), - &(numBinsSphereTouches[simParams->nSpheresGM - 1]), this_stream); - deviceAssign(&(numBinsSphereTouchesScan[simParams->nSpheresGM]), - pNumBinSphereTouchPairs, this_stream); - // numBinSphereTouchPairs stores data on device, but now we need it on host, so a migrantion is needed. This - // cannot be forgotten. - scratchPad.syncDualStructDeviceToHost("numBinSphereTouchPairs"); - // Now pNumBinSphereTouchPairs is host pointer and exclusively used on host - pNumBinSphereTouchPairs = scratchPad.getDualStructHost("numBinSphereTouchPairs"); - // The same process is done for sphere--analytical geometry pairs as well. - // One extra elem is used for storing the final elem in scan result. - CD_temp_arr_bytes = (simParams->nSpheresGM + 1) * sizeof(binSphereTouchPairs_t); - binSphereTouchPairs_t* numAnalGeoSphereTouchesScan = - (binSphereTouchPairs_t*)scratchPad.allocateTempVector("numAnalGeoSphereTouchesScan", CD_temp_arr_bytes); - cubDEMPrefixScan(numAnalGeoSphereTouches, numAnalGeoSphereTouchesScan, - simParams->nSpheresGM, this_stream, scratchPad); - deviceAdd( - &(scratchPad.numContacts), &(numAnalGeoSphereTouches[simParams->nSpheresGM - 1]), - &(numAnalGeoSphereTouchesScan[simParams->nSpheresGM - 1]), this_stream); - deviceAssign(&(numAnalGeoSphereTouchesScan[simParams->nSpheresGM]), - &(scratchPad.numContacts), this_stream); - // numContact is updated (with geo--sphere pair number), get it to host - scratchPad.numContacts.toHost(); - if (*(scratchPad.numContacts) > idGeometryA.size()) { - contactEventArraysResize(*(scratchPad.numContacts), idGeometryA, idGeometryB, contactType, granData); - } - // std::cout << *pNumBinSphereTouchPairs << std::endl; - // displayDeviceArray(numBinsSphereTouches, simParams->nSpheresGM); - // displayDeviceArray(numBinsSphereTouchesScan, simParams->nSpheresGM); - - // 3rd step: use a custom kernel to figure out all sphere--bin touching pairs. Note numBinsSphereTouches can - // retire now. - scratchPad.finishUsingTempVector("numBinsSphereTouches"); - scratchPad.finishUsingTempVector("numAnalGeoSphereTouches"); - - CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(binID_t); - binID_t* binIDsEachSphereTouches = - (binID_t*)scratchPad.allocateTempVector("binIDsEachSphereTouches", CD_temp_arr_bytes); - CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(bodyID_t); - bodyID_t* sphereIDsEachBinTouches = - (bodyID_t*)scratchPad.allocateTempVector("sphereIDsEachBinTouches", CD_temp_arr_bytes); - // This kernel is also responsible of figuring out sphere--analytical geometry pairs - bin_sphere_kernels->kernel("populateBinSphereTouchingPairs") - .instantiate() - .configure(dim3(blocks_needed_for_bodies), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, numBinsSphereTouchesScan, numAnalGeoSphereTouchesScan, - binIDsEachSphereTouches, sphereIDsEachBinTouches, granData->idGeometryA, granData->idGeometryB, - granData->contactType); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // std::cout << "Unsorted bin IDs: "; - // displayDeviceArray(binIDsEachSphereTouches, *pNumBinSphereTouchPairs); - // std::cout << "Corresponding sphere IDs: "; - // displayDeviceArray(sphereIDsEachBinTouches, *pNumBinSphereTouchPairs); - - // 4th step: allocate and populate SORTED binIDsEachSphereTouches and sphereIDsEachBinTouches. Note - // numBinsSphereTouchesScan can retire now (analytical contacts have been processed). - scratchPad.finishUsingTempVector("numBinsSphereTouchesScan"); - scratchPad.finishUsingTempVector("numAnalGeoSphereTouchesScan"); - CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(bodyID_t); - bodyID_t* sphereIDsEachBinTouches_sorted = - (bodyID_t*)scratchPad.allocateTempVector("sphereIDsEachBinTouches_sorted", CD_temp_arr_bytes); - CD_temp_arr_bytes = (*pNumBinSphereTouchPairs) * sizeof(binID_t); - binID_t* binIDsEachSphereTouches_sorted = - (binID_t*)scratchPad.allocateTempVector("binIDsEachSphereTouches_sorted", CD_temp_arr_bytes); - // hostSortByKey(granData->binIDsEachSphereTouches, granData->sphereIDsEachBinTouches, - // *pNumBinSphereTouchPairs); - cubDEMSortByKeys(binIDsEachSphereTouches, binIDsEachSphereTouches_sorted, - sphereIDsEachBinTouches, sphereIDsEachBinTouches_sorted, - *pNumBinSphereTouchPairs, this_stream, scratchPad); - // std::cout << "Sorted bin IDs: "; - // displayDeviceArray(binIDsEachSphereTouches_sorted, *pNumBinSphereTouchPairs); - // std::cout << "Corresponding sphere IDs: "; - // displayDeviceArray(sphereIDsEachBinTouches_sorted, *pNumBinSphereTouchPairs); - - // 5th step: use DeviceRunLengthEncode to identify those active (that have bodies in them) bins. - // Also, binIDsEachSphereTouches is large enough for a unique scan because total sphere--bin pairs are more than - // active bins. - binID_t* binIDsUnique = (binID_t*)binIDsEachSphereTouches; - scratchPad.allocateDualStruct("numActiveBins"); - size_t* pNumActiveBins = scratchPad.getDualStructDevice("numActiveBins"); - cubDEMUnique(binIDsEachSphereTouches_sorted, binIDsUnique, pNumActiveBins, *pNumBinSphereTouchPairs, - this_stream, scratchPad); - // Allocate space for encoding output, and run it. Note the (unsorted) binIDsEachSphereTouches and - // sphereIDsEachBinTouches can retire now. - scratchPad.finishUsingTempVector("binIDsEachSphereTouches"); - scratchPad.finishUsingTempVector("sphereIDsEachBinTouches"); - // Get the unique check result to host - scratchPad.syncDualStructDeviceToHost("numActiveBins"); - pNumActiveBins = scratchPad.getDualStructHost("numActiveBins"); - CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binID_t); - // This activeBinIDs will need some host treatment later on... - scratchPad.allocateDualArray("activeBinIDs", CD_temp_arr_bytes); - binID_t* activeBinIDs = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDs"); - CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(spheresBinTouches_t); - spheresBinTouches_t* numSpheresBinTouches = - (spheresBinTouches_t*)scratchPad.allocateTempVector("numSpheresBinTouches", CD_temp_arr_bytes); - // Here you don't have to toHost() again as the runlength should give the same numActiveBins as before - pNumActiveBins = scratchPad.getDualStructDevice("numActiveBins"); - cubDEMRunLengthEncode(binIDsEachSphereTouches_sorted, activeBinIDs, - numSpheresBinTouches, pNumActiveBins, - *pNumBinSphereTouchPairs, this_stream, scratchPad); - pNumActiveBins = scratchPad.getDualStructHost("numActiveBins"); - // std::cout << "numActiveBins: " << *pNumActiveBins << std::endl; - // std::cout << "activeBinIDs: "; - // displayDeviceArray(activeBinIDs, *pNumActiveBins); - // std::cout << "numSpheresBinTouches: "; - // displayDeviceArray(numSpheresBinTouches, *pNumActiveBins); - // std::cout << "binIDsEachSphereTouches_sorted: "; - // displayDeviceArray(binIDsEachSphereTouches_sorted, *pNumBinSphereTouchPairs); - scratchPad.finishUsingDualStruct("numBinSphereTouchPairs"); - - // We find the max geo num in a bin for the purpose of adjusting bin size. - scratchPad.allocateDualStruct("maxGeoInBin"); - spheresBinTouches_t* pMaxGeoInBin = (spheresBinTouches_t*)scratchPad.getDualStructDevice("maxGeoInBin"); - cubDEMMax(numSpheresBinTouches, pMaxGeoInBin, *pNumActiveBins, this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("maxGeoInBin"); - // Hmm... this only works in little-endian systems... I don't use undefined behavior that often but this one... - stateParams.maxSphFoundInBin = *((spheresBinTouches_t*)scratchPad.getDualStructHost("maxGeoInBin")); - scratchPad.finishUsingDualStruct("maxGeoInBin"); - - // Then, scan to find the offsets that are used to index into sphereIDsEachBinTouches_sorted to obtain bin-wise - // spheres. Note binIDsEachSphereTouches_sorted can retire. - scratchPad.finishUsingTempVector("binIDsEachSphereTouches_sorted"); - CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binSphereTouchPairs_t); - binSphereTouchPairs_t* sphereIDsLookUpTable = - (binSphereTouchPairs_t*)scratchPad.allocateTempVector("sphereIDsLookUpTable", CD_temp_arr_bytes); - cubDEMPrefixScan(numSpheresBinTouches, sphereIDsLookUpTable, - *pNumActiveBins, this_stream, scratchPad); - // std::cout << "sphereIDsLookUpTable: "; - // displayDeviceArray(sphereIDsLookUpTable, *pNumActiveBins); - - //////////////////////////////////////////////////////////////////////////////// - // Triangle-related discretization - //////////////////////////////////////////////////////////////////////////////// - - // If there are meshes, they need to be processed too - scratchPad.allocateDualStruct("numActiveBinsForTri"); - size_t* pNumActiveBinsForTri = scratchPad.getDualStructDevice("numActiveBinsForTri"); - bodyID_t* triIDsEachBinTouches_sorted; - trianglesBinTouches_t* numTrianglesBinTouches; - binsTriangleTouchPairs_t* triIDsLookUpTable; - float3 *sandwichANode1, *sandwichANode2, *sandwichANode3, *sandwichBNode1, *sandwichBNode2, *sandwichBNode3; - // The following two pointers are device pointers, as outside this (simParams->nTriGM > 0) block, they are only - // used as read-only ingredients in kernels. But their data is stored using DualArray, as the data need at some - // point be processed on host. - binID_t *mapTriActBinToSphActBin, *activeBinIDsForTri; - if (simParams->nTriGM > 0) { - // 0-th step: Make `sandwich' for each triangle (or say, create a prism out of each triangle). This is - // obviously for our delayed contact detection safety. And finally, if a sphere's distance away from one of - // the 2 prism surfaces is smaller than its radius, it has contact with this prism, hence potentially with - // this triangle. - CD_temp_arr_bytes = simParams->nTriGM * sizeof(float3) * 3; - sandwichANode1 = (float3*)scratchPad.allocateTempVector("sandwichANode1", CD_temp_arr_bytes); - sandwichANode2 = sandwichANode1 + simParams->nTriGM; - sandwichANode3 = sandwichANode2 + simParams->nTriGM; - sandwichBNode1 = (float3*)scratchPad.allocateTempVector("sandwichBNode1", CD_temp_arr_bytes); - sandwichBNode2 = sandwichBNode1 + simParams->nTriGM; - sandwichBNode3 = sandwichBNode2 + simParams->nTriGM; - size_t blocks_needed_for_tri = - (simParams->nTriGM + DEME_NUM_TRIANGLE_PER_BLOCK - 1) / DEME_NUM_TRIANGLE_PER_BLOCK; - bin_triangle_kernels->kernel("makeTriangleSandwich") - .instantiate() - .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, - sandwichBNode2, sandwichBNode3); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - // 1st step: register the number of triangle--bin touching pairs for each triangle for further processing. - // Because we do a `sandwich' contact detection, we are - CD_temp_arr_bytes = simParams->nTriGM * sizeof(binsTriangleTouches_t); - binsTriangleTouches_t* numBinsTriTouches = - (binsTriangleTouches_t*)scratchPad.allocateTempVector("numBinsTriTouches", CD_temp_arr_bytes); - { - bin_triangle_kernels->kernel("getNumberOfBinsEachTriangleTouches") - .instantiate() - .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, numBinsTriTouches, sandwichANode1, sandwichANode2, sandwichANode3, - sandwichBNode1, sandwichBNode2, sandwichBNode3); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // std::cout << "numBinsTriTouches: " << std::endl; - // displayDeviceArray(numBinsTriTouches, simParams->nTriGM); - // displayDeviceArray(numBinsTriTouches + simParams->nTriGM, simParams->nTriGM); - - // 2nd step: prefix scan sphere--bin touching pairs - // The last element of this scanned array is useful: it can be used to check if the 2 sweeps reach the same - // conclusion on bin--tri touch pairs - CD_temp_arr_bytes = (simParams->nTriGM + 1) * sizeof(binsTriangleTouchPairs_t); - binsTriangleTouchPairs_t* numBinsTriTouchesScan = - (binsTriangleTouchPairs_t*)scratchPad.allocateTempVector("numBinsTriTouchesScan", CD_temp_arr_bytes); - cubDEMPrefixScan( - numBinsTriTouches, numBinsTriTouchesScan, simParams->nTriGM, this_stream, scratchPad); - scratchPad.allocateDualStruct("numBinTriTouchPairs"); - size_t* pNumBinTriTouchPairs = scratchPad.getDualStructDevice("numBinTriTouchPairs"); - deviceAdd( - pNumBinTriTouchPairs, &(numBinsTriTouchesScan[simParams->nTriGM - 1]), - &(numBinsTriTouches[simParams->nTriGM - 1]), this_stream); - deviceAssign(&(numBinsTriTouchesScan[simParams->nTriGM]), - pNumBinTriTouchPairs, this_stream); - scratchPad.syncDualStructDeviceToHost("numBinTriTouchPairs"); - pNumBinTriTouchPairs = scratchPad.getDualStructHost("numBinTriTouchPairs"); - // Again, numBinsTriTouchesScan is used in populateBinTriangleTouchingPairs - - // 3rd step: use a custom kernel to figure out all sphere--bin touching pairs. Note numBinsTriTouches can - // retire now. - scratchPad.finishUsingTempVector("numBinsTriTouches"); - CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(binID_t); - binID_t* binIDsEachTriTouches = - (binID_t*)scratchPad.allocateTempVector("binIDsEachTriTouches", CD_temp_arr_bytes); - CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(bodyID_t); - bodyID_t* triIDsEachBinTouches = - (bodyID_t*)scratchPad.allocateTempVector("triIDsEachBinTouches", CD_temp_arr_bytes); - { - bin_triangle_kernels->kernel("populateBinTriangleTouchingPairs") - .instantiate() - .configure(dim3(blocks_needed_for_tri), dim3(DEME_NUM_TRIANGLE_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, numBinsTriTouchesScan, binIDsEachTriTouches, triIDsEachBinTouches, - sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, sandwichBNode2, - sandwichBNode3); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // std::cout << "binIDsEachTriTouches: " << std::endl; - // displayDeviceArray(binIDsEachTriTouches, *pNumBinTriTouchPairs); - - // 4th step: allocate and populate SORTED binIDsEachTriTouches and triIDsEachBinTouches. Note - // numBinsTriTouchesScan can retire now. - scratchPad.finishUsingTempVector("numBinsTriTouchesScan"); - CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(bodyID_t); - triIDsEachBinTouches_sorted = - (bodyID_t*)scratchPad.allocateTempVector("triIDsEachBinTouches_sorted", CD_temp_arr_bytes); - CD_temp_arr_bytes = *pNumBinTriTouchPairs * sizeof(binID_t); - binID_t* binIDsEachTriTouches_sorted = - (binID_t*)scratchPad.allocateTempVector("binIDsEachTriTouches_sorted", CD_temp_arr_bytes); - cubDEMSortByKeys(binIDsEachTriTouches, binIDsEachTriTouches_sorted, triIDsEachBinTouches, - triIDsEachBinTouches_sorted, *pNumBinTriTouchPairs, this_stream, - scratchPad); - - // 5th step: use DeviceRunLengthEncode to identify those active (that have tris in them) bins. - // Also, binIDsEachTriTouches is large enough for a unique scan because total sphere--bin pairs are more - // than active bins. - binID_t* binIDsUnique = (binID_t*)binIDsEachTriTouches; - cubDEMUnique(binIDsEachTriTouches_sorted, binIDsUnique, pNumActiveBinsForTri, - *pNumBinTriTouchPairs, this_stream, scratchPad); - // Allocate space for encoding output, and run it. Note the (unsorted) binIDsEachTriTouches and - // triIDsEachBinTouches can retire now. - scratchPad.finishUsingTempVector("binIDsEachTriTouches"); - scratchPad.finishUsingTempVector("triIDsEachBinTouches"); - // Bring value to host - scratchPad.syncDualStructDeviceToHost("numActiveBinsForTri"); - pNumActiveBinsForTri = scratchPad.getDualStructHost("numActiveBinsForTri"); - CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binID_t); - // Again, activeBinIDsForTri has some data processing needed on host, so allocated as DualArray - scratchPad.allocateDualArray("activeBinIDsForTri", CD_temp_arr_bytes); - activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDsForTri"); - CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(trianglesBinTouches_t); - numTrianglesBinTouches = - (trianglesBinTouches_t*)scratchPad.allocateTempVector("numTrianglesBinTouches", CD_temp_arr_bytes); - // Again, no need to bring numActiveBinsForTri to host again, as values not changed - pNumActiveBinsForTri = scratchPad.getDualStructDevice("numActiveBinsForTri"); - cubDEMRunLengthEncode(binIDsEachTriTouches_sorted, activeBinIDsForTri, - numTrianglesBinTouches, pNumActiveBinsForTri, - *pNumBinTriTouchPairs, this_stream, scratchPad); - pNumActiveBinsForTri = scratchPad.getDualStructHost("numActiveBinsForTri"); - // std::cout << "activeBinIDsForTri: " << std::endl; - // displayDeviceArray(activeBinIDsForTri, *pNumActiveBinsForTri); - // std::cout << "NumActiveBinsForTri: " << *pNumActiveBinsForTri << std::endl; - // std::cout << "NumActiveBins: " << *pNumActiveBins << std::endl; - scratchPad.finishUsingDualStruct("numBinTriTouchPairs"); - - // We find the max tri num in a bin for the purpose of adjusting bin size - scratchPad.allocateDualStruct("maxGeoInBin"); - trianglesBinTouches_t* pMaxGeoInBin = (trianglesBinTouches_t*)scratchPad.getDualStructDevice("maxGeoInBin"); - cubDEMMax(numTrianglesBinTouches, pMaxGeoInBin, *pNumActiveBinsForTri, this_stream, - scratchPad); - scratchPad.syncDualStructDeviceToHost("maxGeoInBin"); - // Find the max tri number. Hmm... this only works in little-endian systems... I don't use undefined - // behavior that often but this one... - stateParams.maxTriFoundInBin = *((trianglesBinTouches_t*)scratchPad.getDualStructHost("maxGeoInBin")); - scratchPad.finishUsingDualStruct("maxGeoInBin"); - - // 6th step: map activeBinIDsForTri to activeBinIDs, so that when we are processing the bins in - // activeBinIDsForTri, we know where to find the corresponding bin that resides in activeBinIDs, to bring - // spheres into this bin-wise contact detection sweep. - CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binID_t); - scratchPad.allocateDualArray("mapTriActBinToSphActBin", CD_temp_arr_bytes); - { - // This `merge search' task is very unsuitable for GPU... So we create host version of these work arrays - // temporarily and use them. Don't worry about the temporary part, as it's only in logic, no actual - // memory allocation, as vectorPool handles it. - scratchPad.syncDualArrayDeviceToHost("activeBinIDsForTri"); - scratchPad.syncDualArrayDeviceToHost("activeBinIDs"); - binID_t* activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayHost("activeBinIDsForTri"); - binID_t* activeBinIDs = (binID_t*)scratchPad.getDualArrayHost("activeBinIDs"); - binID_t* mapTriActBinToSphActBin = (binID_t*)scratchPad.getDualArrayHost("mapTriActBinToSphActBin"); - hostMergeSearchMapGen(activeBinIDsForTri, activeBinIDs, mapTriActBinToSphActBin, *pNumActiveBinsForTri, - *pNumActiveBins, deme::NULL_BINID); - scratchPad.syncDualArrayHostToDevice("activeBinIDsForTri"); - scratchPad.syncDualArrayHostToDevice("activeBinIDs"); - scratchPad.syncDualArrayHostToDevice("mapTriActBinToSphActBin"); - } - // Bind back some of the device pointers is not necessary as they are not changed - // But it's needed for mapTriActBinToSphActBin as it's its first assignment... - mapTriActBinToSphActBin = (binID_t*)scratchPad.getDualArrayDevice("mapTriActBinToSphActBin"); - // activeBinIDsForTri = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDsForTri"); - // activeBinIDs = (binID_t*)scratchPad.getDualArrayDevice("activeBinIDs"); - // std::cout << "mapTriActBinToSphActBin: " << std::endl; - // displayDeviceArray(mapTriActBinToSphActBin, *pNumActiveBinsForTri); - - // 7th step: scan to find the offsets that are used to index into triIDsEachBinTouches_sorted to obtain - // bin-wise triangles. Note binIDsEachTriTouches_sorted can retire. - scratchPad.finishUsingTempVector("binIDsEachTriTouches_sorted"); - CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binsTriangleTouchPairs_t); - triIDsLookUpTable = - (binsTriangleTouchPairs_t*)scratchPad.allocateTempVector("triIDsLookUpTable", CD_temp_arr_bytes); - cubDEMPrefixScan( - numTrianglesBinTouches, triIDsLookUpTable, *pNumActiveBinsForTri, this_stream, scratchPad); - } - timers.GetTimer("Discretize domain").stop(); - - //////////////////////////////////////////////////////////////////////////////// - // Populating contact pairs - //////////////////////////////////////////////////////////////////////////////// - - timers.GetTimer("Find contact pairs").start(); - // Final step: find the contact pairs. One-two punch: first find num of contacts in each bin, then prescan, then - // find the actual pair names. A new temp array is needed for this numSphContactsInEachBin. Note we assume the - // number of contact in each bin is the same level as the number of spheres in each bin (capped by the same data - // type). - CD_temp_arr_bytes = (*pNumActiveBins) * sizeof(binContactPairs_t); - binContactPairs_t* numSphContactsInEachBin = - (binContactPairs_t*)scratchPad.allocateTempVector("numSphContactsInEachBin", CD_temp_arr_bytes); - size_t blocks_needed_for_bins_sph = *pNumActiveBins; - // Some quantities and arrays for triangles as well, should we need them - size_t blocks_needed_for_bins_tri = 0; - // binContactPairs_t also doubles as the type for the number of tri--sph contact pairs - binContactPairs_t* numTriSphContactsInEachBin; - if (simParams->nTriGM > 0) { - blocks_needed_for_bins_tri = *pNumActiveBinsForTri; - CD_temp_arr_bytes = (*pNumActiveBinsForTri) * sizeof(binContactPairs_t); - numTriSphContactsInEachBin = - (binContactPairs_t*)scratchPad.allocateTempVector("numTriSphContactsInEachBin", CD_temp_arr_bytes); - } - - if (blocks_needed_for_bins_sph > 0) { - sphere_contact_kernels->kernel("getNumberOfSphereContactsEachBin") - .instantiate() - .configure(dim3(blocks_needed_for_bins_sph), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, - sphereIDsLookUpTable, numSphContactsInEachBin, *pNumActiveBins); - DEME_GPU_CALL_WATCH_BETA(cudaStreamSynchronize(this_stream)); - - if (blocks_needed_for_bins_tri > 0) { - sphTri_contact_kernels->kernel("getNumberOfSphTriContactsEachBin") - .instantiate() - .configure(dim3(blocks_needed_for_bins_tri), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, - sphereIDsLookUpTable, mapTriActBinToSphActBin, triIDsEachBinTouches_sorted, - activeBinIDsForTri, numTrianglesBinTouches, triIDsLookUpTable, numTriSphContactsInEachBin, - sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, sandwichBNode2, - sandwichBNode3, *pNumActiveBinsForTri); - DEME_GPU_CALL_WATCH_BETA(cudaStreamSynchronize(this_stream)); - // std::cout << "numTriSphContactsInEachBin: " << std::endl; - // displayDeviceArray(numTriSphContactsInEachBin, *pNumActiveBinsForTri); - } - - //// TODO: sphere should have jitified and non-jitified part. Use a component ID > max_comp_id to signal - /// bringing data from global memory. / TODO: Add tri--sphere CD kernel (if mesh support is to be added). - /// This kernel integrates tri--boundary CD. Note triangle facets can have jitified (many bodies of the same - /// type) and non-jitified (a big meshed body) part. Use a component ID > max_comp_id to signal bringing - /// data from global memory. / TODO: Add tri--tri CD kernel (in the far future, should mesh-rerpesented - /// geometry to be supported). This kernel integrates tri--boundary CD. / TODO: remember that boundary types - /// are either all jitified or non-jitified. In principal, they should be all jitified. - - // Prescan numSphContactsInEachBin to get the final sphSphContactReportOffsets and - // triSphContactReportOffsets. New vectors are needed. - // The extra entry is maybe superfluous and is for extra safety, in case the 2 sweeps do not agree with each - // other. - CD_temp_arr_bytes = (*pNumActiveBins + 1) * sizeof(contactPairs_t); - contactPairs_t* sphSphContactReportOffsets = - (contactPairs_t*)scratchPad.allocateTempVector("sphSphContactReportOffsets", CD_temp_arr_bytes); - cubDEMPrefixScan(numSphContactsInEachBin, sphSphContactReportOffsets, - *pNumActiveBins, this_stream, scratchPad); - contactPairs_t* triSphContactReportOffsets; - if (simParams->nTriGM > 0) { - CD_temp_arr_bytes = (*pNumActiveBinsForTri + 1) * sizeof(contactPairs_t); - triSphContactReportOffsets = - (contactPairs_t*)scratchPad.allocateTempVector("triSphContactReportOffsets", CD_temp_arr_bytes); - cubDEMPrefixScan(numTriSphContactsInEachBin, - triSphContactReportOffsets, *pNumActiveBinsForTri, - this_stream, scratchPad); - } - // DEME_DEBUG_PRINTF("Num contacts each bin:"); - // DEME_DEBUG_EXEC(displayDeviceArray(numSphContactsInEachBin, *pNumActiveBins)); - // DEME_DEBUG_PRINTF("Tri contact report offsets:"); - // DEME_DEBUG_EXEC(displayDeviceArray(triSphContactReportOffsets, *pNumActiveBinsForTri)); - // DEME_DEBUG_PRINTF("Family number:"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->familyID.device(), simParams->nOwnerBodies)); - - // Add sphere--sphere contacts together with sphere--analytical geometry contacts - size_t nSphereGeoContact = *scratchPad.numContacts; - size_t nSphereSphereContact = 0, nTriSphereContact = 0; - { - scratchPad.allocateDualStruct("numSSContact"); - deviceAdd( - scratchPad.getDualStructDevice("numSSContact"), &(numSphContactsInEachBin[*pNumActiveBins - 1]), - &(sphSphContactReportOffsets[*pNumActiveBins - 1]), this_stream); - deviceAssign(&(sphSphContactReportOffsets[*pNumActiveBins]), - scratchPad.getDualStructDevice("numSSContact"), this_stream); - scratchPad.syncDualStructDeviceToHost("numSSContact"); - nSphereSphereContact = *scratchPad.getDualStructHost("numSSContact"); - scratchPad.finishUsingDualStruct("numSSContact"); - - if (simParams->nTriGM > 0) { - scratchPad.allocateDualStruct("numSMContact"); - deviceAdd( - scratchPad.getDualStructDevice("numSMContact"), - &(numTriSphContactsInEachBin[*pNumActiveBinsForTri - 1]), - &(triSphContactReportOffsets[*pNumActiveBinsForTri - 1]), this_stream); - deviceAssign(&(triSphContactReportOffsets[*pNumActiveBinsForTri]), - scratchPad.getDualStructDevice("numSMContact"), this_stream); - scratchPad.syncDualStructDeviceToHost("numSMContact"); - nTriSphereContact = *scratchPad.getDualStructHost("numSMContact"); - scratchPad.finishUsingDualStruct("numSMContact"); - } - // std::cout << "nSphereGeoContact: " << nSphereGeoContact << std::endl; - // std::cout << "nSphereSphereContact: " << nSphereSphereContact << std::endl; - } - - *scratchPad.numContacts = nSphereSphereContact + nSphereGeoContact + nTriSphereContact; - if (*scratchPad.numContacts > idGeometryA.size()) { - contactEventArraysResize(*scratchPad.numContacts, idGeometryA, idGeometryB, contactType, granData); - } - - // Sphere--sphere contact pairs go after sphere--anal-geo contacts - bodyID_t* idSphA = (granData->idGeometryA + nSphereGeoContact); - bodyID_t* idSphB = (granData->idGeometryB + nSphereGeoContact); - contact_t* dType = (granData->contactType + nSphereGeoContact); - // Then fill in those contacts - sphere_contact_kernels->kernel("populateSphSphContactPairsEachBin") - .instantiate() - .configure(dim3(blocks_needed_for_bins_sph), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, - sphereIDsLookUpTable, sphSphContactReportOffsets, idSphA, idSphB, dType, *pNumActiveBins); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - // Triangle--sphere contact pairs go after sphere--sphere contacts. Remember to mark their type. - if (blocks_needed_for_bins_tri > 0) { - idSphA = (granData->idGeometryA + nSphereGeoContact + nSphereSphereContact); - bodyID_t* idTriB = (granData->idGeometryB + nSphereGeoContact + nSphereSphereContact); - dType = (granData->contactType + nSphereGeoContact + nSphereSphereContact); - sphTri_contact_kernels->kernel("populateTriSphContactsEachBin") - .instantiate() - .configure(dim3(blocks_needed_for_bins_tri), dim3(DEME_KT_CD_NTHREADS_PER_BLOCK), 0, this_stream) - .launch(&simParams, &granData, sphereIDsEachBinTouches_sorted, activeBinIDs, numSpheresBinTouches, - sphereIDsLookUpTable, mapTriActBinToSphActBin, triIDsEachBinTouches_sorted, - activeBinIDsForTri, numTrianglesBinTouches, triIDsLookUpTable, triSphContactReportOffsets, - idSphA, idTriB, dType, sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, - sandwichBNode2, sandwichBNode3, *pNumActiveBinsForTri); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // std::cout << "Contacts: " << std::endl; - // displayDeviceArray(granData->idGeometryA, *scratchPad.numContacts); - // displayDeviceArray(granData->idGeometryB, *scratchPad.numContacts); - // displayDeviceArray(granData->contactType, *scratchPad.numContacts); - } - } // End of bin-wise contact detection subroutine - - // The following vectors are used till the end - scratchPad.finishUsingTempVector("sphereIDsEachBinTouches_sorted"); - scratchPad.finishUsingDualArray("activeBinIDs"); - scratchPad.finishUsingTempVector("numSpheresBinTouches"); - scratchPad.finishUsingTempVector("sphereIDsLookUpTable"); - scratchPad.finishUsingTempVector("sandwichANode1"); - scratchPad.finishUsingTempVector("sandwichBNode1"); - scratchPad.finishUsingTempVector("triIDsEachBinTouches_sorted"); - scratchPad.finishUsingDualArray("activeBinIDsForTri"); - scratchPad.finishUsingTempVector("numTrianglesBinTouches"); - scratchPad.finishUsingDualArray("mapTriActBinToSphActBin"); - scratchPad.finishUsingTempVector("triIDsLookUpTable"); - scratchPad.finishUsingTempVector("numSphContactsInEachBin"); - scratchPad.finishUsingTempVector("numTriSphContactsInEachBin"); - scratchPad.finishUsingTempVector("sphSphContactReportOffsets"); - scratchPad.finishUsingTempVector("triSphContactReportOffsets"); - - scratchPad.finishUsingDualStruct("numActiveBins"); - scratchPad.finishUsingDualStruct("numActiveBinsForTri"); - - // There is in fact one more task: If the user specified persistent contacts, we check the previous contact list - // and see if there are some contacts we need to add to the current list. Even if we detected 0 contacts, we - // might still have persistent contacts to add to the list. - // Also at this point, all temp arrays are freed now. - if (solverFlags.hasPersistentContacts && !solverFlags.isHistoryless) { - // A bool array to help find what persistent contacts from the prev array need to be processed... - size_t flag_arr_bytes = (*scratchPad.numPrevContacts) * sizeof(notStupidBool_t); - notStupidBool_t* grab_flags = (notStupidBool_t*)scratchPad.allocateTempVector("grab_flags", flag_arr_bytes); - size_t blocks_needed_for_flagging = - (*scratchPad.numPrevContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_flagging > 0) { - history_kernels->kernel("markBoolIf") - .instantiate() - .configure(dim3(blocks_needed_for_flagging), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(grab_flags, granData->contactPersistency, CONTACT_IS_PERSISTENT, - *scratchPad.numPrevContacts); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // Store the number of persistent contacts - scratchPad.allocateDualStruct("numPersistCnts"); - - // Then extract the persistent array - // This many elements are sufficient, at very least... - size_t selected_ids_bytes = (*scratchPad.numPrevContacts) * sizeof(bodyID_t); - size_t selected_types_bytes = (*scratchPad.numPrevContacts) * sizeof(contact_t); - bodyID_t* selected_idA = (bodyID_t*)scratchPad.allocateTempVector("selected_idA", selected_ids_bytes); - bodyID_t* selected_idB = (bodyID_t*)scratchPad.allocateTempVector("selected_idB", selected_ids_bytes); - contact_t* selected_types = - (contact_t*)scratchPad.allocateTempVector("selected_types", selected_types_bytes); - - cubDEMSelectFlagged(granData->previous_idGeometryA, selected_idA, grab_flags, - scratchPad.getDualStructDevice("numPersistCnts"), - *scratchPad.numPrevContacts, this_stream, scratchPad); - cubDEMSelectFlagged(granData->previous_idGeometryB, selected_idB, grab_flags, - scratchPad.getDualStructDevice("numPersistCnts"), - *scratchPad.numPrevContacts, this_stream, scratchPad); - cubDEMSelectFlagged(granData->previous_contactType, selected_types, grab_flags, - scratchPad.getDualStructDevice("numPersistCnts"), - *scratchPad.numPrevContacts, this_stream, scratchPad); - // Those flag selections give the same result. Bring it to host. - scratchPad.syncDualStructDeviceToHost("numPersistCnts"); - size_t* pNumPersistCnts = scratchPad.getDualStructHost("numPersistCnts"); - - // Then concatenate the persisten - size_t total_ids_bytes = (*scratchPad.numContacts + *pNumPersistCnts) * sizeof(bodyID_t); - size_t total_types_bytes = (*scratchPad.numContacts + *pNumPersistCnts) * sizeof(contact_t); - size_t total_persistency_bytes = (*scratchPad.numContacts + *pNumPersistCnts) * sizeof(notStupidBool_t); - selected_ids_bytes = (*pNumPersistCnts) * sizeof(bodyID_t); - selected_types_bytes = (*pNumPersistCnts) * sizeof(contact_t); - bodyID_t* total_idA = (bodyID_t*)scratchPad.allocateTempVector("total_idA", total_ids_bytes); - bodyID_t* total_idB = (bodyID_t*)scratchPad.allocateTempVector("total_idB", total_ids_bytes); - contact_t* total_types = (contact_t*)scratchPad.allocateTempVector("total_types", total_types_bytes); - notStupidBool_t* total_persistency = - (notStupidBool_t*)scratchPad.allocateTempVector("total_persistency", total_persistency_bytes); - DEME_GPU_CALL(cudaMemcpy(total_idA, selected_idA, selected_ids_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(total_idA + *pNumPersistCnts, granData->idGeometryA, - total_ids_bytes - selected_ids_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(total_idB, selected_idB, selected_ids_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(total_idB + *pNumPersistCnts, granData->idGeometryB, - total_ids_bytes - selected_ids_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(total_types, selected_types, selected_types_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(total_types + *pNumPersistCnts, granData->contactType, - total_types_bytes - selected_types_bytes, cudaMemcpyDeviceToDevice)); - // For the selected portion, persistency is all 1 - DEME_GPU_CALL(cudaMemset(total_persistency, CONTACT_NOT_PERSISTENT, total_persistency_bytes)); - size_t blocks_needed_for_setting_1 = - (*pNumPersistCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_setting_1 > 0) { - history_kernels->kernel("setArr") - .instantiate() - .configure(dim3(blocks_needed_for_setting_1), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(total_persistency, *pNumPersistCnts, CONTACT_IS_PERSISTENT); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - scratchPad.finishUsingTempVector("grab_flags"); - scratchPad.finishUsingTempVector("selected_idA"); - scratchPad.finishUsingTempVector("selected_idB"); - scratchPad.finishUsingTempVector("selected_types"); - - // Then remove potential redundency in the current contact array. - // To do that, we sort by idA... - size_t numTotalCnts = *scratchPad.numContacts + *pNumPersistCnts; - contact_t* contactType_sorted = - (contact_t*)scratchPad.allocateTempVector("contactType_sorted", total_types_bytes); - bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", total_ids_bytes); - bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", total_ids_bytes); - notStupidBool_t* persistency_sorted = - (notStupidBool_t*)scratchPad.allocateTempVector("persistency_sorted", total_persistency_bytes); - //// TODO: But do I have to SortByKey three times?? Can I zip these value arrays together?? - // Although it is stupid, do pay attention to that it does leverage the fact that RadixSort is stable. - cubDEMSortByKeys(total_idA, idA_sorted, total_idB, idB_sorted, numTotalCnts, - this_stream, scratchPad); - cubDEMSortByKeys(total_idA, idA_sorted, total_types, contactType_sorted, numTotalCnts, - this_stream, scratchPad); - cubDEMSortByKeys(total_idA, idA_sorted, total_persistency, persistency_sorted, - numTotalCnts, this_stream, scratchPad); - // std::cout << "Contacts before duplication check: " << std::endl; - // displayDeviceArray(idA_sorted, numTotalCnts); - // displayDeviceArray(idB_sorted, numTotalCnts); - // displayDeviceArray(contactType_sorted, numTotalCnts); - // displayDeviceArray(persistency_sorted, numTotalCnts); - scratchPad.finishUsingTempVector("total_idA"); - scratchPad.finishUsingTempVector("total_idB"); - scratchPad.finishUsingTempVector("total_types"); - scratchPad.finishUsingTempVector("total_persistency"); - scratchPad.finishUsingDualStruct("numPersistCnts"); - - // Then we run-length it... - size_t run_length_bytes = simParams->nSpheresGM * sizeof(geoSphereTouches_t); - geoSphereTouches_t* idA_runlength = - (geoSphereTouches_t*)scratchPad.allocateTempVector("idA_runlength", run_length_bytes); - size_t unique_id_bytes = simParams->nSpheresGM * sizeof(bodyID_t); - bodyID_t* unique_idA = (bodyID_t*)scratchPad.allocateTempVector("unique_idA", unique_id_bytes); - scratchPad.allocateDualStruct("numUniqueA"); - cubDEMRunLengthEncode(idA_sorted, unique_idA, idA_runlength, - scratchPad.getDualStructDevice("numUniqueA"), - numTotalCnts, this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("numUniqueA"); - size_t* pNumUniqueA = scratchPad.getDualStructHost("numUniqueA"); - size_t scanned_runlength_bytes = (*pNumUniqueA) * sizeof(contactPairs_t); - contactPairs_t* idA_scanned_runlength = - (contactPairs_t*)scratchPad.allocateTempVector("idA_scanned_runlength", scanned_runlength_bytes); - cubDEMPrefixScan(idA_runlength, idA_scanned_runlength, *pNumUniqueA, - this_stream, scratchPad); - - // Then each thread will take care of an id in A to mark redundency... - size_t retain_flags_size = (numTotalCnts) * sizeof(notStupidBool_t); - notStupidBool_t* retain_flags = - (notStupidBool_t*)scratchPad.allocateTempVector("retain_flags", retain_flags_size); - blocks_needed_for_setting_1 = (numTotalCnts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_setting_1 > 0) { - history_kernels->kernel("setArr") - .instantiate() - .configure(dim3(blocks_needed_for_setting_1), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(retain_flags, numTotalCnts, (notStupidBool_t)1); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - blocks_needed_for_flagging = (*pNumUniqueA + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - if (blocks_needed_for_flagging > 0) { - history_kernels->kernel("markDuplicateContacts") - .instantiate() - .configure(dim3(blocks_needed_for_flagging), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(idA_runlength, idA_scanned_runlength, idB_sorted, contactType_sorted, persistency_sorted, - retain_flags, *pNumUniqueA); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // std::cout << "Marked retainers: " << std::endl; - // displayDeviceArray(retain_flags, numTotalCnts); - scratchPad.finishUsingDualStruct("numUniqueA"); - - // Then remove redundency based on the flag array... - // Note the contactPersistency array is managed by the current contact arr. It will also be copied over. - scratchPad.allocateDualStruct("numRetainedCnts"); - cubDEMSum(retain_flags, scratchPad.getDualStructDevice("numRetainedCnts"), - numTotalCnts, this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("numRetainedCnts"); - size_t* pNumRetainedCnts = scratchPad.getDualStructHost("numRetainedCnts"); - // DEME_STEP_DEBUG_PRINTF("Found %zu contacts, including user-specified persistent contacts.", - // *pNumRetainedCnts); - if (*pNumRetainedCnts > idGeometryA.size()) { - contactEventArraysResize(*pNumRetainedCnts, idGeometryA, idGeometryB, contactType, granData); - } - if (*pNumRetainedCnts > contactPersistency.size()) { - DEME_DUAL_ARRAY_RESIZE_NOVAL(contactPersistency, *pNumRetainedCnts); - granData.toDevice(); - } - cubDEMSelectFlagged(idA_sorted, granData->idGeometryA, retain_flags, - scratchPad.getDualStructDevice("numRetainedCnts"), - numTotalCnts, this_stream, scratchPad); - cubDEMSelectFlagged(idB_sorted, granData->idGeometryB, retain_flags, - scratchPad.getDualStructDevice("numRetainedCnts"), - numTotalCnts, this_stream, scratchPad); - cubDEMSelectFlagged(contactType_sorted, granData->contactType, retain_flags, - scratchPad.getDualStructDevice("numRetainedCnts"), - numTotalCnts, this_stream, scratchPad); - cubDEMSelectFlagged( - persistency_sorted, granData->contactPersistency, retain_flags, - scratchPad.getDualStructDevice("numRetainedCnts"), numTotalCnts, this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost( - "numRetainedCnts"); // In theory no need, but when CONTACT_IS_PERSISTENT is not 1... - // DEME_STEP_DEBUG_PRINTF("CUB confirms there are %zu contacts, including user-specified persistent - // contacts.", *pNumRetainedCnts); - // std::cout << "Contacts after duplication check: " << std::endl; - // displayDeviceArray(granData->idGeometryA, *pNumRetainedCnts); - // displayDeviceArray(granData->idGeometryB, *pNumRetainedCnts); - // displayDeviceArray(granData->contactType, *pNumRetainedCnts); - // displayDeviceArray(granData->contactPersistency, *pNumRetainedCnts); - - // And update the number of contacts. - *scratchPad.numContacts = *pNumRetainedCnts; - scratchPad.finishUsingDualStruct("numRetainedCnts"); - - // Unclaim all temp vectors - scratchPad.finishUsingTempVector("idA_runlength"); - scratchPad.finishUsingTempVector("unique_idA"); - scratchPad.finishUsingTempVector("idA_scanned_runlength"); - scratchPad.finishUsingTempVector("contactType_sorted"); - scratchPad.finishUsingTempVector("idA_sorted"); - scratchPad.finishUsingTempVector("idB_sorted"); - scratchPad.finishUsingTempVector("persistency_sorted"); - scratchPad.finishUsingTempVector("retain_flags"); - } - - timers.GetTimer("Find contact pairs").stop(); - } - - //////////////////////////////////////////////////////////////////////////////// - // Constructing contact history - //////////////////////////////////////////////////////////////////////////////// - - timers.GetTimer("Build history map").start(); - // Now, sort idGeometryAB by their owners. Needed for identifying enduring contacts in history-based models. - if (*scratchPad.numContacts > 0) { - // All temp vectors are free now... - // Note that if it hasPersistentContacts, idAB and types are already sorted based on idA, so there is no need to - // do that again. - size_t type_arr_bytes = (*scratchPad.numContacts) * sizeof(contact_t); - - size_t id_arr_bytes = (*scratchPad.numContacts) * sizeof(bodyID_t); - if (!solverFlags.hasPersistentContacts) { - contact_t* contactType_sorted = - (contact_t*)scratchPad.allocateTempVector("contactType_sorted", type_arr_bytes); - bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", id_arr_bytes); - bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", id_arr_bytes); - - //// TODO: But do I have to SortByKey two times?? Can I zip these value arrays together?? - // Although it is stupid, do pay attention to that it does leverage the fact that RadixSort is stable. - cubDEMSortByKeys(granData->idGeometryA, idA_sorted, granData->idGeometryB, idB_sorted, - *scratchPad.numContacts, this_stream, scratchPad); - cubDEMSortByKeys(granData->idGeometryA, idA_sorted, granData->contactType, - contactType_sorted, *scratchPad.numContacts, this_stream, scratchPad); - - // Copy back to idGeometry arrays - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryA, idA_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryB, idB_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL( - cudaMemcpy(granData->contactType, contactType_sorted, type_arr_bytes, cudaMemcpyDeviceToDevice)); - // Short-lifespan vectors can now be freed - scratchPad.finishUsingTempVector("contactType_sorted"); - scratchPad.finishUsingTempVector("idA_sorted"); - scratchPad.finishUsingTempVector("idB_sorted"); - } - // DEME_DEBUG_PRINTF("New contact IDs (A):"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->idGeometryA, *scratchPad.numContacts)); - // DEME_DEBUG_PRINTF("New contact IDs (B):"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->idGeometryB, *scratchPad.numContacts)); - // DEME_DEBUG_PRINTF("New contact types:"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->contactType, *scratchPad.numContacts)); - // DEME_DEBUG_PRINTF("Old contact IDs (A):"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->previous_idGeometryA, *scratchPad.numPrevContacts)); - // DEME_DEBUG_PRINTF("Old contact IDs (B):"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->previous_idGeometryB, *scratchPad.numPrevContacts)); - // DEME_DEBUG_PRINTF("Old contact types:"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->previous_contactType, *scratchPad.numPrevContacts)); - - // For history-based models, construct the enduring contact map. We dwell on the fact that idA is always - // for a sphere. - // This CD run and previous CD run could have different number of spheres in them. We pick the larger - // number to refer in building the persistent contact map to avoid potential problems. - size_t nSpheresSafe = - (simParams->nSpheresGM > *scratchPad.numPrevSpheres) ? simParams->nSpheresGM : *scratchPad.numPrevSpheres; - - // First, identify the new and old idA run-length - size_t run_length_bytes = nSpheresSafe * sizeof(geoSphereTouches_t); - geoSphereTouches_t* new_idA_runlength = - (geoSphereTouches_t*)scratchPad.allocateTempVector("new_idA_runlength", run_length_bytes); - size_t unique_id_bytes = nSpheresSafe * sizeof(bodyID_t); - bodyID_t* unique_new_idA = (bodyID_t*)scratchPad.allocateTempVector("unique_new_idA", unique_id_bytes); - scratchPad.allocateDualStruct("numUniqueNewA"); - cubDEMRunLengthEncode(granData->idGeometryA, unique_new_idA, new_idA_runlength, - scratchPad.getDualStructDevice("numUniqueNewA"), - *scratchPad.numContacts, this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("numUniqueNewA"); - size_t* pNumUniqueNewA = scratchPad.getDualStructHost("numUniqueNewA"); - // Now, we do a tab-keeping job: how many contacts on average a sphere has? - { - // Figure out how many contacts an item in idA array typically has. - stateParams.avgCntsPerSphere = - (*pNumUniqueNewA > 0) ? (float)(*scratchPad.numContacts) / (float)(*pNumUniqueNewA) : 0.0; - - DEME_STEP_DEBUG_PRINTF("Average number of contacts for each geometry: %.7g", stateParams.avgCntsPerSphere); - if (stateParams.avgCntsPerSphere > solverFlags.errOutAvgSphCnts) { - DEME_ERROR( - "On average a sphere has %.7g contacts, more than the max allowance (%.7g).\nIf you believe " - "this is not abnormal, set the allowance high using SetErrorOutAvgContacts before " - "initialization.\nIf you think this is because dT drifting too much ahead of kT so the contact " - "margin added is too big, use SetCDMaxUpdateFreq to limit the max dT future drift.\nOtherwise, the " - "simulation may have diverged and relaxing the physics may help, such as decreasing the step size " - "and modifying material properties.\nIf this happens at the start of simulation, check if there " - "are initial penetrations, a.k.a. elements initialized inside walls.", - stateParams.avgCntsPerSphere, solverFlags.errOutAvgSphCnts); - } - } - - // Only need to proceed if history-based - if (!solverFlags.isHistoryless) { - geoSphereTouches_t* old_idA_runlength = - (geoSphereTouches_t*)scratchPad.allocateTempVector("old_idA_runlength", run_length_bytes); - bodyID_t* unique_old_idA = (bodyID_t*)scratchPad.allocateTempVector("unique_old_idA", unique_id_bytes); - scratchPad.allocateDualStruct("numUniqueOldA"); - cubDEMRunLengthEncode(granData->previous_idGeometryA, unique_old_idA, - old_idA_runlength, - scratchPad.getDualStructDevice("numUniqueOldA"), - *(scratchPad.numPrevContacts), this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("numUniqueOldA"); - size_t* pNumUniqueOldA = scratchPad.getDualStructHost("numUniqueOldA"); - // Then, add zeros to run-length arrays such that even if a sphereID is not present in idA, it has a - // place in the run-length arrays that indicates 0 run-length - geoSphereTouches_t* new_idA_runlength_full = - (geoSphereTouches_t*)scratchPad.allocateTempVector("new_idA_runlength_full", run_length_bytes); - geoSphereTouches_t* old_idA_runlength_full = - (geoSphereTouches_t*)scratchPad.allocateTempVector("old_idA_runlength_full", run_length_bytes); - DEME_GPU_CALL(cudaMemset((void*)new_idA_runlength_full, 0, run_length_bytes)); - DEME_GPU_CALL(cudaMemset((void*)old_idA_runlength_full, 0, run_length_bytes)); - size_t blocks_needed_for_mapping = - (*pNumUniqueNewA + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_mapping > 0) { - history_kernels->kernel("fillRunLengthArray") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(new_idA_runlength_full, unique_new_idA, new_idA_runlength, *pNumUniqueNewA); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - - blocks_needed_for_mapping = (*pNumUniqueOldA + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_mapping > 0) { - history_kernels->kernel("fillRunLengthArray") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(old_idA_runlength_full, unique_old_idA, old_idA_runlength, *pNumUniqueOldA); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // DEME_DEBUG_PRINTF("Unique contact IDs (A):"); - // DEME_DEBUG_EXEC(displayDeviceArray(unique_new_idA, *pNumUniqueNewA)); - // DEME_DEBUG_PRINTF("Unique contacts run-length:"); - // DEME_DEBUG_EXEC(displayDeviceArray(new_idA_runlength, *pNumUniqueNewA)); - scratchPad.finishUsingTempVector("old_idA_runlength"); - scratchPad.finishUsingTempVector("unique_old_idA"); - scratchPad.finishUsingDualStruct("numUniqueOldA"); - - // Then, prescan to find run-length offsets, in preparation for custom kernels - size_t scanned_runlength_bytes = nSpheresSafe * sizeof(contactPairs_t); - contactPairs_t* new_idA_scanned_runlength = - (contactPairs_t*)scratchPad.allocateTempVector("new_idA_scanned_runlength", scanned_runlength_bytes); - contactPairs_t* old_idA_scanned_runlength = - (contactPairs_t*)scratchPad.allocateTempVector("old_idA_scanned_runlength", scanned_runlength_bytes); - cubDEMPrefixScan(new_idA_runlength_full, new_idA_scanned_runlength, - nSpheresSafe, this_stream, scratchPad); - cubDEMPrefixScan(old_idA_runlength_full, old_idA_scanned_runlength, - nSpheresSafe, this_stream, scratchPad); - - // Then, each thread will scan a sphere, if this sphere has non-zero run-length in both new and old idA, - // manually store the mapping. This mapping's elemental values are the indices of the corresponding - // contacts in the previous contact array. - if (*scratchPad.numContacts > contactMapping.size()) { - DEME_DUAL_ARRAY_RESIZE_NOVAL(contactMapping, *scratchPad.numContacts); - granData.toDevice(); - } - blocks_needed_for_mapping = (nSpheresSafe + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - if (blocks_needed_for_mapping > 0) { - history_kernels->kernel("buildPersistentMap") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(new_idA_runlength_full, old_idA_runlength_full, new_idA_scanned_runlength, - old_idA_scanned_runlength, granData->contactMapping, &granData, nSpheresSafe); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // DEME_DEBUG_PRINTF("Contact mapping:"); - // DEME_DEBUG_EXEC(displayDeviceArray(granData->contactMapping, - // *scratchPad.numContacts)); - scratchPad.finishUsingTempVector("new_idA_runlength_full"); - scratchPad.finishUsingTempVector("old_idA_runlength_full"); - scratchPad.finishUsingTempVector("new_idA_scanned_runlength"); - scratchPad.finishUsingTempVector("old_idA_scanned_runlength"); - - // One thing we need to do before storing the old contact pairs: figure out how it is mapped to the actually - // shipped contact pair array. - contactPairs_t* old_arr_unsort_to_sort_map; - if (solverFlags.should_sort_pairs) { - size_t map_arr_bytes = (*scratchPad.numPrevContacts) * sizeof(contactPairs_t); - old_arr_unsort_to_sort_map = - (contactPairs_t*)scratchPad.allocateTempVector("old_arr_unsort_to_sort_map", map_arr_bytes); - contactPairs_t* one_to_n = (contactPairs_t*)scratchPad.allocateTempVector("one_to_n", map_arr_bytes); - size_t blocks_needed_for_mapping = - (*scratchPad.numPrevContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_mapping > 0) { - history_kernels->kernel("lineNumbers") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(one_to_n, *scratchPad.numPrevContacts); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - contact_t* old_contactType_sorted = (contact_t*)scratchPad.allocateTempVector( - "old_contactType_sorted", (*scratchPad.numPrevContacts) * sizeof(contact_t)); - // Sorted by type is how we shipped the old contact pair info - cubDEMSortByKeys(granData->previous_contactType, old_contactType_sorted, - one_to_n, old_arr_unsort_to_sort_map, - *scratchPad.numPrevContacts, this_stream, scratchPad); - // Now, we have `map from' info. But we need `map to' info. - history_kernels->kernel("convertToAndFrom") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(old_arr_unsort_to_sort_map, one_to_n, *scratchPad.numPrevContacts); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - // one_to_n used for temp storage; now give it back to the true mapping we wanted. - // So here, old_arr_unsort_to_sort_map's memory space is not needed anymore, but one_to_n must still - // live, a little nuance to pay attention to. However, alas, we can always just delay memory freeing - // and do it all at the very end: this is exactly what I did here. - old_arr_unsort_to_sort_map = one_to_n; - scratchPad.finishUsingTempVector("old_contactType_sorted"); - } - - // Finally, copy new contact array to old contact array for the record. Note we register old contact pairs - // with the array sorted by A, but when supplying dT, it was sorted by contact type. - if (*scratchPad.numContacts > previous_idGeometryA.size()) { - // Note these resizing are automatically on kT's device - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idGeometryA, *scratchPad.numContacts); - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idGeometryB, *scratchPad.numContacts); - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_contactType, *scratchPad.numContacts); - - // Re-packing pointers now is automatic - granData.toDevice(); - } - DEME_GPU_CALL(cudaMemcpy(granData->previous_idGeometryA, granData->idGeometryA, id_arr_bytes, - cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->previous_idGeometryB, granData->idGeometryB, id_arr_bytes, - cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->previous_contactType, granData->contactType, type_arr_bytes, - cudaMemcpyDeviceToDevice)); - - // dT potentially benefits from type-sorted contact array - if (solverFlags.should_sort_pairs) { - size_t type_arr_bytes = (*scratchPad.numContacts) * sizeof(contact_t); - contact_t* contactType_sorted = - (contact_t*)scratchPad.allocateTempVector("contactType_sorted", type_arr_bytes); - size_t id_arr_bytes = (*scratchPad.numContacts) * sizeof(bodyID_t); - bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", id_arr_bytes); - bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", id_arr_bytes); - size_t cnt_arr_bytes = (*scratchPad.numContacts) * sizeof(contactPairs_t); - contactPairs_t* map_sorted = - (contactPairs_t*)scratchPad.allocateTempVector("map_sorted", cnt_arr_bytes); - - //// TODO: But do I have to SortByKey three times?? Can I zip these value arrays together?? - cubDEMSortByKeys(granData->contactType, contactType_sorted, granData->idGeometryB, - idB_sorted, *scratchPad.numContacts, this_stream, scratchPad); - cubDEMSortByKeys(granData->contactType, contactType_sorted, granData->idGeometryA, - idA_sorted, *scratchPad.numContacts, this_stream, scratchPad); - cubDEMSortByKeys(granData->contactType, contactType_sorted, - granData->contactMapping, map_sorted, - *scratchPad.numContacts, this_stream, scratchPad); - - // Finally, map the mapping array so it takes into account that arrays are shipped after sorting. - size_t blocks_needed_for_mapping = - (*scratchPad.numContacts + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; - if (blocks_needed_for_mapping > 0) { - history_kernels->kernel("rearrangeMapping") - .instantiate() - .configure(dim3(blocks_needed_for_mapping), dim3(DEME_MAX_THREADS_PER_BLOCK), 0, this_stream) - .launch(map_sorted, old_arr_unsort_to_sort_map, *scratchPad.numContacts); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - } - - // Copy back to idGeometry arrays - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryA, idA_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryB, idB_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL( - cudaMemcpy(granData->contactType, contactType_sorted, type_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL( - cudaMemcpy(granData->contactMapping, map_sorted, cnt_arr_bytes, cudaMemcpyDeviceToDevice)); - - scratchPad.finishUsingTempVector("contactType_sorted"); - scratchPad.finishUsingTempVector("idA_sorted"); - scratchPad.finishUsingTempVector("idB_sorted"); - scratchPad.finishUsingTempVector("map_sorted"); - } - } else { // If historyless, might still want to sort based on type - if (solverFlags.should_sort_pairs) { - size_t type_arr_bytes = (*scratchPad.numContacts) * sizeof(contact_t); - contact_t* contactType_sorted = - (contact_t*)scratchPad.allocateTempVector("contactType_sorted", type_arr_bytes); - size_t id_arr_bytes = (*scratchPad.numContacts) * sizeof(bodyID_t); - bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", id_arr_bytes); - bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", id_arr_bytes); - - cubDEMSortByKeys(granData->contactType, contactType_sorted, granData->idGeometryB, - idB_sorted, *scratchPad.numContacts, this_stream, scratchPad); - cubDEMSortByKeys(granData->contactType, contactType_sorted, granData->idGeometryA, - idA_sorted, *scratchPad.numContacts, this_stream, scratchPad); - - // Copy back to idGeometry arrays - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryA, idA_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(granData->idGeometryB, idB_sorted, id_arr_bytes, cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL( - cudaMemcpy(granData->contactType, contactType_sorted, type_arr_bytes, cudaMemcpyDeviceToDevice)); - scratchPad.finishUsingTempVector("contactType_sorted"); - scratchPad.finishUsingTempVector("idA_sorted"); - scratchPad.finishUsingTempVector("idB_sorted"); - } - } - // This part is light on memory, so we can delay some freeing - scratchPad.finishUsingTempVector("new_idA_runlength"); - scratchPad.finishUsingTempVector("unique_new_idA"); - scratchPad.finishUsingDualStruct("numUniqueNewA"); - // Note one_to_n corresponds to the pointer name old_arr_unsort_to_sort_map, an exception to general "matched" - // naming convension. But we delayed their freeing, so you see them both freed here. - scratchPad.finishUsingTempVector("one_to_n"); - scratchPad.finishUsingTempVector("old_arr_unsort_to_sort_map"); - - } // End of contact sorting--mapping subroutine - timers.GetTimer("Build history map").stop(); - - // Finally, don't forget to store the number of contacts for the next iteration, even if there is 0 contacts (in - // that case, mapping will not be constructed, but we don't have to worry b/c in the next iteration, simply no work - // will be done for the old array and every contact will be new) - *scratchPad.numPrevContacts = *scratchPad.numContacts; - *scratchPad.numPrevSpheres = simParams->nSpheresGM; - - // dT kT may send these numbers to each other from device - scratchPad.numContacts.toDevice(); - scratchPad.numPrevContacts.toDevice(); - scratchPad.numPrevSpheres.toDevice(); -} - -void overwritePrevContactArrays(DualStruct& kT_data, - DualStruct& dT_data, - DualArray& previous_idGeometryA, - DualArray& previous_idGeometryB, - DualArray& previous_contactType, - DualStruct& simParams, - DualArray& contactPersistency, - DEMSolverScratchData& scratchPad, - cudaStream_t& this_stream, - size_t nContacts) { - // Make sure the storage is large enough - if (nContacts > previous_idGeometryA.size()) { - // Note these resizing are automatically on kT's device - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idGeometryA, nContacts); - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_idGeometryB, nContacts); - DEME_DUAL_ARRAY_RESIZE_NOVAL(previous_contactType, nContacts); - // In the case of user-loaded contacts, if the persistency array is not long enough then we have to manually - // extend it. - DEME_DUAL_ARRAY_RESIZE(contactPersistency, nContacts, CONTACT_NOT_PERSISTENT); - - // Re-packing pointers now is automatic - kT_data.toDevice(); - } - - // Copy to temp array for easier usage - bodyID_t* idA = (bodyID_t*)scratchPad.allocateTempVector("idA", nContacts * sizeof(bodyID_t)); - bodyID_t* idB = (bodyID_t*)scratchPad.allocateTempVector("idB", nContacts * sizeof(bodyID_t)); - contact_t* cType = (contact_t*)scratchPad.allocateTempVector("cType", nContacts * sizeof(contact_t)); - DEME_GPU_CALL(cudaMemcpy(idA, dT_data->idGeometryA, nContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(idB, dT_data->idGeometryB, nContacts * sizeof(bodyID_t), cudaMemcpyDeviceToDevice)); - DEME_GPU_CALL(cudaMemcpy(cType, dT_data->contactType, nContacts * sizeof(contact_t), cudaMemcpyDeviceToDevice)); - - // Prev contact arrays actually need to be sorted based on idA - // bodyID_t* idA_sorted = (bodyID_t*)scratchPad.allocateTempVector("idA_sorted", nContacts * sizeof(bodyID_t)); - // bodyID_t* idB_sorted = (bodyID_t*)scratchPad.allocateTempVector("idB_sorted", nContacts * sizeof(bodyID_t)); - // contact_t* cType_sorted = (contact_t*)scratchPad.allocateTempVector("cType_sorted", nContacts * - // sizeof(contact_t)); DEME_GPU_CALL(cudaMemcpy(idA_sorted, idA, nContacts * sizeof(bodyID_t), - // cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(idB_sorted, idB, nContacts * sizeof(bodyID_t), - // cudaMemcpyDeviceToDevice)); DEME_GPU_CALL(cudaMemcpy(cType_sorted, cType, nContacts * sizeof(contact_t), - // cudaMemcpyDeviceToDevice)); hostSortByKey(idA, idB_sorted, nContacts); hostSortByKey(idA_sorted, cType_sorted, - // nContacts); DEME_GPU_CALL( - // cudaMemcpy(kT_data->previous_idGeometryA, idA_sorted, nContacts * sizeof(bodyID_t), - // cudaMemcpyDeviceToDevice)); - // DEME_GPU_CALL( - // cudaMemcpy(kT_data->previous_idGeometryB, idB_sorted, nContacts * sizeof(bodyID_t), - // cudaMemcpyDeviceToDevice)); - // DEME_GPU_CALL(cudaMemcpy(kT_data->previous_contactType, cType_sorted, nContacts * sizeof(contact_t), - // cudaMemcpyDeviceToDevice)); - cubDEMSortByKeys(idA, kT_data->previous_idGeometryA, idB, kT_data->previous_idGeometryB, - nContacts, this_stream, scratchPad); - cubDEMSortByKeys(idA, kT_data->previous_idGeometryA, cType, kT_data->previous_contactType, - nContacts, this_stream, scratchPad); - - *scratchPad.numPrevContacts = nContacts; - // If nSpheresGM is updated, then it should have been taken care of in the init/populate array phase and in kT's - // simParams now - *scratchPad.numPrevSpheres = simParams->nSpheresGM; - // dT kT may send these numbers to each other from device - scratchPad.numPrevContacts.toDevice(); - scratchPad.numPrevSpheres.toDevice(); - - scratchPad.finishUsingTempVector("idA"); - scratchPad.finishUsingTempVector("idB"); - scratchPad.finishUsingTempVector("cType"); - // scratchPad.finishUsingTempVector("idA_sorted"); - // scratchPad.finishUsingTempVector("idB_sorted"); - // scratchPad.finishUsingTempVector("cType_sorted"); -} - -} // namespace deme diff --git a/src/algorithms/DEMCubForceCollection.cu b/src/algorithms/DEMCubForceCollection.cu deleted file mode 100644 index f91d1490..00000000 --- a/src/algorithms/DEMCubForceCollection.cu +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// -// SPDX-License-Identifier: BSD-3-Clause - -#include -#if defined(USE_HIP) - #include -#else - #include -#endif -// #include -#include - -#include -#include - -#include - -#include - -namespace deme { - -void collectContactForcesThruCub(std::shared_ptr& collect_force_kernels, - DualStruct& granData, - const size_t nContactPairs, - const size_t nClumps, - bool contactPairArr_isFresh, - cudaStream_t& this_stream, - DEMSolverScratchData& scratchPad, - SolverTimers& timers) { - // Preparation: allocate enough temp array memory and chop it to pieces, for the usage of cub operations. Note that - // if contactPairArr_isFresh is false, then this allocation should not alter the size and content of the temp array - // space, so the information in it can be used in the next iteration. - size_t cachedArraySizeOwner = (size_t)2 * nContactPairs * sizeof(bodyID_t); - // Use temp vector to store the flattened owner IDs. - // Note this one is not temp vector as it could be used between time steps. - bodyID_t* idAOwner = (bodyID_t*)scratchPad.allocateVector("idAOwner", cachedArraySizeOwner); - bodyID_t* idBOwner = (bodyID_t*)(idAOwner + nContactPairs); - // size_t cachedArraySizeMass = (size_t)2 * nContactPairs * sizeof(float); - // size_t cachedArraySizeMOI = (size_t)2 * nContactPairs * sizeof(float3); - // float* massAOwner = (float*)scratchPad.allocateCachedMass(cachedArraySizeMass); - // float* massBOwner = (float*)(massAOwner + nContactPairs); - // float3* moiAOwner = (float3*)scratchPad.allocateCachedMOI(cachedArraySizeMOI); - // float3* moiBOwner = (float3*)(moiAOwner + nContactPairs); - - // size_t blocks_needed_for_twice_contacts = (2 * nContactPairs + DEME_NUM_BODIES_PER_BLOCK - 1) / - // DEME_NUM_BODIES_PER_BLOCK; - size_t blocks_needed_for_contacts = (nContactPairs + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - if (contactPairArr_isFresh) { - // First step, prepare the owner ID array (nContactPairs * bodyID_t) for usage in final reduction by key (do it - // for both A and B) - // Note for A, it is always a sphere or a triangle - collect_force_kernels->kernel("cashInOwnerIndexA") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(idAOwner, granData->idGeometryA, granData->ownerClumpBody, granData->contactType, nContactPairs); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - // But for B, it can be sphere, triangle or some analytical geometries - collect_force_kernels->kernel("cashInOwnerIndexB") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(idBOwner, granData->idGeometryB, granData->ownerClumpBody, granData->ownerMesh, - granData->contactType, nContactPairs); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // displayDeviceArray(idAOwner, nContactPairs); - // displayDeviceArray(idBOwner, nContactPairs); - } - - // ============================================== - // 2nd, combine mass and force to get (contact pair-wise) acceleration, which will be reduced... - // Note here allocated is temp vector, since unlike cached vectors, they cannot be reused in the next iteration - size_t tempArraySizeAcc = (size_t)2 * nContactPairs * sizeof(float3); - size_t tempArraySizeAcc_sorted = (size_t)2 * nContactPairs * sizeof(float3); - size_t tempArraySizeOwnerAcc = (size_t)nClumps * sizeof(float3); - size_t tempArraySizeOwner = (size_t)nClumps * sizeof(bodyID_t); - float3* acc_A = (float3*)scratchPad.allocateTempVector("acc_A", tempArraySizeAcc); - float3* acc_B = (float3*)(acc_A + nContactPairs); - float3* acc_A_sorted = (float3*)scratchPad.allocateTempVector("acc_A_sorted", tempArraySizeAcc_sorted); - // float3* acc_B_sorted = (float3*)(acc_A_sorted + nContactPairs); - bodyID_t* idAOwner_sorted = (bodyID_t*)scratchPad.allocateTempVector("idAOwner_sorted", cachedArraySizeOwner); - // bodyID_t* idBOwner_sorted = (bodyID_t*)(idAOwner_sorted + nContactPairs); - float3* accOwner = (float3*)scratchPad.allocateTempVector( - "accOwner", tempArraySizeOwnerAcc); // can store both linear and angular acceleration - bodyID_t* uniqueOwner = (bodyID_t*)scratchPad.allocateTempVector("uniqueOwner", tempArraySizeOwner); - // Collect accelerations for body A (modifier used to be h * h / l when we stored acc as h^2*acc) - // NOTE!! If you pass floating point number to kernels, the number needs to be something like 1.f, not 1.0. - // Somtimes 1.0 got converted to 0.f with the kernel call. - collect_force_kernels->kernel("forceToAcc") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(acc_A, granData->contactForces, idAOwner, 1.f, nContactPairs, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // and don't forget body B - collect_force_kernels->kernel("forceToAcc") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(acc_B, granData->contactForces, idBOwner, -1.f, nContactPairs, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // displayDeviceFloat3(acc_A, 2 * nContactPairs); - // displayDeviceFloat3(granData->contactForces, nContactPairs); - - // Reducing the acceleration (2 * nContactPairs for both body A and B) - // Note: to do this, idAOwner needs to be sorted along with acc_A. So we sort first. - cubDEMSortByKeys(idAOwner, idAOwner_sorted, acc_A, acc_A_sorted, nContactPairs * 2, this_stream, - scratchPad); - // Then we reduce by key - // This variable stores the cub output of how many cub runs it executed for collecting forces - scratchPad.allocateDualStruct("forceCollectionRuns"); - size_t* dpForceCollectionRuns = scratchPad.getDualStructDevice("forceCollectionRuns"); - size_t* hpForceCollectionRuns = scratchPad.getDualStructHost("forceCollectionRuns"); - CubFloat3Add float3_add_op; - cubDEMReduceByKeys(idAOwner_sorted, uniqueOwner, acc_A_sorted, accOwner, - dpForceCollectionRuns, float3_add_op, nContactPairs * 2, - this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("forceCollectionRuns"); - // Then we stash acceleration - size_t blocks_needed_for_stashing = - (*hpForceCollectionRuns + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - collect_force_kernels->kernel("stashElem") - .instantiate() - .configure(dim3(blocks_needed_for_stashing), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(granData->aX, granData->aY, granData->aZ, uniqueOwner, accOwner, *hpForceCollectionRuns); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // displayDeviceArray(granData->aX, nClumps); - // displayDeviceArray(granData->aY, nClumps); - // displayDeviceArray(granData->aZ, nClumps); - - // ===================================================== - // Then take care of angular accelerations - float3* alpha_A = (float3*)(acc_A); // Memory spaces for accelerations can be reused - float3* alpha_B = (float3*)(acc_B); - float3* alpha_A_sorted = (float3*)(acc_A_sorted); - // float3* alpha_B_sorted = (float3*)(acc_B_sorted); - // collect angular accelerations for body A (modifier used to be h * h when we stored acc as h^2*acc) - collect_force_kernels->kernel("forceToAngAcc") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(alpha_A, granData->contactPointGeometryA, granData->oriQw, granData->oriQx, granData->oriQy, - granData->oriQz, granData->contactForces, granData->contactTorque_convToForce, idAOwner, 1.f, - nContactPairs, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // and don't forget body B - collect_force_kernels->kernel("forceToAngAcc") - .instantiate() - .configure(dim3(blocks_needed_for_contacts), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(alpha_B, granData->contactPointGeometryB, granData->oriQw, granData->oriQx, granData->oriQy, - granData->oriQz, granData->contactForces, granData->contactTorque_convToForce, idBOwner, -1.f, - nContactPairs, &granData); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - // Reducing the angular acceleration (2 * nContactPairs for both body A and B) - // Note: to do this, idAOwner needs to be sorted along with alpha_A. So we sort first. - cubDEMSortByKeys(idAOwner, idAOwner_sorted, alpha_A, alpha_A_sorted, nContactPairs * 2, - this_stream, scratchPad); - // Then we reduce - cubDEMReduceByKeys(idAOwner_sorted, uniqueOwner, alpha_A_sorted, accOwner, - dpForceCollectionRuns, float3_add_op, nContactPairs * 2, - this_stream, scratchPad); - scratchPad.syncDualStructDeviceToHost("forceCollectionRuns"); - // Then we stash angular acceleration - blocks_needed_for_stashing = (*hpForceCollectionRuns + DEME_NUM_BODIES_PER_BLOCK - 1) / DEME_NUM_BODIES_PER_BLOCK; - collect_force_kernels->kernel("stashElem") - .instantiate() - .configure(dim3(blocks_needed_for_stashing), dim3(DEME_NUM_BODIES_PER_BLOCK), 0, this_stream) - .launch(granData->alphaX, granData->alphaY, granData->alphaZ, uniqueOwner, accOwner, *hpForceCollectionRuns); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); - - scratchPad.finishUsingTempVector("acc_A"); - scratchPad.finishUsingTempVector("acc_A_sorted"); - scratchPad.finishUsingTempVector("idAOwner_sorted"); - scratchPad.finishUsingTempVector("accOwner"); - scratchPad.finishUsingTempVector("uniqueOwner"); - scratchPad.finishUsingDualStruct("forceCollectionRuns"); -} - -} // namespace deme diff --git a/src/algorithms/DEMCubInstantiations.cu b/src/algorithms/DEMCubInstantiations.cu index a71f7f4b..5e26f4b6 100644 --- a/src/algorithms/DEMCubInstantiations.cu +++ b/src/algorithms/DEMCubInstantiations.cu @@ -3,15 +3,9 @@ // // SPDX-License-Identifier: BSD-3-Clause -#include -#if defined(USE_HIP) - #include -#else - #include -#endif +#include #include -#include #include namespace deme { @@ -76,6 +70,33 @@ template void cubSumReduceByKey(notStupidBool_t* d_keys size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); +// Special instantiation for float3 (normals etc.) with contactPairs_t keys (for voting) +template void cubSumReduceByKey(contactPairs_t* d_keys_in, + contactPairs_t* d_unique_out, + float3* d_vals_in, + float3* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); +// Special instantiation for double3 (contact points etc.) with contactPairs_t keys (for voting) +template void cubSumReduceByKey(contactPairs_t* d_keys_in, + contactPairs_t* d_unique_out, + double3* d_vals_in, + double3* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); +// Special instantiation for double (area) with contactPairs_t keys (for voting) +template void cubSumReduceByKey(contactPairs_t* d_keys_in, + contactPairs_t* d_unique_out, + double* d_vals_in, + double* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); //////////////////////////////////////////////////////////////////////////////// // Reduce::Max @@ -131,6 +152,42 @@ template void cubMaxReduceByKey(notStupidBool_t* d_keys size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); +// Special instantiation for double (penetration) with contactPairs_t keys (for voting zero-area case) +template void cubMaxReduceByKey(contactPairs_t* d_keys_in, + contactPairs_t* d_unique_out, + double* d_vals_in, + double* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + +//////////////////////////////////////////////////////////////////////////////// +// Reduce::MaxNegative (finds largest negative value, treats positives as very negative) +//////////////////////////////////////////////////////////////////////////////// + +template +void cubMaxNegativeReduceByKey(T1* d_keys_in, + T1* d_unique_out, + T2* d_vals_in, + T2* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + CubOpMaxNegative max_negative_op; + cubDEMReduceByKeys>(d_keys_in, d_unique_out, d_vals_in, d_aggregates_out, d_num_out, + max_negative_op, n, this_stream, scratchPad); +} +// Special instantiation for double (penetration) with contactPairs_t keys (for finding max negative penetration) +template void cubMaxNegativeReduceByKey(contactPairs_t* d_keys_in, + contactPairs_t* d_unique_out, + double* d_vals_in, + double* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); //////////////////////////////////////////////////////////////////////////////// // Reduce::Min @@ -210,4 +267,44 @@ template void cubSortByKey(notStupidBool_t* d_keys_in, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); + +//////////////////////////////////////////////////////////////////////////////// +// Vector-wise encoding or scanning +//////////////////////////////////////////////////////////////////////////////// + +template +void cubRunLengthEncode(T1* d_in, + T1* d_unique_out, + T2* d_counts_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + cubDEMRunLengthEncode(d_in, d_unique_out, d_counts_out, d_num_out, n, this_stream, scratchPad); +} +template void cubRunLengthEncode(contact_t* d_in, + contact_t* d_unique_out, + contactPairs_t* d_counts_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); +template void cubRunLengthEncode(patchIDPair_t* d_in, + patchIDPair_t* d_unique_out, + contactPairs_t* d_counts_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + +template +void cubPrefixScan(T1* d_in, T2* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad) { + cubDEMPrefixScan(d_in, d_out, n, this_stream, scratchPad); +} +template void cubPrefixScan(contactPairs_t* d_in, + contactPairs_t* d_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + } // namespace deme diff --git a/src/algorithms/DEMCubWrappers.cu b/src/algorithms/DEMCubWrappers.cu index f2af1046..a6857f94 100644 --- a/src/algorithms/DEMCubWrappers.cu +++ b/src/algorithms/DEMCubWrappers.cu @@ -3,19 +3,9 @@ // // SPDX-License-Identifier: BSD-3-Clause -#include -#if defined(USE_HIP) - #include - // CUB_RUNTIME_FUNCTION is a CUB macro not defined in hipCUB - #ifndef CUB_RUNTIME_FUNCTION - #define CUB_RUNTIME_FUNCTION - #endif -#else - #include -#endif +#include #include #include -#include #if CUDART_VERSION >= 13000 #define DEME_CUB_SUM_OP(T) \ @@ -63,6 +53,32 @@ struct CubOpMax { } }; +// Custom functor for finding the "max negative" value: +// - Among negative values, find the one closest to zero (largest negative = smallest absolute value) +// - Positive values are treated as having a huge negative value (very far from zero) +// This is useful for finding the least-penetrating contact when all penetrations should be negative +template +struct CubOpMaxNegative { + CUB_RUNTIME_FUNCTION __forceinline__ __device__ __host__ T operator()(const T& a, const T& b) const { + // If both are negative, return the one closest to zero (max of the two negatives) + if (a < 0 && b < 0) { + return (b > a) ? b : a; + } + // If only a is negative, prefer a + if (a < 0) { + return a; + } + // If only b is negative, prefer b + if (b < 0) { + return b; + } + // If both are positive (or zero), return the smaller one to penalize it + // But actually we want to return a very negative value to indicate invalid state + // Return the more negative of the two positive values (the smaller positive) + return (a < b) ? a : b; + } +}; + template inline void cubDEMSelectFlagged(T1* d_in, T1* d_out, @@ -73,10 +89,9 @@ inline void cubDEMSelectFlagged(T1* d_in, DEMSolverScratchData& scratchPad) { size_t cub_scratch_bytes = 0; cub::DeviceSelect::Flagged(NULL, cub_scratch_bytes, d_in, d_flags, d_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceSelect::Flagged(d_scratch_space, cub_scratch_bytes, d_in, d_flags, d_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -93,11 +108,39 @@ inline void cubDEMPrefixScan(T1* d_in, // cub to store results as T2 type. size_t cub_scratch_bytes = 0; cub::DeviceScan::ExclusiveScan(NULL, cub_scratch_bytes, d_in, d_out, DEME_CUB_SUM_OP(T2), (T2)0, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceScan::ExclusiveScan(d_scratch_space, cub_scratch_bytes, d_in, d_out, DEME_CUB_SUM_OP(T2), (T2)0, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); +} + +template +inline void cubDEMInclusiveScan(T1* d_in, + T2* d_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + size_t cub_scratch_bytes = 0; + cub::DeviceScan::InclusiveScan(NULL, cub_scratch_bytes, d_in, d_out, DEME_CUB_SUM_OP(T2), n, this_stream); + void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); + cub::DeviceScan::InclusiveScan(d_scratch_space, cub_scratch_bytes, d_in, d_out, DEME_CUB_SUM_OP(T2), n, + this_stream); + DEME_GPU_DEBUG_SYNC(this_stream); +} + +template +inline void cubDEMSortKeys(T1* d_keys_in, + T1* d_keys_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad) { + size_t cub_scratch_bytes = 0; + cub::DeviceRadixSort::SortKeys(NULL, cub_scratch_bytes, d_keys_in, d_keys_out, n, 0, + sizeof(T1) * DEME_BITS_PER_BYTE, this_stream); + void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); + cub::DeviceRadixSort::SortKeys(d_scratch_space, cub_scratch_bytes, d_keys_in, d_keys_out, n, 0, + sizeof(T1) * DEME_BITS_PER_BYTE, this_stream); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -111,11 +154,10 @@ inline void cubDEMSortByKeys(T1* d_keys_in, size_t cub_scratch_bytes = 0; cub::DeviceRadixSort::SortPairs(NULL, cub_scratch_bytes, d_keys_in, d_keys_out, d_vals_in, d_vals_out, n, 0, sizeof(T1) * DEME_BITS_PER_BYTE, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceRadixSort::SortPairs(d_scratch_space, cub_scratch_bytes, d_keys_in, d_keys_out, d_vals_in, d_vals_out, n, 0, sizeof(T1) * DEME_BITS_PER_BYTE, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -127,10 +169,9 @@ inline void cubDEMUnique(T1* d_in, DEMSolverScratchData& scratchPad) { size_t cub_scratch_bytes = 0; cub::DeviceSelect::Unique(NULL, cub_scratch_bytes, d_in, d_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceSelect::Unique(d_scratch_space, cub_scratch_bytes, d_in, d_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -144,11 +185,10 @@ inline void cubDEMRunLengthEncode(T1* d_in, size_t cub_scratch_bytes = 0; cub::DeviceRunLengthEncode::Encode(NULL, cub_scratch_bytes, d_in, d_unique_out, d_counts_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceRunLengthEncode::Encode(d_scratch_space, cub_scratch_bytes, d_in, d_unique_out, d_counts_out, d_num_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -164,42 +204,38 @@ inline void cubDEMReduceByKeys(T1* d_keys_in, size_t cub_scratch_bytes = 0; cub::DeviceReduce::ReduceByKey(NULL, cub_scratch_bytes, d_keys_in, d_unique_out, d_vals_in, d_aggregates_out, d_num_out, reduce_op, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceReduce::ReduceByKey(d_scratch_space, cub_scratch_bytes, d_keys_in, d_unique_out, d_vals_in, d_aggregates_out, d_num_out, reduce_op, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template void cubDEMSum(T1* d_in, T2* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad) { size_t cub_scratch_bytes = 0; cub::DeviceReduce::Reduce(NULL, cub_scratch_bytes, d_in, d_out, n, DEME_CUB_SUM_OP(T2), (T2)0, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceReduce::Reduce(d_scratch_space, cub_scratch_bytes, d_in, d_out, n, DEME_CUB_SUM_OP(T2), (T2)0, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template void cubDEMMax(T1* d_in, T1* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad) { size_t cub_scratch_bytes = 0; cub::DeviceReduce::Max(NULL, cub_scratch_bytes, d_in, d_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceReduce::Max(d_scratch_space, cub_scratch_bytes, d_in, d_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template void cubDEMMin(T1* d_in, T1* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad) { size_t cub_scratch_bytes = 0; cub::DeviceReduce::Min(NULL, cub_scratch_bytes, d_in, d_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); void* d_scratch_space = (void*)scratchPad.allocateScratchSpace(cub_scratch_bytes); cub::DeviceReduce::Min(d_scratch_space, cub_scratch_bytes, d_in, d_out, n, this_stream); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } } // namespace deme diff --git a/src/algorithms/DEMDynamicMisc.cu b/src/algorithms/DEMDynamicMisc.cu index e4ece147..d3f9e91a 100644 --- a/src/algorithms/DEMDynamicMisc.cu +++ b/src/algorithms/DEMDynamicMisc.cu @@ -6,7 +6,6 @@ #include #include -#include #include namespace deme { @@ -24,12 +23,20 @@ __global__ void getContactForcesConcerningOwners_impl(float3* d_points, bool torque_in_local) { size_t i = blockIdx.x * blockDim.x + threadIdx.x; if (i < numCnt) { - bodyID_t geoA = granData->idGeometryA[i]; - bodyID_t ownerA = granData->ownerClumpBody[geoA]; - bodyID_t geoB = granData->idGeometryB[i]; - contact_t typeB = granData->contactType[i]; - bodyID_t ownerB = DEME_GET_GEO_OWNER_ID(geoB, typeB); - + const bool patch_space = (granData->contactTypePatch && granData->idPatchA && granData->idPatchB); + if (!patch_space) { + // Patch and primitive contact arrays do not share the same index space. This kernel is intentionally + // patch-contact-only; primitive contact queries need a separate launch. + return; + } + contact_t typeContact = granData->contactTypePatch[i]; + if (typeContact == NOT_A_CONTACT) { + return; + } + bodyID_t geoA = granData->idPatchA[i]; + bodyID_t geoB = granData->idPatchB[i]; + bodyID_t ownerA = DEME_GET_PATCH_OWNER_ID(geoA, decodeTypeA(typeContact)); + bodyID_t ownerB = DEME_GET_PATCH_OWNER_ID(geoB, decodeTypeB(typeContact)); bool AorB; // true for A, false for B if (cuda_binary_search(d_ownerIDs, ownerA, 0, IDListSize - 1)) { AorB = true; @@ -39,13 +46,13 @@ __global__ void getContactForcesConcerningOwners_impl(float3* d_points, return; } - float3 force, torque; - force = granData->contactForces[i]; - // Note torque, like force, is in global - if (need_torque) - torque = granData->contactTorque_convToForce[i]; + float3 force = granData->contactForces[i]; + float3 torque_only_force = make_float3(0.f, 0.f, 0.f); + if (need_torque) { + torque_only_force = granData->contactTorque_convToForce[i]; + } { - float mag = (need_torque) ? length(force) + length(torque) : length(force); + float mag = length(force) + length(torque_only_force); if (mag < DEME_TINY_FLOAT) return; } @@ -65,22 +72,12 @@ __global__ void getContactForcesConcerningOwners_impl(float3* d_points, // Force dir flipped force = -force; if (need_torque) - torque = -torque; + torque_only_force = -torque_only_force; } oriQ.w = granData->oriQw[ownerID]; oriQ.x = granData->oriQx[ownerID]; oriQ.y = granData->oriQy[ownerID]; oriQ.z = granData->oriQz[ownerID]; - // Must derive torque in local... - if (need_torque) { - applyOriQToVector3(torque.x, torque.y, torque.z, oriQ.w, -oriQ.x, -oriQ.y, -oriQ.z); - // Force times point... - torque = cross(cntPnt, torque); - if (!torque_in_local) { // back to global if needed - applyOriQToVector3(torque.x, torque.y, torque.z, oriQ.w, oriQ.x, oriQ.y, oriQ.z); - } - } - voxelID_t voxel = granData->voxelID[ownerID]; subVoxelPos_t subVoxX = granData->locX[ownerID]; subVoxelPos_t subVoxY = granData->locY[ownerID]; @@ -91,11 +88,19 @@ __global__ void getContactForcesConcerningOwners_impl(float3* d_points, CoM.x += simParams->LBFX; CoM.y += simParams->LBFY; CoM.z += simParams->LBFZ; + if (need_torque) { + // This is the contact-model "extra torque" represented as a force-like vector, not r x contact force. + // Force-generated torque can be reconstructed from the reported point and force by the caller. + applyOriQToVector3(torque_only_force, make_float4(-oriQ.x, -oriQ.y, -oriQ.z, oriQ.w)); + float3 torque = cross(cntPnt, torque_only_force); + if (!torque_in_local) { + applyOriQToVector3(torque, oriQ); + } + d_torques[writeIndex] = torque; + } applyFrameTransformLocalToGlobal(cntPnt, CoM, oriQ); d_points[writeIndex] = cntPnt; d_forces[writeIndex] = force; - if (need_torque) - d_torques[writeIndex] = torque; } } @@ -115,7 +120,689 @@ void getContactForcesConcerningOwners(float3* d_points, getContactForcesConcerningOwners_impl<<>>( d_points, d_forces, d_torques, reinterpret_cast(d_numUsefulCnt), d_ownerIDs, IDListSize, simParams, granData, numCnt, need_torque, torque_in_local); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); +} + +//////////////////////////////////////////////////////////////////////////////// +// Patch-based voting kernels for mesh contact correction +//////////////////////////////////////////////////////////////////////////////// + +// Optimized kernel: prepares weighted normals only (normal * area). +// Keys are sourced directly from granData->geomToPatchMap by the caller; no key/area output arrays needed. +__global__ void prepareWeightedNormalsForVoting_impl(DEMDataDT* granData, + float3* weightedNormals, + contactPairs_t startOffset, + contactPairs_t count) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < count) { + contactPairs_t myContactID = startOffset + idx; + + const float3 normal = granData->contactForces[myContactID]; + const float3 areaStorage = granData->contactPointGeometryB[myContactID]; + const double area = float3StorageToDouble(areaStorage); + + // Compute weighted normal (normal * area). + // Fake contacts do not contribute since their area is 0. + weightedNormals[idx] = make_float3(normal.x * area, normal.y * area, normal.z * area); + } +} + +void prepareWeightedNormalsForVoting(DEMDataDT* granData, + float3* weightedNormals, + contactPairs_t startOffset, + contactPairs_t count, + cudaStream_t& this_stream) { + size_t blocks_needed = (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + prepareWeightedNormalsForVoting_impl<<>>( + granData, weightedNormals, startOffset, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +// Kernel to normalize the voted normals by dividing by total area and scatter to output +// If total area is 0, set result to (0,0,0) +// Assumes uniqueKeys are sorted (CUB's ReduceByKey maintains sort order) +// Uses contactPairs_t keys (geomToPatchMap values) +__global__ void normalizeAndScatterVotedNormals_impl(float3* votedWeightedNormals, + float3* output, + contactPairs_t count) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < count) { + float3 votedNormal = votedWeightedNormals[idx]; + float len2 = length2(votedNormal); + if (len2 > 0.f) { + // Normalize votedNormal + votedNormal *= rsqrtf(len2); + } else { + // If total area is 0, set to (0,0,0) to mark no real contact + votedNormal = make_float3(0.0f, 0.0f, 0.0f); + } + + // Write to output at the correct position + output[idx] = votedNormal; + } +} + +void normalizeAndScatterVotedNormals(float3* votedWeightedNormals, + float3* output, + contactPairs_t count, + cudaStream_t& this_stream) { + size_t blocks_needed = (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + normalizeAndScatterVotedNormals_impl<<>>( + votedWeightedNormals, output, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Per-primitive weighted quantity computation (fused kernel) +//////////////////////////////////////////////////////////////////////////////// + +// Fused kernel that computes four per-primitive quantities in a single pass: +// projectedAreas[i] = area_i * dot(normal_i, votedNormal) (clamped to >= 0) +// projectedPens[i] = pen_i * dot(normal_i, votedNormal) (clamped to >= 0) +// weights[i] = projectedArea_i * projectedPen_i (w = projArea * projPen) +// weightedCPs[i] = contactPoint_i * weights[i] +// These arrays are later reduced per-patch via CUB: +// projectedAreas and weights and weightedCPs via cubSumReduceByKey, +// projectedPens via cubMaxReduceByKey (to get per-patch max projected penetration). +__global__ void computePerPrimitiveWeightedQuantities_impl(DEMDataDT* granData, + const float3* votedNormals, + const contactPairs_t* keys, + double* projectedAreas, + double* projectedPens, + double* weights, + double3* weightedCPs, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t count) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < count) { + contactPairs_t myContactID = startOffsetPrimitive + idx; + + // Read area, penetration, normal, and contact point stored by the primitive kernel + double area = float3StorageToDouble(granData->contactPointGeometryB[myContactID]); + double pen = float3StorageToDouble(granData->contactPointGeometryA[myContactID]); + if (pen <= 0.0) { + pen = 0.0; + } + float3 normal = granData->contactForces[myContactID]; + double3 cp = to_double3(granData->contactTorque_convToForce[myContactID]); + + // Project onto patch-voted normal + contactPairs_t localPatchIdx = keys[idx] - startOffsetPatch; + float3 vn = votedNormals[localPatchIdx]; + float dp = dot(normal, vn); + + double projArea = area * (double)dp; + double projPen = pen * (double)dp; + if (projArea <= 0.0) { + projArea = 0.0; + } + if (projPen <= 0.0) { + projPen = 0.0; + } + + double w = projArea * projPen; + + projectedAreas[idx] = projArea; + projectedPens[idx] = projPen; + weights[idx] = w; + weightedCPs[idx] = cp * w; + + // printf( + // "voted normal: (%f, %f, %f), original normal: (%f, %f, %f), original pen: %f, dot: %f, " + // "projected pen: %f, area: %f, projected area: %f\n", vn.x, vn.y, vn.z, normal.x, + // normal.y, normal.z, pen, dp, projPen, area, projArea); + } +} + +void computePerPrimitiveWeightedQuantities(DEMDataDT* granData, + const float3* votedNormals, + const contactPairs_t* keys, + double* projectedAreas, + double* projectedPens, + double* weights, + double3* weightedCPs, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t count, + cudaStream_t& this_stream) { + size_t blocks_needed = (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + computePerPrimitiveWeightedQuantities_impl<<>>( + granData, votedNormals, keys, projectedAreas, projectedPens, weights, weightedCPs, startOffsetPrimitive, + startOffsetPatch, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Special case handling: zero-area patches (no positive-penetration primitives) +//////////////////////////////////////////////////////////////////////////////// + +// Kernel to extract primitive penetrations for max-reduce operation +// For zero-area case handling, we need the max (biggest/least-negative) penetration per patch +__global__ void extractPrimitivePenetrations_impl(DEMDataDT* granData, + double* penetrations, + contactPairs_t startOffset, + contactPairs_t count) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < count) { + contactPairs_t myContactID = startOffset + idx; + + // Extract penetration from contactPointGeometryA (stored as double in float3) + float3 penetrationStorage = granData->contactPointGeometryA[myContactID]; + penetrations[idx] = float3StorageToDouble(penetrationStorage); + } +} + +void extractPrimitivePenetrations(DEMDataDT* granData, + double* penetrations, + contactPairs_t startOffset, + contactPairs_t count, + cudaStream_t& this_stream) { + size_t blocks_needed = (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + extractPrimitivePenetrations_impl<<>>( + granData, penetrations, startOffset, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +// Kernel to handle zero-area patches by finding the primitive with max penetration +// and using its penetration, normal, and contact point for the patch result. +// For each primitive, check if it has the max penetration for its patch. +// Note: Race condition when multiple primitives have the same max penetration is acceptable +// since any of them produces a valid result. +__global__ void findMaxPenetrationPrimitiveForZeroAreaPatches_impl(DEMDataDT* granData, + double* maxPenetrations, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + contactPairs_t* keys, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < countPrimitive) { + contactPairs_t myContactID = startOffsetPrimitive + idx; + contactPairs_t patchIdx = keys[idx]; + contactPairs_t localPatchIdx = patchIdx - startOffsetPatch; + + // In fact, we just need to proceed if area is zero or the SAT check failed. But these no-contact cases are so + // common, that we don't do an early termination here. + + // Get this primitive's penetration + float3 penetrationStorage = granData->contactPointGeometryA[myContactID]; + double myPenetration = float3StorageToDouble(penetrationStorage); + + // Check if this primitive has the max penetration for its patch + // Use a relative tolerance for floating-point comparison + double maxPen = maxPenetrations[localPatchIdx]; + double absTol = 1e-15; // Absolute tolerance for very small values + double relTol = 1e-12; // Relative tolerance for larger values + double tolerance = fmax(absTol, fabs(maxPen) * relTol); + if (fabs(myPenetration - maxPen) <= tolerance) { + // This primitive has the max penetration - use its normal, penetration, and contact point + // Note: if multiple primitives have the same max, any one of them is fine + // The race condition is acceptable since all competing values are valid + float3 myNormal = granData->contactForces[myContactID]; + zeroAreaNormals[localPatchIdx] = myNormal; + zeroAreaPenetrations[localPatchIdx] = myPenetration < 0.0 ? myPenetration : -DEME_HUGE_FLOAT; + // This zeroAreaPenetrations should store a negative number, as when it is needed, it's usually the + // separation case (all zero-area primitives). But for the no-SAT case, which can resemble cross-particle + // erroneous detection, we could have a positive max here (search for CubOpMaxNegative to understand how + // this max is derived). In that case, we give it a very negative number, so in the patch-based force + // calculation, this one is considered a non-contact. + + // Also store the contact point from this max-penetration primitive + double3 myContactPoint = to_double3(granData->contactTorque_convToForce[myContactID]); + zeroAreaContactPoints[localPatchIdx] = myContactPoint; + } + } +} + +void findMaxPenetrationPrimitiveForZeroAreaPatches(DEMDataDT* granData, + double* maxPenetrations, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + contactPairs_t* keys, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive, + cudaStream_t& this_stream) { + size_t blocks_needed = (countPrimitive + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + findMaxPenetrationPrimitiveForZeroAreaPatches_impl<<>>( + granData, maxPenetrations, zeroAreaNormals, zeroAreaPenetrations, zeroAreaContactPoints, keys, + startOffsetPrimitive, startOffsetPatch, countPrimitive); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +// Kernel to finalize patch results by combining normal voting results with zero-area case handling. +// finalPen is the max projected penetration among all primitives in the patch (from cubMaxReduceByKey). +// The contact point is still the weight-averaged contact point (weight = projArea * projPen). +__global__ void finalizePatchResults_impl(double* totalProjectedAreas, + double* maxProjPens, + double* totalWeights, + float3* votedNormals, + double3* totalWeightedCPs, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + double* finalAreas, + float3* finalNormals, + double* finalPenetrations, + double3* finalContactPoints, + contactPairs_t count) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < count) { + double projArea = totalProjectedAreas[idx]; + + if (projArea > 0.0) { + // Normal case: use voted results. + // Penetration is the max projected penetration among all primitives in this patch. + finalAreas[idx] = projArea; + finalNormals[idx] = votedNormals[idx]; + finalPenetrations[idx] = maxProjPens[idx]; + double totalWeight = totalWeights[idx]; + if (totalWeight > 0.0) { + finalContactPoints[idx] = totalWeightedCPs[idx] * (1.0 / totalWeight); + } else { + // No positive-weight primitive contributed; set CP to zero (pen = 0 means no force anyway). + finalContactPoints[idx] = make_double3(0.0, 0.0, 0.0); + } + } else { + // Zero-area case: use max-penetration primitive's results (zero-area fallback). + finalAreas[idx] = 0.0; + finalNormals[idx] = zeroAreaNormals[idx]; + finalPenetrations[idx] = zeroAreaPenetrations[idx]; + finalContactPoints[idx] = zeroAreaContactPoints[idx]; + } + } +} + +void finalizePatchResults(double* totalProjectedAreas, + double* maxProjPens, + double* totalWeights, + float3* votedNormals, + double3* totalWeightedCPs, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + double* finalAreas, + float3* finalNormals, + double* finalPenetrations, + double3* finalContactPoints, + contactPairs_t count, + cudaStream_t& this_stream) { + size_t blocks_needed = (count + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + finalizePatchResults_impl<<>>( + totalProjectedAreas, maxProjPens, totalWeights, votedNormals, totalWeightedCPs, zeroAreaNormals, + zeroAreaPenetrations, zeroAreaContactPoints, finalAreas, finalNormals, finalPenetrations, + finalContactPoints, count); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Per-triangle P / V / P*V accumulation from patch contacts +//////////////////////////////////////////////////////////////////////////////// + +__global__ void computePatchPVScalars_impl(const DEMSimParams* simParams, + DEMDataDT* granData, + const float3* finalNormals, + const double3* finalContactPoints, + contactPairs_t startOffsetPatch, + contactPairs_t countPatch, + float* patchNormalForce, + float* patchSlipSpeed) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= countPatch || !patchNormalForce || !patchSlipSpeed || !finalContactPoints) { + return; + } + + patchNormalForce[idx] = 0.f; + patchSlipSpeed[idx] = 0.f; + + const contactPairs_t patchContactID = startOffsetPatch + idx; + const contact_t patchType = granData->contactTypePatch[patchContactID]; + if (patchType == NOT_A_CONTACT) { + return; + } + + float3 normal = finalNormals[idx]; + const float n2 = dot(normal, normal); + if (!(n2 > DEME_TINY_FLOAT)) { + return; + } + normal *= rsqrtf(n2); + + const float3 patchForce = granData->contactForces[patchContactID]; + const float normalForce = fabsf(dot(patchForce, normal)); + if (!(normalForce > DEME_TINY_FLOAT)) { + return; + } + + const bodyID_t geoA = granData->idPatchA[patchContactID]; + const bodyID_t geoB = granData->idPatchB[patchContactID]; + const bodyID_t ownerA = DEME_GET_PATCH_OWNER_ID(geoA, decodeTypeA(patchType)); + const bodyID_t ownerB = DEME_GET_PATCH_OWNER_ID(geoB, decodeTypeB(patchType)); + + float3 velCPA = make_float3(0.f, 0.f, 0.f); + float3 velCPB = make_float3(0.f, 0.f, 0.f); + const double3 cp_d = finalContactPoints[idx]; + if (!isfinite(cp_d.x) || !isfinite(cp_d.y) || !isfinite(cp_d.z)) { + return; + } + const float3 cp_global = make_float3((float)cp_d.x, (float)cp_d.y, (float)cp_d.z); + + if (ownerA != NULL_BODYID && ownerA < simParams->nOwnerBodies) { + float3 linVelA = make_float3(granData->vX[ownerA], granData->vY[ownerA], granData->vZ[ownerA]); + float3 angVelA_local = + make_float3(granData->omgBarX[ownerA], granData->omgBarY[ownerA], granData->omgBarZ[ownerA]); + const float4 oriA = make_float4(granData->oriQx[ownerA], granData->oriQy[ownerA], granData->oriQz[ownerA], + granData->oriQw[ownerA]); + if (isfinite(linVelA.x) && isfinite(linVelA.y) && isfinite(linVelA.z) && isfinite(angVelA_local.x) && + isfinite(angVelA_local.y) && isfinite(angVelA_local.z)) { + float3 angVelA_global = angVelA_local; + applyOriQToVector3(angVelA_global, oriA); + + double3 comA; + voxelIDToPosition( + comA.x, comA.y, comA.z, granData->voxelID[ownerA], granData->locX[ownerA], granData->locY[ownerA], + granData->locZ[ownerA], simParams->nvXp2, simParams->nvYp2, simParams->voxelSize, simParams->l); + comA.x += simParams->LBFX; + comA.y += simParams->LBFY; + comA.z += simParams->LBFZ; + + float3 rA_global = + make_float3(cp_global.x - (float)comA.x, cp_global.y - (float)comA.y, cp_global.z - (float)comA.z); + velCPA = linVelA + cross(angVelA_global, rA_global); + } + } + + if (ownerB != NULL_BODYID && ownerB < simParams->nOwnerBodies) { + float3 linVelB = make_float3(granData->vX[ownerB], granData->vY[ownerB], granData->vZ[ownerB]); + float3 angVelB_local = + make_float3(granData->omgBarX[ownerB], granData->omgBarY[ownerB], granData->omgBarZ[ownerB]); + const float4 oriB = make_float4(granData->oriQx[ownerB], granData->oriQy[ownerB], granData->oriQz[ownerB], + granData->oriQw[ownerB]); + if (isfinite(linVelB.x) && isfinite(linVelB.y) && isfinite(linVelB.z) && isfinite(angVelB_local.x) && + isfinite(angVelB_local.y) && isfinite(angVelB_local.z)) { + float3 angVelB_global = angVelB_local; + applyOriQToVector3(angVelB_global, oriB); + + double3 comB; + voxelIDToPosition( + comB.x, comB.y, comB.z, granData->voxelID[ownerB], granData->locX[ownerB], granData->locY[ownerB], + granData->locZ[ownerB], simParams->nvXp2, simParams->nvYp2, simParams->voxelSize, simParams->l); + comB.x += simParams->LBFX; + comB.y += simParams->LBFY; + comB.z += simParams->LBFZ; + + float3 rB_global = + make_float3(cp_global.x - (float)comB.x, cp_global.y - (float)comB.y, cp_global.z - (float)comB.z); + velCPB = linVelB + cross(angVelB_global, rB_global); + } + } + + const float3 relVel = velCPA - velCPB; + const float vRelN = dot(relVel, normal); + const float3 vRelT = relVel - vRelN * normal; + const float slipSpeed = length(vRelT); + if (!isfinite(slipSpeed) || !(slipSpeed >= 0.f)) { + return; + } + patchNormalForce[idx] = normalForce; + patchSlipSpeed[idx] = slipSpeed; +} + +void computePatchPVScalars(DEMSimParams* simParams, + DEMDataDT* granData, + const float3* finalNormals, + const double3* finalContactPoints, + contactPairs_t startOffsetPatch, + contactPairs_t countPatch, + float* patchNormalForce, + float* patchSlipSpeed, + cudaStream_t& this_stream) { + size_t blocks_needed = (countPatch + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + computePatchPVScalars_impl<<>>( + simParams, granData, finalNormals, finalContactPoints, startOffsetPatch, countPatch, patchNormalForce, + patchSlipSpeed); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +__global__ void accumulateTrianglePVFromPatchContacts_impl(const DEMSimParams* simParams, + DEMDataDT* granData, + const contactPairs_t* keys, + const double* primitiveWeights, + const double* patchWeights, + const float* patchNormalForce, + const float* patchSlipSpeed, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive, + const int* triGlobalToLocal, + float* triAccumP, + float* triAccumPV) { + contactPairs_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= countPrimitive || !triGlobalToLocal || !triAccumP || !triAccumPV) { + return; + } + + const contactPairs_t primContactID = startOffsetPrimitive + idx; + const contactPairs_t patchContactID = keys[idx]; + if (patchContactID < startOffsetPatch) { + return; + } + const contactPairs_t localPatchIdx = patchContactID - startOffsetPatch; + + const double patchWeight = patchWeights[localPatchIdx]; + const double primitiveWeight = primitiveWeights[idx]; + if (patchWeight <= 0.0 || primitiveWeight <= 0.0) { + return; + } + + float share = static_cast(primitiveWeight / patchWeight); + if (!(share > 0.f)) { + return; + } + share = fminf(share, 1.f); + + const float normalForce = patchNormalForce[localPatchIdx]; + if (!(normalForce > DEME_TINY_FLOAT)) { + return; + } + const float pContribution = normalForce * share; + const float slipSpeed = patchSlipSpeed[localPatchIdx]; + const float pvContribution = pContribution * slipSpeed; + + const contact_t primType = granData->contactTypePrimitive[primContactID]; + if (primType == NOT_A_CONTACT) { + return; + } + + const geoType_t typeA = decodeTypeA(primType); + const geoType_t typeB = decodeTypeB(primType); + + if (typeA == GEO_T_TRIANGLE) { + const bodyID_t triA = granData->idPrimitiveA[primContactID]; + if (triA < simParams->nTriGM) { + const int localIdx = triGlobalToLocal[triA]; + if (localIdx >= 0) { + atomicAdd(triAccumP + localIdx, pContribution); + atomicAdd(triAccumPV + localIdx, pvContribution); + } + } + } + if (typeB == GEO_T_TRIANGLE) { + const bodyID_t triB = granData->idPrimitiveB[primContactID]; + if (triB < simParams->nTriGM) { + const int localIdx = triGlobalToLocal[triB]; + if (localIdx >= 0) { + atomicAdd(triAccumP + localIdx, pContribution); + atomicAdd(triAccumPV + localIdx, pvContribution); + } + } + } +} + +void accumulateTrianglePVFromPatchContacts(DEMSimParams* simParams, + DEMDataDT* granData, + const contactPairs_t* keys, + const double* primitiveWeights, + const double* patchWeights, + const float* patchNormalForce, + const float* patchSlipSpeed, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive, + const int* triGlobalToLocal, + float* triAccumP, + float* triAccumPV, + cudaStream_t& this_stream) { + size_t blocks_needed = (countPrimitive + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + accumulateTrianglePVFromPatchContacts_impl<<>>( + simParams, granData, keys, primitiveWeights, patchWeights, patchNormalForce, patchSlipSpeed, + startOffsetPrimitive, startOffsetPatch, countPrimitive, triGlobalToLocal, triAccumP, triAccumPV); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Prep force kernels +//////////////////////////////////////////////////////////////////////////////// + +inline __device__ void cleanUpContactForces(size_t thisContact, DEMSimParams* simParams, DEMDataDT* granData) { + const float3 zeros = make_float3(0, 0, 0); + granData->contactForces[thisContact] = zeros; + granData->contactTorque_convToForce[thisContact] = zeros; +} + +inline __device__ void cleanUpAcc(size_t thisClump, DEMSimParams* simParams, DEMDataDT* granData) { + // If should not clear acc arrays, then just mark it to be clear in the next ts + if (granData->accSpecified[thisClump]) { + granData->accSpecified[thisClump] = 0; + } else { + granData->aX[thisClump] = 0; + granData->aY[thisClump] = 0; + granData->aZ[thisClump] = 0; + } + if (granData->angAccSpecified[thisClump]) { + granData->angAccSpecified[thisClump] = 0; + } else { + granData->alphaX[thisClump] = 0; + granData->alphaY[thisClump] = 0; + granData->alphaZ[thisClump] = 0; + } +} + +__global__ void prepareAccArrays_impl(DEMSimParams* simParams, DEMDataDT* granData) { + size_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < simParams->nOwnerBodies) { + cleanUpAcc(myID, simParams, granData); + } +} + +__global__ void prepareForceArrays_impl(DEMSimParams* simParams, DEMDataDT* granData, size_t nContactPairs) { + size_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nContactPairs) { + cleanUpContactForces(myID, simParams, granData); + } +} + +void prepareForceArrays(DEMSimParams* simParams, + DEMDataDT* granData, + size_t nPrimitiveContactPairs, + cudaStream_t& this_stream) { + size_t blocks_needed_for_force_prep = + (nPrimitiveContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_force_prep > 0) { + prepareForceArrays_impl<<>>( + simParams, granData, nPrimitiveContactPairs); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +void prepareAccArrays(DEMSimParams* simParams, DEMDataDT* granData, bodyID_t nOwnerBodies, cudaStream_t& this_stream) { + size_t blocks_needed_for_acc_prep = (nOwnerBodies + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed_for_acc_prep > 0) { + prepareAccArrays_impl<<>>(simParams, + granData); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +__global__ void rearrangeContactWildcards_impl(DEMDataDT* granData, + float* newWildcards, + notStupidBool_t* sentry, + unsigned int nWildcards, + size_t nContactPairs) { + size_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nContactPairs) { + contactPairs_t map_from = granData->contactMapping[myID]; + if (map_from == NULL_MAPPING_PARTNER) { + // If it is a NULL ID then kT says this contact is new. Initialize all wildcard arrays. + for (size_t i = 0; i < nWildcards; i++) { + newWildcards[nContactPairs * i + myID] = 0; + } + } else { + // Not a new contact, need to map it from somewhere in the old history array + for (size_t i = 0; i < nWildcards; i++) { + newWildcards[nContactPairs * i + myID] = granData->contactWildcards[i][map_from]; + } + // This sentry trys to make sure that all `alive' contacts got mapped to some place + sentry[map_from] = 0; + } + } +} + +void rearrangeContactWildcards(DEMDataDT* granData, + float* wildcard, + notStupidBool_t* sentry, + unsigned int nWildcards, + size_t nContactPairs, + cudaStream_t& this_stream) { + size_t blocks_needed = (nContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + rearrangeContactWildcards_impl<<>>( + granData, wildcard, sentry, nWildcards, nContactPairs); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +__global__ void markAliveContacts_impl(float* wildcard, notStupidBool_t* sentry, size_t nContactPairs) { + size_t myID = blockIdx.x * blockDim.x + threadIdx.x; + if (myID < nContactPairs) { + float myEntry = abs(wildcard[myID]); + // If this is alive then mark it + if (myEntry > DEME_TINY_FLOAT) { + sentry[myID] = 1; + } else { + sentry[myID] = 0; + } + } +} + +void markAliveContacts(float* wildcard, notStupidBool_t* sentry, size_t nContactPairs, cudaStream_t& this_stream) { + size_t blocks_needed = (nContactPairs + DEME_MAX_THREADS_PER_BLOCK - 1) / DEME_MAX_THREADS_PER_BLOCK; + if (blocks_needed > 0) { + markAliveContacts_impl<<>>(wildcard, sentry, + nContactPairs); + DEME_GPU_DEBUG_SYNC(this_stream); + } } } // namespace deme diff --git a/src/algorithms/DEMMiscKernels.cu b/src/algorithms/DEMMiscKernels.cu new file mode 100644 index 00000000..a2103d75 --- /dev/null +++ b/src/algorithms/DEMMiscKernels.cu @@ -0,0 +1,68 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "algorithms/DEMStaticDeviceSubroutines.h" +#include "algorithms/DEMStaticDeviceUtilities.cuh" + +#include "kernel/DEMHelperKernels.cuh" + +namespace deme { + +//////////////////////////////////////////////////////////////////////////////// +// Misc kernels implementations +//////////////////////////////////////////////////////////////////////////////// + +// granData is a device pointer: select its primitive arrays inside the kernel, never in the host launch wrapper. +template +__global__ void fillMarginValues_impl(DEMSimParams* simParams, DEMDataKT* granData, size_t n) { + size_t ID = blockIdx.x * blockDim.x + threadIdx.x; + if (ID < n) { + float* marginSizeArr; + bodyID_t* ownerIDArr; + if constexpr (OwnerType == OWNER_T_CLUMP) { + marginSizeArr = granData->marginSizeSphere; + ownerIDArr = granData->ownerClumpBody; + } else if constexpr (OwnerType == OWNER_T_MESH) { + marginSizeArr = granData->marginSizeTriangle; + ownerIDArr = granData->ownerTriMesh; + } else { + marginSizeArr = granData->marginSizeAnalytical; + ownerIDArr = granData->ownerAnalBody; + } + bodyID_t ownerID = ownerIDArr[ID]; + unsigned int my_family = granData->familyID[ownerID]; + marginSizeArr[ID] = simParams->dyn.beta + granData->familyExtraMarginSize[my_family]; + } +} + +// Fill each primitive kind with its fixed margin and family allowance using the device-resident metadata. +void fillMarginValues(DEMSimParams* simParams, + DEMDataKT* granData, + size_t nSphere, + size_t nTri, + size_t nAnal, + cudaStream_t& this_stream) { + constexpr unsigned int FIXED_MARGIN_BLOCK = 256; + size_t blocks_needed = (nSphere + FIXED_MARGIN_BLOCK - 1) / FIXED_MARGIN_BLOCK; + if (blocks_needed > 0) { + fillMarginValues_impl + <<>>(simParams, granData, nSphere); + DEME_GPU_DEBUG_SYNC(this_stream); + } + blocks_needed = (nTri + FIXED_MARGIN_BLOCK - 1) / FIXED_MARGIN_BLOCK; + if (blocks_needed > 0) { + fillMarginValues_impl + <<>>(simParams, granData, nTri); + DEME_GPU_DEBUG_SYNC(this_stream); + } + blocks_needed = (nAnal + FIXED_MARGIN_BLOCK - 1) / FIXED_MARGIN_BLOCK; + if (blocks_needed > 0) { + fillMarginValues_impl + <<>>(simParams, granData, nAnal); + DEME_GPU_DEBUG_SYNC(this_stream); + } +} + +} // namespace deme diff --git a/src/algorithms/DEMOwnerDataRetrieval.cu b/src/algorithms/DEMOwnerDataRetrieval.cu new file mode 100644 index 00000000..12320560 --- /dev/null +++ b/src/algorithms/DEMOwnerDataRetrieval.cu @@ -0,0 +1,325 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "DEMOwnerDataRetrieval.h" + +#include "DEM/Defines.h" +#include "kernel/DEMHelperKernels.cuh" + +namespace deme { + +namespace { + +constexpr unsigned int OWNER_DATA_RETRIEVAL_BLOCK_SIZE = 256; + +__global__ void ValidateOwnerOrientationsKernel(const float4* input, size_t count, unsigned int* invalid) { + const size_t index = blockIdx.x * blockDim.x + threadIdx.x; + if (index >= count) { + return; + } + const float4 q = input[index]; + const double norm_squared = static_cast(q.x) * q.x + static_cast(q.y) * q.y + + static_cast(q.z) * q.z + static_cast(q.w) * q.w; + if (!isfinite(norm_squared) || norm_squared == 0.0) { + atomicExch(invalid, 1u); + } +} + +__global__ void PackOwnerDataKernel(void* output, + OwnerDataField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + const DEMDataDT* data, + bool mass_properties_are_jitified, + unsigned int wildcard_index) { + const size_t output_index = blockIdx.x * blockDim.x + threadIdx.x; + if (output_index >= count) { + return; + } + const bodyID_t owner = owner_begin + output_index; + + const float4 orientation = + make_float4(data->oriQx[owner], data->oriQy[owner], data->oriQz[owner], data->oriQw[owner]); + switch (field) { + case OwnerDataField::POSITION: { + double x, y, z; + voxelIDToPosition( + x, y, z, data->voxelID[owner], data->locX[owner], data->locY[owner], data->locZ[owner], + sim_params->nvXp2, sim_params->nvYp2, sim_params->voxelSize, sim_params->l); + static_cast(output)[output_index] = + make_float3(x + sim_params->LBFX, y + sim_params->LBFY, z + sim_params->LBFZ); + break; + } + case OwnerDataField::VELOCITY: + static_cast(output)[output_index] = make_float3(data->vX[owner], data->vY[owner], data->vZ[owner]); + break; + case OwnerDataField::ANGULAR_VELOCITY_LOCAL: + static_cast(output)[output_index] = + make_float3(data->omgBarX[owner], data->omgBarY[owner], data->omgBarZ[owner]); + break; + case OwnerDataField::ANGULAR_VELOCITY_GLOBAL: { + float3 value = make_float3(data->omgBarX[owner], data->omgBarY[owner], data->omgBarZ[owner]); + applyOriQToVector3(value, orientation); + static_cast(output)[output_index] = value; + break; + } + case OwnerDataField::ORIENTATION: + static_cast(output)[output_index] = orientation; + break; + case OwnerDataField::CONTACT_ACCELERATION: + static_cast(output)[output_index] = make_float3(data->aX[owner], data->aY[owner], data->aZ[owner]); + break; + case OwnerDataField::CONTACT_ANGULAR_ACCELERATION_LOCAL: + static_cast(output)[output_index] = + make_float3(data->alphaX[owner], data->alphaY[owner], data->alphaZ[owner]); + break; + case OwnerDataField::CONTACT_ANGULAR_ACCELERATION_GLOBAL: { + float3 value = make_float3(data->alphaX[owner], data->alphaY[owner], data->alphaZ[owner]); + applyOriQToVector3(value, orientation); + static_cast(output)[output_index] = value; + break; + } + case OwnerDataField::FAMILY: + static_cast(output)[output_index] = static_cast(+(data->familyID[owner])); + break; + case OwnerDataField::MASS: { + const inertiaOffset_t offset = mass_properties_are_jitified ? data->inertiaPropOffsets[owner] : owner; + static_cast(output)[output_index] = data->massOwnerBody[offset]; + break; + } + case OwnerDataField::MOI: { + const inertiaOffset_t offset = mass_properties_are_jitified ? data->inertiaPropOffsets[owner] : owner; + static_cast(output)[output_index] = + make_float3(data->mmiXX[offset], data->mmiYY[offset], data->mmiZZ[offset]); + break; + } + case OwnerDataField::WILDCARD: + static_cast(output)[output_index] = data->ownerWildcards[wildcard_index][owner]; + break; + } +} + +__global__ void UnpackOwnerStateKernel(const void* input, + OwnerStateField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + DEMDataDT* data) { + const size_t input_index = blockIdx.x * blockDim.x + threadIdx.x; + if (input_index >= count) { + return; + } + const bodyID_t owner = owner_begin + input_index; + + switch (field) { + case OwnerStateField::POSITION: { + const float3 position = static_cast(input)[input_index]; + const double x = static_cast(position.x) - sim_params->LBFX; + const double y = static_cast(position.y) - sim_params->LBFY; + const double z = static_cast(position.z) - sim_params->LBFZ; + positionToVoxelID( + data->voxelID[owner], data->locX[owner], data->locY[owner], data->locZ[owner], x, y, z, + sim_params->nvXp2, sim_params->nvYp2, sim_params->voxelSize, sim_params->l); + break; + } + case OwnerStateField::VELOCITY: { + const float3 velocity = static_cast(input)[input_index]; + data->vX[owner] = velocity.x; + data->vY[owner] = velocity.y; + data->vZ[owner] = velocity.z; + break; + } + case OwnerStateField::ANGULAR_VELOCITY_LOCAL: { + const float3 angular_velocity = static_cast(input)[input_index]; + data->omgBarX[owner] = angular_velocity.x; + data->omgBarY[owner] = angular_velocity.y; + data->omgBarZ[owner] = angular_velocity.z; + break; + } + case OwnerStateField::ANGULAR_VELOCITY_GLOBAL: { + float3 angular_velocity = static_cast(input)[input_index]; + const float4 inverse_orientation = + make_float4(-data->oriQx[owner], -data->oriQy[owner], -data->oriQz[owner], data->oriQw[owner]); + applyOriQToVector3(angular_velocity, inverse_orientation); + data->omgBarX[owner] = angular_velocity.x; + data->omgBarY[owner] = angular_velocity.y; + data->omgBarZ[owner] = angular_velocity.z; + break; + } + case OwnerStateField::ORIENTATION: { + const float4 orientation = static_cast(input)[input_index]; + const double norm_squared = static_cast(orientation.x) * orientation.x + + static_cast(orientation.y) * orientation.y + + static_cast(orientation.z) * orientation.z + + static_cast(orientation.w) * orientation.w; + const float inverse_norm = static_cast(1.0 / sqrt(norm_squared)); + data->oriQx[owner] = orientation.x * inverse_norm; + data->oriQy[owner] = orientation.y * inverse_norm; + data->oriQz[owner] = orientation.z * inverse_norm; + data->oriQw[owner] = orientation.w * inverse_norm; + break; + } + case OwnerStateField::NEXT_STEP_ACCELERATION: { + const float3 acceleration = static_cast(input)[input_index]; + data->aX[owner] = acceleration.x; + data->aY[owner] = acceleration.y; + data->aZ[owner] = acceleration.z; + data->accSpecified[owner] = 1; + break; + } + case OwnerStateField::NEXT_STEP_ANGULAR_ACCELERATION_LOCAL: { + const float3 angular_acceleration = static_cast(input)[input_index]; + data->alphaX[owner] = angular_acceleration.x; + data->alphaY[owner] = angular_acceleration.y; + data->alphaZ[owner] = angular_acceleration.z; + data->angAccSpecified[owner] = 1; + break; + } + } +} + +__device__ void AccumulateOwnerContactWrench(float3* forces, + float3* torques, + bodyID_t owner_begin, + size_t count, + const DEMDataDT* data, + bodyID_t owner, + const float3& force_a, + const float3& extra_torque_force_a, + float sign, + const float3& local_contact_point) { + if (owner < owner_begin || static_cast(owner - owner_begin) >= count) { + return; + } + const size_t output = static_cast(owner - owner_begin); + const float3 signed_force = force_a * sign; + float3 total_force_form = (force_a + extra_torque_force_a) * sign; + const float4 orientation = + make_float4(data->oriQx[owner], data->oriQy[owner], data->oriQz[owner], data->oriQw[owner]); + applyOriQToVector3(total_force_form, make_float4(-orientation.x, -orientation.y, -orientation.z, orientation.w)); + float3 global_torque = cross(local_contact_point, total_force_form); + applyOriQToVector3(global_torque, orientation); + + atomicAdd(&forces[output].x, signed_force.x); + atomicAdd(&forces[output].y, signed_force.y); + atomicAdd(&forces[output].z, signed_force.z); + atomicAdd(&torques[output].x, global_torque.x); + atomicAdd(&torques[output].y, global_torque.y); + atomicAdd(&torques[output].z, global_torque.z); +} + +__global__ void ReduceOwnerContactWrenchesKernel(float3* forces, + float3* torques, + bodyID_t owner_begin, + size_t count, + const DEMDataDT* granData, + size_t num_contacts) { + const size_t contact = blockIdx.x * blockDim.x + threadIdx.x; + if (contact >= num_contacts || !granData->contactTypePatch || !granData->idPatchA || !granData->idPatchB) { + return; + } + const contact_t contact_type = granData->contactTypePatch[contact]; + if (contact_type == NOT_A_CONTACT) { + return; + } + + const bodyID_t owner_a = DEME_GET_PATCH_OWNER_ID(granData->idPatchA[contact], decodeTypeA(contact_type)); + const bodyID_t owner_b = DEME_GET_PATCH_OWNER_ID(granData->idPatchB[contact], decodeTypeB(contact_type)); + const float3 force_a = granData->contactForces[contact]; + const float3 extra_torque_force_a = granData->contactTorque_convToForce[contact]; + if (length(force_a) + length(extra_torque_force_a) < DEME_TINY_FLOAT) { + return; + } + + // contactTorque_convToForce is DEME's force-like representation of force-model-only torque. Using the same + // contact arm as forceToAcc yields r x force plus rolling-resistance (or other model-provided) torque. + AccumulateOwnerContactWrench(forces, torques, owner_begin, count, granData, owner_a, force_a, extra_torque_force_a, + 1.f, granData->contactPointGeometryA[contact]); + AccumulateOwnerContactWrench(forces, torques, owner_begin, count, granData, owner_b, force_a, extra_torque_force_a, + -1.f, granData->contactPointGeometryB[contact]); +} + +} // namespace + +void PackOwnerData(void* output, + OwnerDataField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + const DEMDataDT* data, + bool mass_properties_are_jitified, + unsigned int wildcard_index, + cudaStream_t stream) { + if (count == 0) { + return; + } + const size_t blocks = (count + OWNER_DATA_RETRIEVAL_BLOCK_SIZE - 1) / OWNER_DATA_RETRIEVAL_BLOCK_SIZE; + PackOwnerDataKernel<<>>( + output, field, owner_begin, count, sim_params, data, mass_properties_are_jitified, wildcard_index); + DEME_GPU_DEBUG_SYNC(stream); +} + +size_t OwnerDataElementSize(OwnerDataField field) { + switch (field) { + case OwnerDataField::ORIENTATION: + return sizeof(float4); + case OwnerDataField::FAMILY: + return sizeof(unsigned int); + case OwnerDataField::MASS: + case OwnerDataField::WILDCARD: + return sizeof(float); + default: + return sizeof(float3); + } +} + +void ValidateOwnerOrientations(const float4* input, size_t count, unsigned int* invalid, cudaStream_t stream) { + if (count == 0) { + return; + } + const size_t blocks = (count + OWNER_DATA_RETRIEVAL_BLOCK_SIZE - 1) / OWNER_DATA_RETRIEVAL_BLOCK_SIZE; + ValidateOwnerOrientationsKernel<<>>(input, count, invalid); + DEME_GPU_DEBUG_SYNC(stream); +} + +void UnpackOwnerState(const void* input, + OwnerStateField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + DEMDataDT* data, + cudaStream_t stream) { + if (count == 0) { + return; + } + const size_t blocks = (count + OWNER_DATA_RETRIEVAL_BLOCK_SIZE - 1) / OWNER_DATA_RETRIEVAL_BLOCK_SIZE; + UnpackOwnerStateKernel<<>>(input, field, owner_begin, count, + sim_params, data); + DEME_GPU_DEBUG_SYNC(stream); +} + +void ReduceOwnerContactWrenches(float3* forces, + float3* torques, + bodyID_t owner_begin, + size_t count, + const DEMDataDT* data, + size_t num_contacts, + cudaStream_t stream) { + if (count == 0) { + return; + } + DEME_GPU_CALL(cudaMemsetAsync(forces, 0, count * sizeof(float3), stream)); + DEME_GPU_CALL(cudaMemsetAsync(torques, 0, count * sizeof(float3), stream)); + if (num_contacts == 0) { + return; + } + const size_t blocks = (num_contacts + OWNER_DATA_RETRIEVAL_BLOCK_SIZE - 1) / OWNER_DATA_RETRIEVAL_BLOCK_SIZE; + ReduceOwnerContactWrenchesKernel<<>>( + forces, torques, owner_begin, count, data, num_contacts); + DEME_GPU_DEBUG_SYNC(stream); +} + +} // namespace deme diff --git a/src/algorithms/DEMOwnerDataRetrieval.h b/src/algorithms/DEMOwnerDataRetrieval.h new file mode 100644 index 00000000..b7a8c712 --- /dev/null +++ b/src/algorithms/DEMOwnerDataRetrieval.h @@ -0,0 +1,81 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_OWNER_DATA_RETRIEVAL_H +#define DEME_OWNER_DATA_RETRIEVAL_H + +#include + +#include + +#include "DEM/Structs.h" + +namespace deme { + +/// Fixed-size owner properties supported by the GPU retrieval path. +enum class OwnerDataField { + POSITION, + VELOCITY, + ANGULAR_VELOCITY_LOCAL, + ANGULAR_VELOCITY_GLOBAL, + ORIENTATION, + CONTACT_ACCELERATION, + CONTACT_ANGULAR_ACCELERATION_LOCAL, + CONTACT_ANGULAR_ACCELERATION_GLOBAL, + FAMILY, + MASS, + MOI, + WILDCARD +}; + +/// Owner state fields supported by the GPU input path. +enum class OwnerStateField { + POSITION, + VELOCITY, + ANGULAR_VELOCITY_LOCAL, + ANGULAR_VELOCITY_GLOBAL, + ORIENTATION, + NEXT_STEP_ACCELERATION, + NEXT_STEP_ANGULAR_ACCELERATION_LOCAL +}; + +/// Pack one consecutive owner range from DEME's internal SoA representation into the public AoS/scalar representation. +void PackOwnerData(void* output, + OwnerDataField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + const DEMDataDT* data, + bool mass_properties_are_jitified, + unsigned int wildcard_index, + cudaStream_t stream); + +/// Return the size of one output element for a retrieval field. +size_t OwnerDataElementSize(OwnerDataField field); + +/// Check that every public-order quaternion is finite and has nonzero length before an owner-state update. +void ValidateOwnerOrientations(const float4* input, size_t count, unsigned int* invalid, cudaStream_t stream); + +/// Unpack one public AoS owner-state range directly into DEME's internal SoA representation. +void UnpackOwnerState(const void* input, + OwnerStateField field, + bodyID_t owner_begin, + size_t count, + const DEMSimParams* sim_params, + DEMDataDT* data, + cudaStream_t stream); + +/// Reduce the current patch-contact records into one global-frame resultant wrench per consecutive owner. +void ReduceOwnerContactWrenches(float3* forces, + float3* torques, + bodyID_t owner_begin, + size_t count, + const DEMDataDT* data, + size_t num_contacts, + cudaStream_t stream); + +} // namespace deme + +#endif diff --git a/src/algorithms/DEMStaticDeviceSubroutines.h b/src/algorithms/DEMStaticDeviceSubroutines.h index d2de6ef7..bd427d2f 100644 --- a/src/algorithms/DEMStaticDeviceSubroutines.h +++ b/src/algorithms/DEMStaticDeviceSubroutines.h @@ -51,6 +51,16 @@ void cubMaxReduceByKey(T1* d_keys_in, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); +template +void cubMaxNegativeReduceByKey(T1* d_keys_in, + T1* d_unique_out, + T2* d_vals_in, + T2* d_aggregates_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + template void cubMinReduce(T1* d_in, T1* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); template @@ -72,34 +82,63 @@ void cubSortByKey(T1* d_keys_in, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); +template +void cubRunLengthEncode(T1* d_in, + T1* d_unique_out, + T2* d_counts_out, + size_t* d_num_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + +template +void cubPrefixScan(T1* d_in, T2* d_out, size_t n, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad); +template +inline void cubDEMInclusiveScan(T1* d_in, + T2* d_out, + size_t n, + cudaStream_t& this_stream, + DEMSolverScratchData& scratchPad); + //////////////////////////////////////////////////////////////////////////////// // For kT and dT's private usage //////////////////////////////////////////////////////////////////////////////// -void contactDetection(std::shared_ptr& bin_sphere_kernels, - std::shared_ptr& bin_triangle_kernels, - std::shared_ptr& sphere_contact_kernels, - std::shared_ptr& sphTri_contact_kernels, - std::shared_ptr& history_kernels, +void contactDetection(std::shared_ptr& bin_sphere_kernels, + std::shared_ptr& bin_triangle_kernels, + std::shared_ptr& sphere_contact_kernels, + std::shared_ptr& sphTri_contact_kernels, DualStruct& granData, DualStruct& simParams, SolverFlags& solverFlags, - VERBOSITY& verbosity, + verbosity_t& verbosity, // The following arrays may need to change sizes, so we can't pass pointers - DualArray& idGeometryA, - DualArray& idGeometryB, - DualArray& contactType, - DualArray& previous_idGeometryA, - DualArray& previous_idGeometryB, - DualArray& previous_contactType, + DualArray& idPrimitiveA, + DualArray& idPrimitiveB, + DualArray& contactTypePrimitive, + DualArray& previous_idPrimitiveA, + DualArray& previous_idPrimitiveB, + DualArray& previous_contactTypePrimitive, + DualArray& previous_primitivePatchIsland, DualArray& contactPersistency, DualArray& contactMapping, + // NEW: Separate patch ID arrays and mapping + DualArray& idPatchA, + DualArray& idPatchB, + DualArray& previous_idPatchA, + DualArray& previous_idPatchB, + DualArray& contactTypePatch, + DualArray& previous_contactTypePatch, + DualArray& contactPatchIsland, + DualArray& previous_contactPatchIsland, + ContactTypeMap>& typeStartCountPatchMap, + DualArray& geomToPatchMap, cudaStream_t& this_stream, DEMSolverScratchData& scratchPad, SolverTimers& timers, kTStateParams& stateParams); -void collectContactForcesThruCub(std::shared_ptr& collect_force_kernels, +void collectContactForcesThruCub(std::shared_ptr& collect_force_kernels, DualStruct& granData, const size_t nContactPairs, const size_t nClumps, @@ -110,11 +149,12 @@ void collectContactForcesThruCub(std::shared_ptr& collect_fo void overwritePrevContactArrays(DualStruct& kT_data, DualStruct& dT_data, - DualArray& previous_idGeometryA, - DualArray& previous_idGeometryB, - DualArray& previous_contactType, + DualArray& previous_idPatchA, + DualArray& previous_idPatchB, + DualArray& previous_contactTypePatch, + DualArray& previous_contactPatchIsland, + ContactTypeMap>& typeStartCountPatchMap, DualStruct& simParams, - DualArray& contactPersistency, DEMSolverScratchData& scratchPad, cudaStream_t& this_stream, size_t nContacts); @@ -132,6 +172,138 @@ void getContactForcesConcerningOwners(float3* d_points, bool torque_in_local, cudaStream_t& this_stream); +//////////////////////////////////////////////////////////////////////////////// +// Patch-based voting wrappers for mesh contact correction +//////////////////////////////////////////////////////////////////////////////// + +// Prepares weighted normals (normal * area) for voting. +// Keys are sourced directly from granData->geomToPatchMap + startOffsetPrimitive by the caller, +// avoiding a temporary key buffer and a separate kernel write for keys/areas. +void prepareWeightedNormalsForVoting(DEMDataDT* granData, + float3* weightedNormals, + contactPairs_t startOffset, + contactPairs_t count, + cudaStream_t& this_stream); + +// Normalizes voted normals by total area and scatters to output +// If total area is 0, output is (0,0,0) indicating no contact +void normalizeAndScatterVotedNormals(float3* votedWeightedNormals, + float3* output, + contactPairs_t count, + cudaStream_t& this_stream); + +// Computes four per-primitive weighted quantities in a single fused kernel pass: +// projectedAreas[i] = area_i * dot(normal_i, votedNormal) (clamped to >= 0) +// projectedPens[i] = pen_i * dot(normal_i, votedNormal) (clamped to >= 0) +// weights[i] = projectedArea_i * projectedPen_i (= projArea * projPen) +// weightedCPs[i] = contactPoint_i * weights[i] +// projectedAreas/weights/weightedCPs are sum-reduced per patch; projectedPens are max-reduced per patch. +void computePerPrimitiveWeightedQuantities(DEMDataDT* granData, + const float3* votedNormals, + const contactPairs_t* keys, + double* projectedAreas, + double* projectedPens, + double* weights, + double3* weightedCPs, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t count, + cudaStream_t& this_stream); + +// Extracts primitive penetrations from contactPointGeometryA for max-reduce operation +void extractPrimitivePenetrations(DEMDataDT* granData, + double* penetrations, + contactPairs_t startOffset, + contactPairs_t count, + cudaStream_t& this_stream); + +// Finds the primitive with max penetration for zero-area patches and extracts its normal, penetration, and contact +// point +void findMaxPenetrationPrimitiveForZeroAreaPatches(DEMDataDT* granData, + double* maxPenetrations, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + contactPairs_t* keys, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive, + cudaStream_t& this_stream); + +// Finalizes patch results: finalPen = max projected penetration (from cubMaxReduceByKey); +// finalCP = weight-averaged contact point (weight = projArea * projPen). +// Zero-area patches fall back to the max-penetration primitive's values. +void finalizePatchResults(double* totalProjectedAreas, + double* maxProjPens, + double* totalWeights, + float3* votedNormals, + double3* totalWeightedCPs, + float3* zeroAreaNormals, + double* zeroAreaPenetrations, + double3* zeroAreaContactPoints, + double* finalAreas, + float3* finalNormals, + double* finalPenetrations, + double3* finalContactPoints, + contactPairs_t count, + cudaStream_t& this_stream); + +// Compute per-patch normal-force magnitude and tangential slip speed used by per-triangle diagnostics. +void computePatchPVScalars(DEMSimParams* simParams, + DEMDataDT* granData, + const float3* finalNormals, + const double3* finalContactPoints, + contactPairs_t startOffsetPatch, + contactPairs_t countPatch, + float* patchNormalForce, + float* patchSlipSpeed, + cudaStream_t& this_stream); + +// Redistribute patch-level normal force to participating triangles using primitive contact weight share, and +// accumulate per-triangle P and P*V contributions over the current output window. +void accumulateTrianglePVFromPatchContacts(DEMSimParams* simParams, + DEMDataDT* granData, + const contactPairs_t* keys, + const double* primitiveWeights, + const double* patchWeights, + const float* patchNormalForce, + const float* patchSlipSpeed, + contactPairs_t startOffsetPrimitive, + contactPairs_t startOffsetPatch, + contactPairs_t countPrimitive, + const int* triGlobalToLocal, + float* triAccumP, + float* triAccumPV, + cudaStream_t& this_stream); + +//////////////////////////////////////////////////////////////////////////////// +// Prep force kernels declaration +//////////////////////////////////////////////////////////////////////////////// + +void prepareForceArrays(DEMSimParams* simParams, + DEMDataDT* granData, + size_t nPrimitiveContactPairs, + cudaStream_t& this_stream); +void prepareAccArrays(DEMSimParams* simParams, DEMDataDT* granData, bodyID_t nOwnerBodies, cudaStream_t& this_stream); +void rearrangeContactWildcards(DEMDataDT* granData, + float* wildcard, + notStupidBool_t* sentry, + unsigned int nWildcards, + size_t nContactPairs, + cudaStream_t& this_stream); +void markAliveContacts(float* wildcard, notStupidBool_t* sentry, size_t nContactPairs, cudaStream_t& this_stream); + +//////////////////////////////////////////////////////////////////////////////// +// Misc kernels declarations +//////////////////////////////////////////////////////////////////////////////// + +void fillMarginValues(DEMSimParams* simParams, + DEMDataKT* granData, + size_t nSphere, + size_t nTri, + size_t nAnal, + cudaStream_t& this_stream); + } // namespace deme #endif diff --git a/src/algorithms/DEMStaticDeviceUtilities.cuh b/src/algorithms/DEMStaticDeviceUtilities.cuh index f55e2488..e508b0d7 100644 --- a/src/algorithms/DEMStaticDeviceUtilities.cuh +++ b/src/algorithms/DEMStaticDeviceUtilities.cuh @@ -24,7 +24,7 @@ __global__ void oneNumAdd(T1* res, const T2* a, const T3* b) { template void deviceAdd(T1* res, const T2* a, const T3* b, cudaStream_t& this_stream) { oneNumAdd<<<1, 1, 0, this_stream>>>(res, a, b); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } template @@ -35,7 +35,7 @@ __global__ void oneNumAssign(T1* res, const T2* a) { template void deviceAssign(T1* res, const T2* a, cudaStream_t& this_stream) { oneNumAssign<<<1, 1, 0, this_stream>>>(res, a); - DEME_GPU_CALL(cudaStreamSynchronize(this_stream)); + DEME_GPU_DEBUG_SYNC(this_stream); } } // namespace deme diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7df0a1f3..4bd27c7e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -13,24 +13,35 @@ configure_file( @ONLY ) - -# Jitify is only needed for CUDA builds (HIP uses hiprtc directly via JitKernel abstraction) -if(NOT USE_HIP) - set(DEMEJitifyHeader "${ProjectIncludeSource}/jitify/jitify.hpp") - if(NOT EXISTS "${DEMEJitifyHeader}") - set(DEMEJitifyHeader "${NVIDIAJitifyPath}/jitify.hpp") - endif() - - message(STATUS "${core_message} Extracting Jitify header...") - configure_file( - ${DEMEJitifyHeader} - ${CMAKE_BINARY_DIR}/src/jitify/jitify.hpp - COPYONLY - ) +# Prefer DEME's Jitify variant, which adds the optional persistent CUDA-header cache. Keep the submodule path as a +# fallback so source layouts that intentionally omit the vendored variant still configure with upstream Jitify. +set(DEMEJitifyHeader "${ProjectIncludeSource}/jitify/jitify.hpp") +if(NOT EXISTS "${DEMEJitifyHeader}") + set(DEMEJitifyHeader "${NVIDIAJitifyPath}/jitify.hpp") endif() +message(STATUS "${core_message} Extracting Jitify header...") +configure_file( + ${DEMEJitifyHeader} + ${CMAKE_BINARY_DIR}/src/jitify/jitify.hpp + COPYONLY +) + add_library(core OBJECT) +# Capture CUDA include dirs from whichever variable is populated +set(_DEME_CUDA_INC_DIRS "${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}") +if(NOT _DEME_CUDA_INC_DIRS AND DEFINED CUDAToolkit_INCLUDE_DIRS) + set(_DEME_CUDA_INC_DIRS "${CUDAToolkit_INCLUDE_DIRS}") +endif() +if(NOT _DEME_CUDA_INC_DIRS AND DEFINED CUDA_INCLUDE_DIRS) + set(_DEME_CUDA_INC_DIRS "${CUDA_INCLUDE_DIRS}") +endif() + +target_compile_definitions(core PRIVATE + DEME_CUDA_TOOLKIT_INCLUDE_DIRS="$" +) + target_include_directories( core @@ -39,30 +50,26 @@ target_include_directories( ) -# Core is pure CXX, no GPU library dependencies. -# CUB/hipcub is only needed by algorithms target. -if(USE_CHPF) - target_link_libraries( - core - INTERFACE ${ChPF_IMPORTED_NAME} - ) -endif() +target_link_libraries( + core + PUBLIC CUB::CUB +) set(core_headers ${CMAKE_BINARY_DIR}/src/core/ApiVersion.h + ${CMAKE_CURRENT_SOURCE_DIR}/utils/BaseClasses.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/CudaAllocator.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/ManagedMemory.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/CudaDebugSync.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitHelper.h - ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitKernel.h - ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitKernel.inl ${CMAKE_CURRENT_SOURCE_DIR}/utils/ThreadManager.h - ${CMAKE_CURRENT_SOURCE_DIR}/utils/GpuError.h + ${CMAKE_CURRENT_SOURCE_DIR}/utils/Logger.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/GpuManager.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/WavefrontMeshLoader.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/csv.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/Timer.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/DataMigrationHelper.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/DeviceDataTransfer.hpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/DEMEPaths.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/RuntimeData.h ) @@ -70,17 +77,11 @@ set(core_headers set(core_sources ${CMAKE_CURRENT_SOURCE_DIR}/DebugInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/GpuManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utils/DeviceDataTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitHelper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/DEMEPaths.cpp ) -# JitKernel abstraction: select backend based on USE_HIP -if(USE_HIP) - list(APPEND core_sources ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitKernel_hip.cpp) -else() - list(APPEND core_sources ${CMAKE_CURRENT_SOURCE_DIR}/utils/JitKernel_cuda.cpp) -endif() - target_sources( core PUBLIC ${core_headers} ${core_utils_headers} @@ -94,6 +95,29 @@ set_target_properties( PUBLIC_HEADER "${core_headers}" ) + +# The Python extension needs import-time CUDA header configuration. Compile its core separately so +# this hook and its state are absent from native C++ libraries, even when both targets are built. +if(DEME_BUILD_PYTHON) + add_library(core_python OBJECT) + target_sources(core_python PRIVATE ${core_sources}) + target_include_directories(core_python PUBLIC + $ + $ + ) + target_link_libraries(core_python PUBLIC CUB::CUB) + target_compile_definitions(core_python PRIVATE + DEME_PYTHON_JIT + DEME_CUDA_TOOLKIT_INCLUDE_DIRS="$" + ) + set_target_properties(core_python PROPERTIES + POSITION_INDEPENDENT_CODE True + CXX_STANDARD ${CXXSTD_SUPPORTED} + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN True + ) +endif() + # Install Core Headers install( DIRECTORY ${ProjectIncludeSource}/core @@ -111,16 +135,13 @@ install( ${CMAKE_INSTALL_INCLUDEDIR}/core ) -# Install Third-party Headers (jitify only for CUDA builds) -if(NOT USE_HIP) - install( - FILES - "${DEMEJitifyHeader}" - DESTINATION - ${CMAKE_INSTALL_INCLUDEDIR}/jitify - ) -endif() - +# Install Third-party Headers +install( + FILES + "${DEMEJitifyHeader}" + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/jitify +) # --------------------------------------------------------- # # Runtime data path helper library @@ -129,7 +150,7 @@ endif() message(STATUS "${core_message} Generating Runtime Data helper (build)...") set(DEME_RUNTIME_DATA_DIRECTORY "${CMAKE_BINARY_DIR}") -set(DEME_RUNTIME_INCLUDE_DIRECTORY "${CMAKE_SOURCE_DIR}/src") +set(DEME_RUNTIME_INCLUDE_DIRECTORY "${CMAKE_BINARY_DIR}") configure_file( utils/RuntimeData.cpp.in utils/RuntimeDataBuild.cpp @ONLY @@ -217,17 +238,27 @@ set_target_properties( # Make sure this target is generated **AFTER** the build-tree version add_dependencies(DEMERuntimeDataHelper_install DEMERuntimeDataHelper) +# Python version (for Python bindings) if(DEME_BUILD_PYTHON) add_library(DEMERuntimeDataHelper_python STATIC) + # The Python module calls SetPathPrefix during import, but retain an install-layout fallback if that discovery fails. set(DEME_RUNTIME_DATA_DIRECTORY "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/DEME") set(DEME_RUNTIME_INCLUDE_DIRECTORY "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}") - configure_file(utils/RuntimeData.cpp.in utils/RuntimeDataPython.cpp @ONLY) + configure_file(utils/RuntimeData.cpp.in utils/RuntimeDataPython.cpp) + target_sources(DEMERuntimeDataHelper_python + PUBLIC $ $ PRIVATE "${CMAKE_BINARY_DIR}/src/core/utils/RuntimeDataPython.cpp" ) - target_include_directories(DEMERuntimeDataHelper_python PUBLIC ${ProjectIncludeSource}) - set_target_properties(DEMERuntimeDataHelper_python PROPERTIES - POSITION_INDEPENDENT_CODE TRUE - CXX_STANDARD ${CXXSTD_SUPPORTED} + + target_include_directories( + DEMERuntimeDataHelper_python + PUBLIC $ $ + ) + + set_target_properties( + DEMERuntimeDataHelper_python PROPERTIES + POSITION_INDEPENDENT_CODE True + CXX_STANDARD "${CXXSTD_SUPPORTED}" ) endif() diff --git a/src/core/utils/BaseClasses.hpp b/src/core/utils/BaseClasses.hpp new file mode 100644 index 00000000..197e61c2 --- /dev/null +++ b/src/core/utils/BaseClasses.hpp @@ -0,0 +1,46 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_BASE_CLASSES_HPP +#define DEME_BASE_CLASSES_HPP + +namespace deme { + +// ----------------------------- +// Utility: Non-copyable class +// ----------------------------- +// We protect GPU-related data types with NonCopyable, because the device pointers inside these data types are too +// fragile for copying. If say a shallow copy is enforced to our array types in a vector-of-arrays resizing, then if you +// check the copied array's pointer from host, CUDA might not recognize it properly. The best practice is just using +// unique_ptr to manage these array classes if you expect to put them in places where some under-the-hood copying could +// happen. +class NonCopyable { + protected: + NonCopyable() = default; + ~NonCopyable() = default; + + NonCopyable(const NonCopyable&) = delete; + NonCopyable& operator=(const NonCopyable&) = delete; +}; + +// ----------------------------- +// Utility: CRTP Singleton +// ----------------------------- +template +class Singleton { + public: + static T& GetInstance() { + static T instance; // Guaranteed to be lazy and thread-safe in C++11+ + return instance; + } + + protected: + Singleton() = default; + ~Singleton() = default; +}; + +} // namespace deme + +#endif diff --git a/src/core/utils/CudaAllocator.hpp b/src/core/utils/CudaAllocator.hpp index 32a576aa..0d139a60 100644 --- a/src/core/utils/CudaAllocator.hpp +++ b/src/core/utils/CudaAllocator.hpp @@ -8,7 +8,7 @@ #include -#include "cuda_to_hip.h" +#include #include #include #include @@ -18,131 +18,6 @@ namespace deme { -// Unified memory allocator -template -struct ManagedAllocator { - public: - using value_type = T; - using size_type = std::size_t; - using difference_type = std::ptrdiff_t; - -#if CXX_OLDER(STD_CXX20) - using pointer = T*; - using const_pointer = const T*; - using reference = T&; - using const_reference = const T&; - - template - struct rebind { - typedef typename deme::ManagedAllocator other; - }; -#endif - -#if CXX_EQ_NEWER(STD_CXX14) - using propagate_on_container_move_assignment = std::false_type; - - // These aren't required by the standard, but some implementations - // care about them... - using propagate_on_container_copy_assignment = std::false_type; - using propagate_on_container_swap = std::false_type; -#endif - -#if CXX_EQ_NEWER(STD_CXX17) - using is_always_equal = std::true_type; -#endif - -#if CXX_EQ_OLDER(STD_CXX17) // C++17 or older - ManagedAllocator() noexcept {} - ManagedAllocator(const ManagedAllocator& other) noexcept {} - - template - ManagedAllocator(const ManagedAllocator& other) noexcept {} -#else // CXX_EQ_NEWER(STD_CXX20) C++20 or newer - constexpr ManagedAllocator() noexcept {}; - constexpr ManagedAllocator(const ManagedAllocator& other) noexcept {} - - template - constexpr ManagedAllocator(const ManagedAllocator& other) noexcept {} -#endif - -#if CXX_OLDER(STD_CXX20) // before C++20 - value_type* address(value_type& x) const noexcept { - return &x; - } - - size_type max_size() const noexcept { - return ULLONG_MAX / sizeof(value_type); - } - - template - void construct(U* p, Args&&... args) { - ::new ((void*)p) U(std::forward(args)...); - } - - template - void destroy(U* p) { - p->~U(); - } -#endif - -// #if CXX_OLDER(STD_CXX17) -// pointer allocate(size_type n, std::allocator::const_pointer hint = 0) { return this->__alloc_impl(n); } -// #elif CXX_EQUAL(STD_CXX17) -#if CXX_OLDER(STD_CXX20) - T* allocate(std::size_t n) { - return this->__alloc_impl(n); - } - T* allocate(std::size_t n, const void* hint) { - return this->__alloc_impl(n); - } -#else - [[nodiscard]] constexpr T* allocate(std::size_t n) { - return this->__alloc_impl(n); - } -#endif - // #elif CXX_EQ_NEWER(STD_CXX20) - // [[nodiscard]] constexpr T* allocate(std::size_t n) { return this->__alloc_impl(n); } - // #endif - -#if CXX_OLDER(STD_CXX20) - void deallocate(T* p, std::size_t n) { - cudaFree(p); - } -#else // CXX_EQ_NEWER(STD_CXX20) - constexpr void deallocate(T* p, std::size_t n) { - cudaFree(p); - } -#endif - -#if CXX_OLDER(STD_CXX20) - template - bool operator==(const ManagedAllocator& other) noexcept { - return true; - } - - template - bool operator!=(const ManagedAllocator& other) noexcept { - return false; - } - -#else // CXX_EQ_NEWER(STD_CXX20) - template - constexpr bool operator==(const ManagedAllocator& other) noexcept { - return true; - } -#endif - - private: - constexpr T* __alloc_impl(std::size_t n) { - void* vptr = nullptr; - cudaError_t err = cudaMallocManaged(&vptr, n * sizeof(T), cudaMemAttachGlobal); - if (err == cudaErrorMemoryAllocation || err == cudaErrorNotSupported) { - throw std::bad_alloc(); - } - return (T*)vptr; - } -}; - // Host pinned memory allocator template struct PinnedAllocator { diff --git a/src/core/utils/CudaDebugSync.hpp b/src/core/utils/CudaDebugSync.hpp new file mode 100644 index 00000000..19e5ecf9 --- /dev/null +++ b/src/core/utils/CudaDebugSync.hpp @@ -0,0 +1,36 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_CUDA_DEBUG_SYNC_HPP +#define DEME_CUDA_DEBUG_SYNC_HPP + +#include + +#include + +namespace deme { + +// Debug-only synchronization is process-wide because JIT launch helpers and low-level data migration utilities do not +// belong to a particular DEMSolver. Keep this disabled for normal asynchronous execution; enable it while debugging to +// report asynchronous CUDA errors at the call that enqueued the work. +inline std::atomic& CudaDebugSyncEnabledFlag() { + static std::atomic enabled{false}; + return enabled; +} + +inline void SetCudaDebugSyncEnabled(bool enabled) { + CudaDebugSyncEnabledFlag().store(enabled, std::memory_order_relaxed); +} + +inline bool IsCudaDebugSyncEnabled() { + return CudaDebugSyncEnabledFlag().load(std::memory_order_relaxed); +} + +inline cudaError_t DebugSyncStream(cudaStream_t stream) { + return IsCudaDebugSyncEnabled() ? cudaStreamSynchronize(stream) : cudaSuccess; +} + +} // namespace deme + +#endif diff --git a/src/core/utils/DEMEPaths.cpp b/src/core/utils/DEMEPaths.cpp index e805cfae..a4075eac 100644 --- a/src/core/utils/DEMEPaths.cpp +++ b/src/core/utils/DEMEPaths.cpp @@ -7,6 +7,7 @@ #include #include "DEMEPaths.h" +#include "JitHelper.h" namespace deme { @@ -14,13 +15,13 @@ namespace deme { // Functions for manipulating the DEME data directory // ----------------------------------------------------------------------------- -// Set the path to the DEME data directory (ATTENTION: not thread safe) -void SetDEMEDataPath(const std::string& path) { - DEME_data_path = path; +// Set the path to the DEME data directory (usually reserved for the solver's own usage) +void SetDEMEDataPath() { + DEME_data_path = DEMERuntimeDataHelper::data_path / "data"; } // Obtain the current path to the DEME data directory (thread safe) -const std::filesystem::path& GetDEMEDataPath() { +std::filesystem::path& GetDEMEDataPath() { return DEME_data_path; } @@ -30,4 +31,14 @@ std::string GetDEMEDataFile(const std::string& filename) { return (DEME_data_path / filename).string(); } +// Set the path to the DEME data kernel directory (usually reserved for the solver's own usage) +void SetDEMEKernelPath() { + JitHelper::KERNEL_DIR = DEMERuntimeDataHelper::data_path / "kernel"; +} + +// Set the path to the DEME include directory (usually reserved for the solver's own usage) +void SetDEMEIncludePath() { + JitHelper::KERNEL_INCLUDE_DIR = DEMERuntimeDataHelper::include_path; +} + } // namespace deme diff --git a/src/core/utils/DEMEPaths.h b/src/core/utils/DEMEPaths.h index d5f27e45..6bf9f2f1 100644 --- a/src/core/utils/DEMEPaths.h +++ b/src/core/utils/DEMEPaths.h @@ -21,16 +21,22 @@ namespace deme { const std::filesystem::path BUILD_DATA_PATH = DEMERuntimeDataHelper::data_path / "data"; static std::filesystem::path DEME_data_path(BUILD_DATA_PATH); -// Set the path to the DEME data directory (ATTENTION: not thread safe) -void SetDEMEDataPath(const std::string& path); +// Set the path to the DEME data directory (usually reserved for the solver's own usage) +void SetDEMEDataPath(); // Obtain the current path to the DEME data directory (thread safe) -const std::filesystem::path& GetDEMEDataPath(); +std::filesystem::path& GetDEMEDataPath(); // Obtain the complete path to the specified filename, given relative to the // DEME data directory (thread safe) std::string GetDEMEDataFile(const std::string& filename); +// Set the path to the DEME data kernel directory (usually reserved for the solver's own usage) +void SetDEMEKernelPath(); + +// Set the path to the DEME include directory (usually reserved for the solver's own usage) +void SetDEMEIncludePath(); + } // namespace deme #endif diff --git a/src/core/utils/DataMigrationHelper.hpp b/src/core/utils/DataMigrationHelper.hpp index 07eb33d3..2c3e36a6 100644 --- a/src/core/utils/DataMigrationHelper.hpp +++ b/src/core/utils/DataMigrationHelper.hpp @@ -8,13 +8,47 @@ #include #include +#include #include -#include "GpuError.h" +#include "Logger.hpp" +#include "BaseClasses.hpp" +#include "CudaAllocator.hpp" #include "../../DEM/VariableTypes.h" namespace deme { +// Temporarily select the device that owns a CUDA allocation or stream. CUDA's current device is thread-local, so API +// calls made by the solver's main thread may otherwise inherit the device selected by the other worker. +class ScopedCudaDevice { + public: + explicit ScopedCudaDevice(int device) { + DEME_GPU_CALL(cudaGetDevice(&m_previous_device)); + if (m_previous_device != device) { + DEME_GPU_CALL(cudaSetDevice(device)); + m_restore = true; + } + } + + ~ScopedCudaDevice() { + if (m_restore) { + // Destructors must not throw while unwinding an error from the guarded CUDA operation. + (void)cudaSetDevice(m_previous_device); + } + } + + private: + int m_previous_device = -1; + bool m_restore = false; +}; + +template +class DualArray; +template +class DeviceArray; +template +bool swap_device_buffer(DualArray& lhs, DeviceArray& rhs); + // A to-device memcpy wrapper template void CudaCopyToDevice(T* pD, T* pH) { @@ -68,12 +102,6 @@ inline void HostPtrAlloc(T*& ptr, size_t size) { DEME_GPU_CALL(cudaMallocHost((void**)&ptr, size * sizeof(T))); } -// Managed advise doesn't seem to do anything... -#define DEME_ADVISE_DEVICE(vec, device) \ - { advise(vec, ManagedAdvice::PREFERRED_LOC, device); } -#define DEME_MIGRATE_TO_DEVICE(vec, device, stream) \ - { migrate(vec, device, stream); } - // DEME_DUAL_ARRAY_RESIZE is a reminder for developers that a work array is resized, and this may automatically change // the external device pointer this array's bound to. Therefore, after this call, syncing the data pointer bundle // (granData) to device may be needed, and you remember to cudaSetDevice beforehand so it allocates to correct places. @@ -89,36 +117,30 @@ inline void HostPtrAlloc(T*& ptr, size_t size) { // Use (void) to silence unused warnings. // #define assertm(exp, msg) assert(((void)msg, exp)) +// Used for wrapping data structures so they become usable on GPU. // We protect GPU-related data types with NonCopyable, because the device pointers inside these data types are too // fragile for copying. If say a shallow copy is enforced to our array types in a vector-of-arrays resizing, then if you // check the copied array's pointer from host, CUDA might not recognize it properly. The best practice is just using // unique_ptr to manage these array classes if you expect to put them in places where some under-the-hood copying could // happen. -class NonCopyable { - protected: - NonCopyable() = default; - ~NonCopyable() = default; - - NonCopyable(const NonCopyable&) = delete; - NonCopyable& operator=(const NonCopyable&) = delete; -}; - -// Used for wrapping data structures so they become usable on GPU template class DualStruct : private NonCopyable { private: T* host_data; // Pointer to host memory (pinned) T* device_data; // Pointer to device memory + int device; // Device that owns device_data bool modified_on_host; // Flag to track if host data has been modified public: // Constructor: Initialize and allocate memory for both host and device DualStruct() : modified_on_host(false) { + DEME_GPU_CALL(cudaGetDevice(&device)); DEME_GPU_CALL(cudaMallocHost((void**)&host_data, sizeof(T))); DEME_GPU_CALL(cudaMalloc((void**)&device_data, sizeof(T))); } // Constructor: Initialize and allocate memory for both host and device with init values DualStruct(T init_val) : modified_on_host(false) { + DEME_GPU_CALL(cudaGetDevice(&device)); DEME_GPU_CALL(cudaMallocHost((void**)&host_data, sizeof(T))); DEME_GPU_CALL(cudaMalloc((void**)&device_data, sizeof(T))); @@ -131,34 +153,77 @@ class DualStruct : private NonCopyable { ~DualStruct() { free(); } void free() { - HostPtrDealloc(host_data); // Free pinned memory + HostPtrDealloc(host_data); // Free pinned memory + ScopedCudaDevice device_scope(device); DevicePtrDealloc(device_data); // Free device memory } // Synchronize changes from host to device void toDevice() { + ScopedCudaDevice device_scope(device); DEME_GPU_CALL(cudaMemcpy(device_data, host_data, sizeof(T), cudaMemcpyHostToDevice)); modified_on_host = false; } + // Asynchronous host->device copy on a user stream. Host memory is pinned (cudaMallocHost), + // so the caller must ensure host_data isn't modified until the copy completes. + void toDeviceAsync(cudaStream_t stream) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(device_data, host_data, sizeof(T), cudaMemcpyHostToDevice, stream), stream); + modified_on_host = false; + } + // Synchronize changes from device to host - void toHost() { DEME_GPU_CALL(cudaMemcpy(host_data, device_data, sizeof(T), cudaMemcpyDeviceToHost)); } - - // // Synchronize change of one field of the struct to device - // template - // void syncMemberToDevice(ptrdiff_t offset) { - // DEME_GPU_CALL(cudaMemcpy(reinterpret_cast(device_data) + offset, - // reinterpret_cast(host_data) + offset, sizeof(MemberType), - // cudaMemcpyHostToDevice)); - // } - - // // Synchronize change of one field of the struct to host - // template - // void syncMemberToHost(ptrdiff_t offset) { - // DEME_GPU_CALL(cudaMemcpy(reinterpret_cast(host_data) + offset, - // reinterpret_cast(device_data) + offset, sizeof(MemberType), - // cudaMemcpyDeviceToHost)); - // } + void toHost() { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL(cudaMemcpy(host_data, device_data, sizeof(T), cudaMemcpyDeviceToHost)); + modified_on_host = false; + } + + // Asynchronous device->host copy on a user stream. Host memory is pinned (cudaMallocHost). + void toHostAsync(cudaStream_t stream) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(host_data, device_data, sizeof(T), cudaMemcpyDeviceToHost, stream), stream); + modified_on_host = false; + } + + // Synchronize change of one field of the struct to device + template + void syncMemberToDevice(ptrdiff_t offset) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL(cudaMemcpy(reinterpret_cast(device_data) + offset, + reinterpret_cast(host_data) + offset, sizeof(MemberType), + cudaMemcpyHostToDevice)); + } + + // Asynchronous partial host->device copy on a user stream. + template + void syncMemberToDeviceAsync(ptrdiff_t offset, cudaStream_t stream) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(reinterpret_cast(device_data) + offset, reinterpret_cast(host_data) + offset, + sizeof(MemberType), cudaMemcpyHostToDevice, stream), + stream); + } + + // Synchronize change of one field of the struct to host + template + void syncMemberToHost(ptrdiff_t offset) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL(cudaMemcpy(reinterpret_cast(host_data) + offset, + reinterpret_cast(device_data) + offset, sizeof(MemberType), + cudaMemcpyDeviceToHost)); + } + + // Asynchronous partial device->host copy on a user stream. + template + void syncMemberToHostAsync(ptrdiff_t offset, cudaStream_t stream) { + ScopedCudaDevice device_scope(device); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(reinterpret_cast(host_data) + offset, reinterpret_cast(device_data) + offset, + sizeof(MemberType), cudaMemcpyDeviceToHost, stream), + stream); + } // Check if host data has been modified and not synced bool checkNoPendingModification() { return !modified_on_host; } @@ -198,12 +263,13 @@ class DualStruct : private NonCopyable { size_t getNumBytes() const { return sizeof(T); } }; -#ifndef DEME_USE_MANAGED_ARRAYS -// CPU--GPU unified array, leveraging pinned memory +// Paired host/device array, leveraging pinned host memory template class DualArray : private NonCopyable { public: using PinnedVector = std::vector>; + template + friend bool swap_device_buffer(DualArray& lhs, DeviceArray& rhs); explicit DualArray(size_t* host_external_counter = nullptr, size_t* device_external_counter = nullptr) : m_host_mem_counter(host_external_counter), m_device_mem_counter(device_external_counter) { @@ -234,17 +300,10 @@ class DualArray : private NonCopyable { const size_t old_size = size(); resizeHost(n); resizeDevice(n); - // New elements must be defined on BOTH residences. resizeHost value-initialises - // the host tail, but resizeDevice never initialises grown elements: on - // reallocation it copies forward only pre-existing data, and when capacity - // already suffices it returns without touching memory (so the tail would hold - // stale bytes). Push the host tail down so device [old_size, n) is defined. - // Precondition: device [0, old_size) is already defined, which holds whenever all - // prior growth went through resize(); this method does not repair a host-only - // prefix created by growing through resizeHost() directly (no in-tree caller - // does that). Shrink and same-size calls return without touching device memory: - // device capacity >= logical size is the invariant, and device bytes beyond the - // new logical size are dead, not cleared. + // resizeHost value-initializes the host tail, while resizeDevice only preserves the old device prefix. + // Define the newly grown device range without overwriting device-authoritative values in that prefix. + // This assumes prior growth also used resize(); shrink and same-size calls intentionally leave device storage + // untouched, and bytes beyond the logical size are not considered live array elements. if (n > old_size) { toDevice(old_size, n - old_size); } @@ -255,12 +314,8 @@ class DualArray : private NonCopyable { const size_t old_size = size(); resizeHost(n, val); resizeDevice(n); - // Same as resize(n) above: without this push, the device tail [old_size, n) is - // undefined on both growth paths while every caller reasonably believes it holds - // `val`. Only the grown tail is copied; device data in [0, old_size) stays - // authoritative and is never overwritten by host state here. The same - // preconditions as resize(n) apply: an already-defined device prefix, and - // shrink/same-size calls leave device memory untouched. + // Keep the fill-value contract symmetric across host and device residences. Only the grown tail is copied so + // existing device data remains authoritative. The same growth and shrink preconditions as resize(n) apply. if (n > old_size) { toDevice(old_size, n - old_size); } @@ -351,8 +406,9 @@ class DualArray : private NonCopyable { size_t count = size(); if (count > m_device_capacity) resizeDevice(count); - DEME_GPU_CALL( - cudaMemcpyAsync(m_device_ptr, m_host_vec_ptr->data(), count * sizeof(T), cudaMemcpyHostToDevice, stream)); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(m_device_ptr, m_host_vec_ptr->data(), count * sizeof(T), cudaMemcpyHostToDevice, stream), + stream); m_host_dirty = false; } @@ -361,8 +417,9 @@ class DualArray : private NonCopyable { void toDeviceAsync(cudaStream_t& stream, size_t start, size_t n) { assert(m_host_vec_ptr && m_device_ptr); // Partial flavor aims for speed, no size check - DEME_GPU_CALL(cudaMemcpyAsync(m_device_ptr + start, m_host_vec_ptr->data() + start, n * sizeof(T), - cudaMemcpyHostToDevice, stream)); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(m_device_ptr + start, m_host_vec_ptr->data() + start, n * sizeof(T), + cudaMemcpyHostToDevice, stream), + stream); } void toHost() { @@ -379,16 +436,18 @@ class DualArray : private NonCopyable { void toHostAsync(cudaStream_t& stream) { assert(m_host_vec_ptr && m_device_ptr); - DEME_GPU_CALL( - cudaMemcpyAsync(m_host_vec_ptr->data(), m_device_ptr, size() * sizeof(T), cudaMemcpyDeviceToHost, stream)); + DEME_GPU_CALL_ASYNC( + cudaMemcpyAsync(m_host_vec_ptr->data(), m_device_ptr, size() * sizeof(T), cudaMemcpyDeviceToHost, stream), + stream); m_host_dirty = false; } void toHostAsync(cudaStream_t& stream, size_t start, size_t n) { assert(m_host_vec_ptr && m_device_ptr); // Async partial flavor aims for speed, no size check - DEME_GPU_CALL(cudaMemcpyAsync(m_host_vec_ptr->data() + start, m_device_ptr + start, n * sizeof(T), - cudaMemcpyDeviceToHost, stream)); + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(m_host_vec_ptr->data() + start, m_device_ptr + start, n * sizeof(T), + cudaMemcpyDeviceToHost, stream), + stream); } T getVal(size_t start) { @@ -509,190 +568,13 @@ class DualArray : private NonCopyable { *m_device_mem_counter += delta; } }; -#else -// CPU--GPU unified array, leveraging managed memory -template -class DualArray : private NonCopyable { - public: - using ManagedVector = std::vector>; - - explicit DualArray(size_t* host_external_counter = nullptr, size_t* device_external_counter = nullptr) - : m_host_mem_counter(host_external_counter), m_device_mem_counter(device_external_counter) { - ensureHostVector(); - } - - DualArray(size_t n, size_t* host_external_counter = nullptr, size_t* device_external_counter = nullptr) - : m_host_mem_counter(host_external_counter), m_device_mem_counter(device_external_counter) { - resize(n); - } - - DualArray(size_t n, T val, size_t* host_external_counter = nullptr, size_t* device_external_counter = nullptr) - : m_host_mem_counter(host_external_counter), m_device_mem_counter(device_external_counter) { - resize(n, val); - } - - ~DualArray() { free(); } - - void resize(size_t n) { - assert(m_host_vec_ptr == m_pinned_vec.get() && "resize() requires internal host ownership"); - resizeHost(n); - resizeDevice(n); - } - - // Managed memory is a single allocation seen by host and device alike, so the - // host-side fill in resizeHost already defines what kernels read; no explicit - // host-to-device push is needed here (contrast with the pinned variant). - void resize(size_t n, const T& val) { - assert(m_host_vec_ptr == m_pinned_vec.get() && "resize() requires internal host ownership"); - resizeHost(n, val); - resizeDevice(n); - } - - void resizeHost(size_t n) { - ensureHostVector(); // allocates pinned vec if null - size_t old_bytes = m_host_vec_ptr->size() * sizeof(T); - m_host_vec_ptr->resize(n); - size_t new_bytes = m_host_vec_ptr->size() * sizeof(T); - updateMemCounter(static_cast(new_bytes) - static_cast(old_bytes)); - updateBoundDevicePointer(); - } - - void resizeHost(size_t n, const T& val) { - ensureHostVector(); // allocates pinned vec if null - size_t old_bytes = m_host_vec_ptr->size() * sizeof(T); - m_host_vec_ptr->resize(n, val); - size_t new_bytes = m_host_vec_ptr->size() * sizeof(T); - updateMemCounter(static_cast(new_bytes) - static_cast(old_bytes)); - updateBoundDevicePointer(); - } - - // m_device_capacity is allocated memory, not array usable data range - void resizeDevice(size_t n, bool allow_shrink = false) {} - - void freeHost() { - if (m_host_vec_ptr) { - updateMemCounter(-(ssize_t)(m_host_vec_ptr->size() * sizeof(T))); - } - m_pinned_vec.reset(); - m_host_vec_ptr = nullptr; - updateBoundDevicePointer(); - } - - void freeDevice() {} - - void free() { - freeDevice(); - freeHost(); - } - - void toDevice() {} - - void toDevice(size_t start, size_t n) {} - - void toDeviceAsync(cudaStream_t& stream) {} - - void toDeviceAsync(cudaStream_t& stream, size_t start, size_t n) {} - - void toHost() {} - - void toHost(size_t start, size_t n) {} - - void toHostAsync(cudaStream_t& stream) {} - - void toHostAsync(cudaStream_t& stream, size_t start, size_t n) {} - - T getVal(size_t start) { return (*m_host_vec_ptr)[start]; } - - std::vector getVal(size_t start, size_t n) { - return std::vector(m_host_vec_ptr->begin() + start, m_host_vec_ptr->begin() + start + n); - } - - void setVal(const T& data, size_t start) { (*m_host_vec_ptr)[start] = data; } - - void setVal(const std::vector& data, size_t start, size_t n = 0) { - size_t count = (n > 0) ? n : data.size(); - std::copy(data.begin(), data.begin() + count, m_host_vec_ptr->begin() + start); - } - - void setVal(cudaStream_t& stream, const T& data, size_t start) { (*m_host_vec_ptr)[start] = data; } - - void setVal(cudaStream_t& stream, const std::vector& data, size_t start, size_t n = 0) { - size_t count = (n > 0) ? n : data.size(); - std::copy(data.begin(), data.begin() + count, m_host_vec_ptr->begin() + start); - } - - void markHostModified() { m_host_dirty = true; } - void unmarkHostModified() { m_host_dirty = false; } - - // Array's in-use data range is always stored on host by size() - size_t size() const { return m_host_vec_ptr ? m_host_vec_ptr->size() : 0; } - - // Get host or device size in bytes - size_t getNumBytes() const { return m_host_vec_ptr ? m_host_vec_ptr->size() * sizeof(T) : 0; } - - T* host() { return m_host_vec_ptr ? m_host_vec_ptr->data() : nullptr; } - - T* device() { return host(); } - - // Overloaded operator& for device pointer access - T* operator&() const { return host(); } - - // data() returns device data for the ease of packing pointers - T* data() { return host(); } - - ManagedVector& getHostVector() { return *m_host_vec_ptr; } - - void bindDevicePointer(T** external_ptr_to_ptr) { - m_bound_device_ptr = external_ptr_to_ptr; - updateBoundDevicePointer(); - } - - void unbindDevicePointer() { m_bound_device_ptr = nullptr; } - - void setHostMemoryCounter(size_t* counter) { m_host_mem_counter = counter; } - void setDeviceMemoryCounter(size_t* counter) { m_device_mem_counter = counter; } - // You can use nullptr to unbind - - T& operator[](size_t i) { return (*m_host_vec_ptr)[i]; } - const T& operator[](size_t i) const { return (*m_host_vec_ptr)[i]; } - T operator()(size_t i) { return getVal(i); } - - private: - std::unique_ptr m_pinned_vec = nullptr; - ManagedVector* m_host_vec_ptr = nullptr; - - size_t* m_host_mem_counter = nullptr; - size_t* m_device_mem_counter = nullptr; - - T** m_bound_device_ptr = nullptr; - - bool m_host_dirty = false; - - void ensureHostVector(size_t n = 0) { - if (!m_host_vec_ptr) { - m_pinned_vec = std::make_unique(n); - m_host_vec_ptr = m_pinned_vec.get(); - } - } - - void updateBoundDevicePointer() { - if (m_bound_device_ptr) - *m_bound_device_ptr = host(); - } - - void updateMemCounter(ssize_t delta) { - if (m_host_mem_counter) - *m_host_mem_counter += delta; - if (m_device_mem_counter) - *m_device_mem_counter += delta; - } -}; -#endif // Pure device data type, usually used for scratching space template class DeviceArray : private NonCopyable { public: + template + friend bool swap_device_buffer(DualArray& lhs, DeviceArray& rhs); DeviceArray(size_t* external_counter = nullptr) : m_mem_counter(external_counter) {} explicit DeviceArray(size_t n, size_t* external_counter = nullptr) : m_mem_counter(external_counter) { resize(n); } @@ -716,6 +598,7 @@ class DeviceArray : private NonCopyable { DevicePtrDealloc(m_data); m_data = new_device_ptr; + updateBoundDevicePointer(); updateMemCounter(static_cast(n * sizeof(T))); m_capacity = n; @@ -726,8 +609,15 @@ class DeviceArray : private NonCopyable { updateMemCounter(-(ssize_t)(m_capacity * sizeof(T))); m_data = nullptr; m_capacity = 0; + updateBoundDevicePointer(); } + void bindDevicePointer(T** external_ptr_to_ptr) { + m_bound_device_ptr = external_ptr_to_ptr; + updateBoundDevicePointer(); + } + void unbindDevicePointer() { m_bound_device_ptr = nullptr; } + size_t size() const { return m_capacity; } // Get host or device size in bytes @@ -741,15 +631,30 @@ class DeviceArray : private NonCopyable { private: T* m_data = nullptr; + T** m_bound_device_ptr = nullptr; size_t m_capacity = 0; size_t* m_mem_counter = nullptr; + void updateBoundDevicePointer() { + if (m_bound_device_ptr) + *m_bound_device_ptr = m_data; + } + void updateMemCounter(ssize_t delta) { if (m_mem_counter) *m_mem_counter += delta; } }; +template +inline bool swap_device_buffer(DualArray& lhs, DeviceArray& rhs) { + using std::swap; + swap(lhs.m_device_ptr, rhs.m_data); + swap(lhs.m_device_capacity, rhs.m_capacity); + lhs.updateBoundDevicePointer(); + return true; +} + /// @brief General abstraction of vector pool /// @tparam T Array data type /// @tparam DerivedEnclosedData The data type of the enclosed arrays in the derived pool class @@ -766,11 +671,20 @@ class ResourcePool : private NonCopyable { void resize(const std::string& name, size_t new_size) { auto it = name_to_index.find(name); if (it == name_to_index.end()) { - throw std::runtime_error("Cannot resize: name not found"); + DEME_ERROR(std::string("Cannot resize: name not found")); } vectors[it->second]->resize(new_size); } + bool exist(const std::string& name) { + auto it = this->name_to_index.find(name); + if (it == this->name_to_index.end()) { + return false; + } else { + return true; + } + } + void unclaim(const std::string& name) { auto it = name_to_index.find(name); if (it == name_to_index.end()) @@ -782,7 +696,7 @@ class ResourcePool : private NonCopyable { void release(const std::string& name) { auto it = name_to_index.find(name); if (it == name_to_index.end()) { - throw std::runtime_error("Cannot release: name not found"); + DEME_ERROR("Cannot release: name not found"); } vectors[it->second]->free(); in_use[it->second] = std::nullopt; @@ -829,7 +743,7 @@ class DeviceVectorPool : public ResourcePool> { Base::vectors[index]->resize(size); return Base::vectors[index]->data(); } else { - throw std::runtime_error("Name already claimed: " + name); + DEME_ERROR("Name already claimed: %s", name.c_str()); } } @@ -852,7 +766,7 @@ class DeviceVectorPool : public ResourcePool> { T* get(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second]->data(); } @@ -885,7 +799,7 @@ class DualArrayPool : public ResourcePool> { Base::vectors[index]->resize(size); return Base::vectors[index].get(); } else { - throw std::runtime_error("Name already claimed: " + name); + DEME_ERROR("Name already claimed: %s", name.c_str()); } } @@ -908,21 +822,21 @@ class DualArrayPool : public ResourcePool> { DualArray* get(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second].get(); } T* getHost(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second]->host(); } T* getDevice(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second]->device(); } @@ -956,7 +870,7 @@ class DualStructPool : public ResourcePool> { size_t index = Base::name_to_index[name]; return Base::vectors[index].get(); } else { - throw std::runtime_error("Name already claimed: " + name); + DEME_ERROR("Name already claimed: %s", name.c_str()); } } @@ -978,21 +892,21 @@ class DualStructPool : public ResourcePool> { DualStruct* get(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second].get(); } T* getHost(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second]->getHostPointer(); } T* getDevice(const std::string& name) { auto it = this->name_to_index.find(name); if (it == this->name_to_index.end()) - throw std::runtime_error("Name not found: " + name); + DEME_ERROR("Name not found: %s", name.c_str()); return this->vectors[it->second]->getDevicePointer(); } }; @@ -1004,6 +918,178 @@ class DualStructPool : public ResourcePool> { // device to host more freely and concern-free, but when copying from host to device, that's either in a centrialized // initialization stage, or we do piecemeal and fine-grain copying which reflects the user's forced system updates only. +namespace xfer { // Memory Transfer Bundle Manager + +enum class XferMode { Same, Peer, Stage }; + +struct HostBounce { + void* ptr = nullptr; + size_t cap = 0; + ~HostBounce() { + if (ptr) + cudaFreeHost(ptr); + } + inline void ensure(size_t need) { + if (cap >= need) + return; + if (ptr) + cudaFreeHost(ptr); + DEME_GPU_CALL(cudaMallocHost(&ptr, need)); // pinned + cap = need; + } +}; + +struct PeerCache { + int dst{-1}, src{-1}; + int can{-1}; +}; // -1 unknown, 0 no, 1 yes +static thread_local HostBounce __bounce; +static thread_local PeerCache __pc; + +inline XferMode plan_mode(int dstDev, int srcDev) { + if (dstDev == srcDev) + return XferMode::Same; + + if (__pc.dst != dstDev || __pc.src != srcDev || __pc.can < 0) { + __pc.dst = dstDev; + __pc.src = srcDev; + __pc.can = 0; + int can = 0; + DEME_GPU_CALL(cudaDeviceCanAccessPeer(&can, dstDev, srcDev)); + if (can) { + int cur = -1; + DEME_GPU_CALL(cudaGetDevice(&cur)); + if (cur != dstDev) + DEME_GPU_CALL(cudaSetDevice(dstDev)); + cudaError_t st = cudaDeviceEnablePeerAccess(srcDev, 0); + if (st == cudaErrorPeerAccessAlreadyEnabled) + (void)cudaGetLastError(); + else if (st != cudaSuccess) + can = 0; + if (cur != dstDev) + DEME_GPU_CALL(cudaSetDevice(cur)); + } + __pc.can = can ? 1 : 0; + } + return __pc.can ? XferMode::Peer : XferMode::Stage; +} + +inline size_t __sum_bytes(const size_t* bytes, int n) { + size_t t = 0; + for (int i = 0; i < n; ++i) + t += bytes[i]; + return t; +} +inline size_t __max_bytes(const size_t* bytes, int n) { + size_t m = 0; + for (int i = 0; i < n; ++i) + m = std::max(m, bytes[i]); + return m; +} + +// Same device: D2D +inline void xfer_same(int dev, + void* const* dst, + const void* const* src, + const size_t* bytes, + int n, + cudaStream_t stream) { + int cur = -1; + DEME_GPU_CALL(cudaGetDevice(&cur)); + int dev_sw = (cur != dev) ? 1 : 0; + if (dev_sw) + DEME_GPU_CALL(cudaSetDevice(dev)); + + for (int i = 0; i < n; ++i) + if (bytes[i]) { + DEME_GPU_CALL_ASYNC(cudaMemcpyAsync(dst[i], src[i], bytes[i], cudaMemcpyDeviceToDevice, stream), stream); + } + if (dev_sw) + DEME_GPU_CALL(cudaSetDevice(cur)); +} + +// Peer: cudaMemcpyPeer +inline void xfer_peer(int dstDev, + int srcDev, + void* const* dst, + const void* const* src, + const size_t* bytes, + int n, + cudaStream_t stream) { + int cur = -1; + DEME_GPU_CALL(cudaGetDevice(&cur)); + int dev_sw = (cur != dstDev) ? 1 : 0; + if (dev_sw) + DEME_GPU_CALL(cudaSetDevice(dstDev)); + for (int i = 0; i < n; ++i) + if (bytes[i]) { + DEME_GPU_CALL_ASYNC(cudaMemcpyPeerAsync(dst[i], dstDev, src[i], srcDev, bytes[i], stream), stream); + } + if (dev_sw) + DEME_GPU_CALL(cudaSetDevice(cur)); +} + +// Stage== No Peer +inline void xfer_stage_as_d2d(int dstDev, + int srcDev, + void* const* dst, + const void* const* src, + const size_t* bytes, + int n, + cudaStream_t /*stream*/) { + for (int i = 0; i < n; ++i) + if (bytes[i]) { + DEME_GPU_CALL(cudaMemcpy(dst[i], src[i], bytes[i], cudaMemcpyDeviceToDevice)); + } +} + +// Frontend +inline void D2D_bundle(int dstDev, + int srcDev, + void* const* dst, + const void* const* src, + const size_t* bytes, + int n, + cudaStream_t stream = 0) { + if (n <= 0) + return; + XferMode mode = plan_mode(dstDev, srcDev); + + switch (mode) { + case XferMode::Same: + xfer_same(srcDev, dst, src, bytes, n, stream); + break; + case XferMode::Peer: + xfer_peer(dstDev, srcDev, dst, src, bytes, n, stream); + break; + case XferMode::Stage: + xfer_stage_as_d2d(dstDev, srcDev, dst, src, bytes, n, stream); + break; + } +} + +// Builder +struct XferList { + static constexpr int MAX = 16; + void* dst[MAX]{}; + const void* src[MAX]{}; + size_t sz[MAX]{}; + int n{0}; + inline void add(void* d, const void* s, size_t b) { + if (b && n < MAX) { + dst[n] = d; + src[n] = s; + sz[n] = b; + ++n; + } + } + inline void run(int dstDev, int srcDev, cudaStream_t stream = 0) { + D2D_bundle(dstDev, srcDev, dst, src, sz, n, stream); + } +}; + +} // namespace xfer + } // namespace deme #endif diff --git a/src/core/utils/DeviceDataTransfer.cpp b/src/core/utils/DeviceDataTransfer.cpp new file mode 100644 index 00000000..4f2a12ed --- /dev/null +++ b/src/core/utils/DeviceDataTransfer.cpp @@ -0,0 +1,60 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include "DeviceDataTransfer.hpp" + +namespace deme { +namespace device_data { + +cudaError_t ValidateOutputPointer(const void* pointer, size_t bytes, int device) { + if (bytes == 0) { + return cudaSuccess; + } + if (!pointer || device < 0) { + return cudaErrorInvalidValue; + } + + int device_count = 0; + cudaError_t status = cudaGetDeviceCount(&device_count); + if (status != cudaSuccess) { + return status; + } + if (device >= device_count) { + return cudaErrorInvalidDevice; + } + + cudaPointerAttributes attributes{}; + status = cudaPointerGetAttributes(&attributes, pointer); + if (status != cudaSuccess) { + return status; + } + if (attributes.type == cudaMemoryTypeDevice && attributes.device != device) { + return cudaErrorInvalidDevicePointer; + } + if (attributes.type != cudaMemoryTypeDevice && attributes.type != cudaMemoryTypeManaged) { + return cudaErrorInvalidDevicePointer; + } + return cudaSuccess; +} + +cudaError_t TransferBuffer::Copy(void* destination, + int /* destination_device */, + const void* source, + int /* source_device */, + size_t bytes) { + if (bytes == 0) { + return cudaSuccess; + } + if (!destination || !source) { + return cudaErrorInvalidValue; + } + // DEME's regular dT/kT exchange uses this CUDA path when direct peer access is unavailable. With unified virtual + // addressing, CUDA identifies both owning devices and selects either a direct peer route or driver-managed + // staging. If the topology cannot support the copy, return that CUDA error to the caller. + return cudaMemcpy(destination, source, bytes, cudaMemcpyDeviceToDevice); +} + +} // namespace device_data +} // namespace deme diff --git a/src/core/utils/DeviceDataTransfer.hpp b/src/core/utils/DeviceDataTransfer.hpp new file mode 100644 index 00000000..14362867 --- /dev/null +++ b/src/core/utils/DeviceDataTransfer.hpp @@ -0,0 +1,37 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_DEVICE_DATA_TRANSFER_HPP +#define DEME_DEVICE_DATA_TRANSFER_HPP + +#include + +#include + +namespace deme { +namespace device_data { + +/// Validate that a caller-provided output range is backed by CUDA-accessible memory on the declared device. +cudaError_t ValidateOutputPointer(const void* pointer, size_t bytes, int device); + +/// Synchronous CUDA device-to-device transfer helper. CUDA selects the available inter-device route, including any +/// driver-managed staging needed when direct peer transfer is unavailable. +/// +/// This utility deliberately depends only on the CUDA runtime and the C++ standard library so it can be moved to a +/// standalone interoperability library without carrying DEM-Engine types or ownership rules with it. +class TransferBuffer { + public: + TransferBuffer() = default; + + TransferBuffer(const TransferBuffer&) = delete; + TransferBuffer& operator=(const TransferBuffer&) = delete; + + cudaError_t Copy(void* destination, int destination_device, const void* source, int source_device, size_t bytes); +}; + +} // namespace device_data +} // namespace deme + +#endif diff --git a/src/core/utils/GpuError.h b/src/core/utils/GpuError.h deleted file mode 100644 index c24bc315..00000000 --- a/src/core/utils/GpuError.h +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// -// SPDX-License-Identifier: BSD-3-Clause - -#ifndef DEME_GPU_ERROR_H -#define DEME_GPU_ERROR_H - -#include -#include -#include -#include "cuda_to_hip.h" - -#define DEME_GPU_CALL(res) \ - { gpu_assert((res), __FILE__, __LINE__); } - -#define DEME_GPU_CALL_NOTHROW(res) \ - { gpu_assert((res), __FILE__, __LINE__, false); } - -#define DEME_GPU_CALL_WATCH_BETA(res) \ - { gpu_assert_watch_beta((res), __FILE__, __LINE__, *(stateParams.maxVel), true); } - -inline bool gpu_assert(cudaError_t code, const char* filename, int line, bool except = true) { - if (code != cudaSuccess) { - if (except) { - std::stringstream out; - out << "GPU Assertion: " << cudaGetErrorString(code) << ". This happened in " << filename << ":" << line - << "\n"; - out << "You can check out the troubleshoot section of DEME to see if it helps, or post this error on " - "Chrono's forum https://groups.google.com/g/projectchrono \n"; - throw std::runtime_error(out.str()); - } - return false; - } - return true; -} - -inline bool gpu_assert_watch_beta(cudaError_t code, const char* filename, int line, float max_vel, bool except = true) { - if (code != cudaSuccess) { - if (except) { - std::stringstream out_msg; - out_msg << "\n\n-------- Simulation crashed \"potentially\" due to too many geometries in a bin --------\n"; - out_msg << "The dT reported max velocity is "; - out_msg << max_vel << "\n"; - out_msg << "------------------------------------\n"; - out_msg << "If the velocity is huge, then the simulation probably diverged due to encountering " - "large particle velocities.\nDecreasing the step size could help, and remember to check if your " - "simulation objects are initially within the domain you specified.\n"; - out_msg << "------------------------------------\n"; - out_msg - << "If the velocity is fair, and you *are* using a custom force model, one thing to do is to " - "SetForceCalcThreadsPerBlock to a small number like 128 (see README.md troubleshooting for " - "details).\nIf you are not using a custom model, one thing to do is to ensure the simulation " - "world size (InstructBoxDomainDimension) is not orders of magnitude larger than the actual " - "space the simulation entities take up.\nIf none works and you are going to discuss this on forum " - "https://groups.google.com/g/projectchrono, " - "please include a visual rendering of the simulation before crash.\n\n"; - std::cerr << out_msg.str(); - std::stringstream out; - out << "GPU Assertion: " << cudaGetErrorString(code) << ". This happened in " << filename << ":" << line - << "\n"; - throw std::runtime_error(out.str()); - } - return false; - } - return true; -} - -#endif \ No newline at end of file diff --git a/src/core/utils/GpuManager.cpp b/src/core/utils/GpuManager.cpp index 20eb618e..23852b5e 100644 --- a/src/core/utils/GpuManager.cpp +++ b/src/core/utils/GpuManager.cpp @@ -5,10 +5,13 @@ #include #include "GpuManager.h" -#include "GpuError.h" +#include "Logger.hpp" + +namespace deme { GpuManager::GpuManager(unsigned int total_streams) { - ndevices = scanNumDevices(); + nvisible_devices = scanNumDevices(); + nactive_devices = std::min(static_cast(total_streams), nvisible_devices); // if (ndevices == 0) { // std::cerr << "No GPU device is detected. Try lspci and see what you get.\nIf you indeed have GPU " @@ -19,10 +22,10 @@ GpuManager::GpuManager(unsigned int total_streams) { // "GPU.\nTry allocating 2 GPU devices if possible.\n\n"; // } - this->streams.resize(ndevices); + this->streams.resize(nvisible_devices); for (unsigned int current_device = 0; total_streams > 0; total_streams--, current_device++) { - if (current_device >= ndevices) { + if (current_device >= static_cast(nvisible_devices)) { current_device = 0; } @@ -33,22 +36,26 @@ GpuManager::GpuManager(unsigned int total_streams) { } } -GpuManager::GpuManager(const std::vector& device_ids) { - // ndevices counts distinct physical devices in use - std::set unique_ids(device_ids.begin(), device_ids.end()); - ndevices = static_cast(unique_ids.size()); - - if (!device_ids.empty()) { - int max_id = *std::max_element(device_ids.begin(), device_ids.end()); - this->streams.resize(max_id + 1); +GpuManager::GpuManager(const std::vector& stream_devices) { + nvisible_devices = scanNumDevices(); + if (stream_devices.empty()) { + DEME_ERROR("GpuManager requires at least one CUDA device assignment."); } - // Each entry in device_ids creates one stream; duplicate device IDs are intentional and result in multiple - // streams on the same device (used when both kT and dT threads run on the same physical device). - for (int dev_id : device_ids) { + this->streams.resize(nvisible_devices); + std::set active_devices; + for (int device : stream_devices) { + if (device < 0 || device >= nvisible_devices) { + DEME_ERROR("CUDA device ID %d is out of range. %d CUDA device(s) are visible.", device, nvisible_devices); + } + cudaStream_t new_stream = nullptr; - this->streams[dev_id].push_back(StreamInfo{dev_id, new_stream, false}); + // Worker streams currently use CUDA's default stream. Keep a separate record per worker even when both workers + // share a device so stream assignment remains deterministic and can adopt non-default streams independently. + this->streams[device].push_back(StreamInfo{device, new_stream, false}); + active_devices.insert(device); } + nactive_devices = static_cast(active_devices.size()); } // TODO: add CUDA error checking @@ -125,4 +132,6 @@ void GpuManager::setStreamAvailable(const StreamInfo& info) { stream->_impl_active = false; } } -} \ No newline at end of file +} + +} // namespace deme diff --git a/src/core/utils/GpuManager.h b/src/core/utils/GpuManager.h index d5a5d3c3..d3dd6a01 100644 --- a/src/core/utils/GpuManager.h +++ b/src/core/utils/GpuManager.h @@ -1,16 +1,19 @@ #ifndef DEME_GPU_MANAGER_H #define DEME_GPU_MANAGER_H -#include "cuda_to_hip.h" +#include #include #include +namespace deme { + // A device number manager that evenly distributes the streams needed to all the available devices class GpuManager { public: GpuManager(unsigned int total_streams = 1); - // Construct using explicit device IDs (one stream per device ID). - GpuManager(const std::vector& device_ids); + /// Create one stream record for each explicitly selected CUDA device. + /// Repeated device IDs are allowed so multiple workers can share one GPU. + explicit GpuManager(const std::vector& stream_devices); ~GpuManager(); struct StreamInfo { @@ -41,13 +44,18 @@ class GpuManager { // Mark a stream as unused. void setStreamAvailable(const StreamInfo&); - // Return the number of devices detected. - int getNumDevices() { return ndevices; } + // Return the number of distinct devices assigned to workers. + int getNumDevices() const { return nactive_devices; } + // Return the number of logical CUDA devices visible to this process. + int getNumVisibleDevices() const { return nvisible_devices; } private: - int ndevices; + int nvisible_devices; + int nactive_devices; std::vector> streams; std::mutex stream_manipulation_mutex; }; +} // namespace deme + #endif diff --git a/src/core/utils/JitHelper.cpp b/src/core/utils/JitHelper.cpp index d10695cd..05868789 100644 --- a/src/core/utils/JitHelper.cpp +++ b/src/core/utils/JitHelper.cpp @@ -1,44 +1,105 @@ // Copyright (c) 2021, SBEL GPU Development Team // Copyright (c) 2021, University of Wisconsin - Madison // -// SPDX-License-Identifier: BSD-3-Clause +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include #include -#include -#include +#include #include -#include +#include +#include #include -#include -#include +#include +#include +#include +#include #include -#include - -#include "cuda_to_hip.h" - -// Compile-time default architecture fallback. -// Can be overridden at build time via -DDEME_DEFAULT_CUDA_ARCH_STR="compute_XY" (CUDA) -// or -DDEME_DEFAULT_HIP_ARCH_STR="gfx90a" (HIP). -// At runtime, the environment variable DEME_DEFAULT_CUDA_ARCH (CUDA) or -// DEME_DEFAULT_HIP_ARCH (HIP) takes precedence. -#if defined(USE_HIP) - #ifndef DEME_DEFAULT_HIP_ARCH_STR - #define DEME_DEFAULT_HIP_ARCH_STR "gfx90a" - #endif -#else - #ifndef DEME_DEFAULT_CUDA_ARCH_STR - #define DEME_DEFAULT_CUDA_ARCH_STR "compute_75" - #endif + +#include +#include + +// Compile-time default CUDA architecture fallback. +// Can be overridden at build time via -DDEME_DEFAULT_CUDA_ARCH_STR="compute_XY". +// At runtime, the environment variable DEME_DEFAULT_CUDA_ARCH takes precedence. +#ifndef DEME_DEFAULT_CUDA_ARCH_STR + #define DEME_DEFAULT_CUDA_ARCH_STR "compute_75" #endif #include #include "RuntimeData.h" #include "JitHelper.h" +#include "Logger.hpp" + +#ifdef DEME_PYTHON_JIT + #include "DEM/python/PythonCudaIncludes.hpp" +#endif + +namespace { + +constexpr uint64_t kFNVOffset = 0xcbf29ce484222325ULL; +constexpr uint64_t kFNVPrime = 0x100000001b3ULL; + +constexpr int getCudaVersion() { +#ifdef CUDA_VERSION + return CUDA_VERSION; +#else + return CUDART_VERSION; +#endif +} + +std::pair getNvrtcVersion() { + int major = 0; + int minor = 0; + if (nvrtcVersion(&major, &minor) != NVRTC_SUCCESS) { + return {0, 0}; + } + return {major, minor}; +} + +bool cudaHeadersMatch(const std::filesystem::path& include_dir, int nvrtc_major, int nvrtc_minor) { + std::ifstream cuda_header(include_dir / "cuda.h"); + if (!cuda_header) { + return false; + } + + std::string line; + const std::regex version_pattern(R"(^\s*#\s*define\s+CUDA_VERSION\s+([0-9]+))"); + std::smatch match; + while (std::getline(cuda_header, line)) { + if (std::regex_search(line, match, version_pattern)) { + const int encoded_version = std::stoi(match[1].str()); + const int header_major = encoded_version / 1000; + const int header_minor = (encoded_version % 1000) / 10; + return header_major == nvrtc_major && header_minor == nvrtc_minor; + } + } + return false; +} + +std::string sanitizeFilename(const std::string& name) { + std::string sanitized = name; + for (auto& c : sanitized) { + if (!(std::isalnum(static_cast(c)) || c == '_' || c == '-')) { + c = '_'; + } + } + return sanitized; +} + +bool isArchitectureError(const std::exception& error) { + const std::string message = error.what(); + return message.find("arch") != std::string::npos || message.find("compute_") != std::string::npos || + message.find("sm_") != std::string::npos; +} -deme::jit::ProgramCache* JitHelper::kcache = nullptr; +} // namespace std::filesystem::path JitHelper::KERNEL_DIR = DEMERuntimeDataHelper::data_path / "kernel"; std::filesystem::path JitHelper::KERNEL_INCLUDE_DIR = DEMERuntimeDataHelper::include_path; +std::filesystem::path JitHelper::CACHE_DIR; JitHelper::Header::Header(const std::filesystem::path& sourcefile) { this->_source = JitHelper::loadSourceFile(sourcefile); @@ -56,139 +117,277 @@ void JitHelper::Header::substitute(const std::string& symbol, const std::string& } } -deme::jit::Program JitHelper::buildProgram(const std::string& name, - const std::filesystem::path& source, - std::unordered_map substitutions, - std::vector flags) { - // Double ensure include paths for runtime headers + CUDA/CCCL (cuda::std) or ROCm - auto add_inc = [&](const std::filesystem::path& p) { - if (p.empty()) - return; - std::error_code ec; - if (!std::filesystem::exists(p, ec)) - return; - const std::string inc_flag = "-I" + p.string(); - if (std::find(flags.begin(), flags.end(), inc_flag) == flags.end()) - flags.push_back(inc_flag); - }; - - // Project/runtime includes - add_inc(KERNEL_INCLUDE_DIR); +JitHelper::CachedProgram JitHelper::buildProgram(const std::string& name, + const std::filesystem::path& source, + std::unordered_map substitutions, + std::vector flags) { + std::string code = name + "\n"; - // Also add the source tree for header includes (cuda_to_hip.h, etc.) - // KERNEL_INCLUDE_DIR is the build tree, but some headers live in src/ - // The source tree is one level up from build, then into src/ - std::error_code ec; - std::filesystem::path src_include = std::filesystem::canonical(KERNEL_INCLUDE_DIR / "..", ec); - if (!ec) { - add_inc(src_include / "src"); - } - - // Common fallbacks -#if defined(USE_HIP) - // Helper: find clang builtin headers by scanning //include. - // Linux ROCm keeps them under lib/llvm/lib/clang; the Windows HIP SDK under lib/clang. - auto add_clang_builtins = [&](const std::filesystem::path& rocm_root) { - for (const auto& clang_base : {rocm_root / "lib" / "llvm" / "lib" / "clang", rocm_root / "lib" / "clang"}) { - std::error_code scan_ec; - for (const auto& entry : std::filesystem::directory_iterator(clang_base, scan_ec)) { - if (entry.is_directory()) { - add_inc(entry.path() / "include"); - break; // use the first (and typically only) version dir + code.append(JitHelper::loadSourceFile(source)); + // Apply the substitutions deterministically (unordered_map iteration is non-deterministic) + std::vector> ordered_subs(substitutions.begin(), substitutions.end()); + std::sort(ordered_subs.begin(), ordered_subs.end(), [](const auto& a, const auto& b) { return a.first < b.first; }); + for (auto& subst : ordered_subs) { + code = std::regex_replace(code, std::regex(subst.first), subst.second); + } + { + // Select CUDA headers matching the NVRTC library loaded on the deployment host. A wheel's build-time CUDA + // path can exist on the host while pointing at another toolkit version, which makes CUDA's internal headers + // fail with misleading undefined-intrinsic errors during JIT compilation. + const auto [nvrtc_major, nvrtc_minor] = getNvrtcVersion(); + std::vector include_paths; + { + std::string dirs = DEME_CUDA_TOOLKIT_INCLUDE_DIRS; // "dir1;dir2;dir3" + std::stringstream ss(dirs); + std::string dir; + while (std::getline(ss, dir, ';')) { + if (!dir.empty()) { + include_paths.emplace_back(dir); } } } - }; + auto add_inc = [&](const std::filesystem::path& p) { + if (p.empty()) { + return; + } + std::error_code ec; + if (!std::filesystem::exists(p, ec)) { + return; + } + std::string inc_flag = "-I" + p.string(); + if (std::find(flags.begin(), flags.end(), inc_flag) == flags.end()) { + flags.push_back(inc_flag); + } + }; + bool found_matching_cuda_headers = false; + auto add_cuda_inc = [&](const std::filesystem::path& p) { + if (nvrtc_major == 0 || !cudaHeadersMatch(p, nvrtc_major, nvrtc_minor)) { + return; + } + add_inc(p); + add_inc(p / "cccl"); + found_matching_cuda_headers = true; + }; +#ifdef DEME_PYTHON_JIT + // Import passes the split NVIDIA wheel directories directly into this extension's core. + // Native C++ does not compile this branch or inherit configuration from a Python parent. + const auto& python_include_paths = deme::python::CudaIncludePaths(); + if (!python_include_paths.empty()) { + for (const auto& path : python_include_paths) { + add_cuda_inc(path); + } + if (!found_matching_cuda_headers) { + DEME_ERROR("The pip CUDA headers do not match loaded NVRTC %d.%d. Use a consistent CUDA environment.", + nvrtc_major, nvrtc_minor); + } + for (const auto& path : python_include_paths) { + add_inc(path); + } + add_inc(KERNEL_INCLUDE_DIR); + } else { +#endif + for (auto& p : include_paths) { + add_cuda_inc(p); + } + add_inc(KERNEL_INCLUDE_DIR); + if (const char* cuda_home = std::getenv("CUDA_HOME")) { + add_cuda_inc(std::filesystem::path(cuda_home) / "include"); + } + if (nvrtc_major > 0) { + add_cuda_inc("/usr/local/cuda-" + std::to_string(nvrtc_major) + "." + std::to_string(nvrtc_minor) + + "/include"); + } + add_cuda_inc("/usr/local/cuda/include"); +#ifdef DEME_PYTHON_JIT + } +#endif + if (!found_matching_cuda_headers) { + DEME_ERROR( + "NVRTC %d.%d is loaded, but matching CUDA headers were not found. Install that CUDA Toolkit or set " + "CUDA_HOME to its root directory.", + nvrtc_major, nvrtc_minor); + } + } - // ROCm include paths for hipRTC. ROCM_PATH is the Linux convention; the Windows HIP SDK - // sets HIP_PATH instead, so accept either (hiprtc on Windows has no implicit include path, - // which makes this lookup load-bearing there, not just a fallback). - const char* rocm_path = std::getenv("ROCM_PATH"); - if (rocm_path == nullptr || rocm_path[0] == '\0') - rocm_path = std::getenv("HIP_PATH"); - if (rocm_path != nullptr && rocm_path[0] != '\0') { - add_inc(std::filesystem::path(rocm_path) / "include"); - add_inc(std::filesystem::path(rocm_path) / "include" / "hipcub"); - add_inc(std::filesystem::path(rocm_path) / "include" / "rocprim"); - // Clang builtin headers (stddef.h, etc.) for hiprtc -- scan for actual version dir - add_clang_builtins(std::filesystem::path(rocm_path)); - } - add_inc("/opt/rocm/include"); - add_inc("/opt/rocm/include/hipcub"); - add_inc("/opt/rocm/include/rocprim"); - // Clang builtin headers for hiprtc (needed for stddef.h, stdint.h, etc.) - add_clang_builtins(std::filesystem::path("/opt/rocm")); -#else - if (const char* cuda_home = std::getenv("CUDA_HOME")) { - add_inc(std::filesystem::path(cuda_home) / "include"); - add_inc(std::filesystem::path(cuda_home) / "include" / "cccl"); + int device = 0; + cudaDeviceProp prop{}; + bool architecture_detected = cudaGetDevice(&device) == cudaSuccess && + cudaGetDeviceProperties(&prop, device) == cudaSuccess && prop.major > 0; + if (!architecture_detected) { + device = 0; + prop.major = 0; + prop.minor = 0; + } + const std::string arch_tag = + architecture_detected ? "compute_" + std::to_string(prop.major) + std::to_string(prop.minor) : "auto"; + // Prefer the active device's exact compute capability. If CUDA device discovery is unavailable, leave the flag + // unset so Jitify can perform its own architecture detection during compilation. + if (architecture_detected) { + flags.push_back("-arch=" + arch_tag); } - add_inc("/usr/local/cuda/include"); - add_inc("/usr/local/cuda/include/cccl"); -#endif -#if defined(USE_HIP) - // For hiprtc: start with hip_runtime.h to provide HIP types/functions in device code. - // The program name is passed separately to hiprtcCreateProgram. - std::string code = "#include \n"; -#else - // For CUDA/jitify: the program name on the first line is a jitify requirement. - std::string code = name + "\n"; -#endif + std::vector flags_sorted = flags; + std::sort(flags_sorted.begin(), flags_sorted.end()); + const std::string flags_sig = jitify::reflection::reflect_list(flags_sorted); + const auto [nvrtc_major, nvrtc_minor] = getNvrtcVersion(); + const std::string nvrtc_tag = std::to_string(nvrtc_major) + "." + std::to_string(nvrtc_minor); + const std::string fingerprint = code + "|flags:" + flags_sig + "|api:" + std::to_string(DEME_API_VERSION) + + "|cuda:" + std::to_string(getCudaVersion()) + "|nvrtc:" + nvrtc_tag + + "|arch:" + arch_tag; + std::string program_hash = hashString(fingerprint); - code.append(JitHelper::loadSourceFile(source)); - // Apply the substitutions - for (auto& subst : substitutions) { - code = std::regex_replace(code, std::regex(subst.first), subst.second); + static std::once_flag cache_dir_init_flag; + std::call_once(cache_dir_init_flag, []() { CACHE_DIR = resolveCacheDir(); }); + const auto program_dir = CACHE_DIR / program_hash; + auto storage = std::make_shared(code, flags); + storage->programHash = program_hash; + storage->cacheDir = program_dir; + storage->device = device; + storage->archTag = arch_tag; + + std::error_code ec; + std::filesystem::create_directories(storage->cacheDir, ec); + std::ofstream fp_out(storage->cacheDir / "fingerprint.txt", std::ios::trunc); + if (fp_out) { + fp_out << program_hash << "\n" << fingerprint; } - if (kcache == nullptr) - kcache = new deme::jit::ProgramCache(); + return CachedProgram(storage); +} - // Detect GPU architecture and add appropriate flags - std::string arch_flag; - { - int dev = 0; - cudaDeviceProp prop; - memset(&prop, 0, sizeof(prop)); - if (cudaGetDevice(&dev) == cudaSuccess && cudaGetDeviceProperties(&prop, dev) == cudaSuccess) { -#if defined(USE_HIP) - // HIP: use gcnArchName (e.g., "gfx90a") - std::string detected_arch = prop.gcnArchName; - // Strip any suffix after the base arch name (e.g., "gfx90a:sramecc+:xnack-" -> "gfx90a") - size_t colon_pos = detected_arch.find(':'); - if (colon_pos != std::string::npos) { - detected_arch = detected_arch.substr(0, colon_pos); - } - arch_flag = "--gpu-architecture=" + detected_arch; -#else - if (prop.major > 0 && prop.minor >= 0) { - std::string detected_arch = "compute_" + std::to_string(prop.major) + std::to_string(prop.minor); - arch_flag = "-arch=" + detected_arch; +std::string JitHelper::hashString(const std::string& in) { + uint64_t hash = kFNVOffset; + for (unsigned char c : in) { + hash ^= c; + hash *= kFNVPrime; + } + return toHex(hash); +} + +std::string JitHelper::toHex(uint64_t value) { + std::ostringstream oss; + oss << std::hex << std::setw(16) << std::setfill('0') << value; + return oss.str(); +} + +std::filesystem::path JitHelper::resolveCacheDir() { + if (const char* env = std::getenv("DEME_JIT_CACHE_DIR")) { + return std::filesystem::path(env); + } + // Prefer to keep cache alongside other runtime data in the build tree + std::filesystem::path default_path = DEMERuntimeDataHelper::data_path / "jit_cache"; + if (std::filesystem::exists(DEMERuntimeDataHelper::data_path)) { + return default_path; + } + return std::filesystem::temp_directory_path() / "dem-jit"; +} +JitHelper::CachedProgram::CachedProgram(std::shared_ptr storage) : m_storage(std::move(storage)) {} +JitHelper::CachedProgram::Kernel JitHelper::CachedProgram::kernel(const std::string& name, + std::vector options) const { + return Kernel(m_storage, name, std::move(options)); +} +JitHelper::CachedProgram::ProgramStorage::ProgramStorage(std::string code_in, std::vector flags_in) + : code(std::move(code_in)), flags(std::move(flags_in)) {} +JitHelper::CachedProgram::Kernel::Kernel(std::shared_ptr storage, + std::string name, + std::vector options) + : m_storage(std::move(storage)), m_name(std::move(name)), m_options(std::move(options)) {} + +std::shared_ptr JitHelper::CachedProgram::Kernel::getKernelInstantiation( + const std::vector& template_args) const { + const std::string template_suffix = + template_args.empty() ? std::string() : jitify::reflection::reflect_template(template_args); + + std::vector options_sorted = m_options; + std::sort(options_sorted.begin(), options_sorted.end()); + const std::string options_sig = jitify::reflection::reflect_list(options_sorted); + + auto make_key = [&]() { + const auto [nvrtc_major, nvrtc_minor] = getNvrtcVersion(); + const std::string key_material = m_storage->programHash + "|" + m_name + "|" + template_suffix + "|" + + options_sig + "|cuda:" + std::to_string(getCudaVersion()) + + "|nvrtc:" + std::to_string(nvrtc_major) + "." + std::to_string(nvrtc_minor) + + "|api:" + std::to_string(DEME_API_VERSION) + "|" + m_storage->archTag; + return JitHelper::hashString(key_material); + }; + std::string key = make_key(); + std::filesystem::path cache_file = m_storage->cacheDir / (sanitizeFilename(m_name) + "_" + key + ".jit"); + std::lock_guard storage_lock(m_storage->mutex); + if (auto it = m_storage->kernelCache.find(key); it != m_storage->kernelCache.end()) { + return it->second; + } + + std::shared_ptr inst; + if (std::filesystem::exists(cache_file)) { + std::ifstream input(cache_file, std::ios::binary); + if (input) { + std::stringstream buffer; + buffer << input.rdbuf(); + try { + inst = std::make_shared( + jitify::experimental::KernelInstantiation::deserialize(buffer.str())); + } catch (const std::exception&) { + inst.reset(); } -#endif } } + if (!inst) { // Compile if changed and not already there and make user aware + DEME_INFO("jit-compiling for %s ...", m_name.c_str()); + if (!m_storage->program) { + try { + // With an unresolved architecture, first allow Jitify to auto-detect it. + m_storage->program = std::make_unique( + m_storage->code, std::vector(), m_storage->flags); + } catch (const std::exception& error) { + if (m_storage->archTag != "auto" || !isArchitectureError(error)) { + throw; + } - // If device detection failed, use fallback - if (arch_flag.empty()) { -#if defined(USE_HIP) - const char* env_arch = std::getenv("DEME_DEFAULT_HIP_ARCH"); - const std::string fallback_arch = - (env_arch != nullptr && env_arch[0] != '\0') ? std::string(env_arch) : DEME_DEFAULT_HIP_ARCH_STR; - arch_flag = "--gpu-architecture=" + fallback_arch; -#else - const char* env_arch = std::getenv("DEME_DEFAULT_CUDA_ARCH"); - const std::string fallback_arch = - (env_arch != nullptr && env_arch[0] != '\0') ? std::string(env_arch) : DEME_DEFAULT_CUDA_ARCH_STR; - arch_flag = "-arch=" + fallback_arch; -#endif + const char* configured_arch = std::getenv("DEME_DEFAULT_CUDA_ARCH"); + const std::string fallback_arch = configured_arch && configured_arch[0] != '\0' + ? std::string(configured_arch) + : DEME_DEFAULT_CUDA_ARCH_STR; + m_storage->flags.push_back("-arch=" + fallback_arch); + m_storage->archTag = fallback_arch; + // The resolved fallback architecture must participate in both the program directory and kernel key; + // otherwise binaries for different GPU targets could collide on disk. + m_storage->programHash = JitHelper::hashString(m_storage->programHash + "|arch:" + fallback_arch); + m_storage->cacheDir = JitHelper::CACHE_DIR / m_storage->programHash; + key = make_key(); + cache_file = m_storage->cacheDir / (sanitizeFilename(m_name) + "_" + key + ".jit"); + std::error_code cache_error; + std::filesystem::create_directories(m_storage->cacheDir, cache_error); + std::ofstream fingerprint_out(m_storage->cacheDir / "fingerprint.txt", std::ios::trunc); + if (fingerprint_out) { + fingerprint_out << m_storage->programHash << "\narch:" << fallback_arch; + } + try { + m_storage->program = std::make_unique( + m_storage->code, std::vector(), m_storage->flags); + } catch (const std::exception& fallback_error) { + throw std::runtime_error("Jitify compilation failed with fallback architecture '" + fallback_arch + + "': " + fallback_error.what()); + } + } + } + auto kernel = m_storage->program->kernel(m_name, m_options); + inst = std::make_shared(kernel, template_args); + std::error_code ec; + std::filesystem::create_directories(cache_file.parent_path(), ec); + std::ofstream output(cache_file, std::ios::binary | std::ios::trunc); + if (output) { + output << inst->serialize(); + } } + m_storage->kernelCache[key] = inst; + return inst; +} - std::vector final_flags = flags; - final_flags.push_back(arch_flag); - - // Use the unified JitKernel abstraction - no headers needed for our usage - std::vector> headers; - return kcache->program(name, code, headers, final_flags); +JitHelper::CachedProgram::Kernel::KernelInstantiation JitHelper::CachedProgram::Kernel::instantiate( + std::vector template_args) const { + return KernelInstantiation(getKernelInstantiation(template_args)); } + +JitHelper::CachedProgram::Kernel::KernelInstantiation::KernelInstantiation( + std::shared_ptr impl) + : m_impl(std::move(impl)) {} diff --git a/src/core/utils/JitHelper.h b/src/core/utils/JitHelper.h index c6f79e3c..7ffc6846 100644 --- a/src/core/utils/JitHelper.h +++ b/src/core/utils/JitHelper.h @@ -7,12 +7,21 @@ #define DEME_JIT_HELPER_H #include -#include +#include #include #include #include +#include +#include +#include +#include +#include -#include "JitKernel.h" +#include +#include +#include + +#include "CudaDebugSync.hpp" #if defined(_WIN32) || defined(_WIN64) #undef max @@ -22,6 +31,8 @@ class JitHelper { public: + class CachedProgram; + class Header { public: Header(const std::filesystem::path& sourcefile); @@ -32,7 +43,7 @@ class JitHelper { std::string _source; }; - static deme::jit::Program buildProgram( + static CachedProgram buildProgram( const std::string& name, const std::filesystem::path& source, std::unordered_map substitutions = std::unordered_map(), @@ -40,9 +51,12 @@ class JitHelper { static std::filesystem::path KERNEL_DIR; static std::filesystem::path KERNEL_INCLUDE_DIR; + static std::filesystem::path CACHE_DIR; private: - static deme::jit::ProgramCache* kcache; + static std::string hashString(const std::string& in); + static std::string toHex(uint64_t value); + static std::filesystem::path resolveCacheDir(); inline static std::string loadSourceFile(const std::filesystem::path& sourcefile) { std::string code; @@ -55,4 +69,171 @@ class JitHelper { }; }; +class JitHelper::CachedProgram { + public: + class Kernel; + + Kernel kernel(const std::string& name, std::vector options = {}) const; + + const std::string& key() const { return m_storage->programHash; } + + private: + struct ProgramStorage { + ProgramStorage(std::string code_in, std::vector flags_in); + std::unique_ptr program; + std::string code; + std::vector flags; + std::string programHash; + std::filesystem::path cacheDir; + int device = 0; + std::string archTag; + std::unordered_map> kernelCache; + std::mutex mutex; + }; + + std::shared_ptr m_storage; + explicit CachedProgram(std::shared_ptr storage); + + friend class JitHelper; +}; + +class JitHelper::CachedProgram::Kernel { + public: + class KernelInstantiation; + + KernelInstantiation instantiate(std::vector template_args = {}) const; + + template + KernelInstantiation instantiate(TemplateArgs... targs) const; + + private: + std::shared_ptr m_storage; + std::string m_name; + std::vector m_options; + + Kernel(std::shared_ptr storage, std::string name, std::vector options); + + std::shared_ptr getKernelInstantiation( + const std::vector& template_args) const; + + friend class CachedProgram; +}; + +class JitHelper::CachedProgram::Kernel::KernelInstantiation { + public: + class KernelLauncher; + + KernelLauncher configure(dim3 grid, dim3 block, unsigned int smem = 0, cudaStream_t stream = 0) const; + KernelLauncher configure_1d_max_occupancy(int max_block_size = 0, + unsigned int smem = 0, + CUoccupancyB2DSize smem_callback = 0, + cudaStream_t stream = 0, + unsigned int flags = 0) const; + + CUdeviceptr get_global_ptr(const char* name, size_t* size = nullptr) const { + return m_impl->get_global_ptr(name, size); + } + + template + CUresult get_global_array(const char* name, T* data, size_t count, CUstream stream = 0) const { + return m_impl->get_global_array(name, data, count, stream); + } + + template + CUresult get_global_value(const char* name, T* value, CUstream stream = 0) const { + return get_global_array(name, value, 1, stream); + } + + template + CUresult set_global_array(const char* name, const T* data, size_t count, CUstream stream = 0) const { + return m_impl->set_global_array(name, data, count, stream); + } + + template + CUresult set_global_value(const char* name, const T& value, CUstream stream = 0) const { + return set_global_array(name, &value, 1, stream); + } + + const std::string& mangled_name() const { return m_impl->mangled_name(); } + const std::string& ptx() const { return m_impl->ptx(); } + + const std::vector& link_files() const { return m_impl->link_files(); } + const std::vector& link_paths() const { return m_impl->link_paths(); } + + private: + std::shared_ptr m_impl; + + KernelInstantiation(std::shared_ptr impl); + + friend class Kernel; +}; + +class JitHelper::CachedProgram::Kernel::KernelInstantiation::KernelLauncher { + public: + KernelLauncher(std::shared_ptr inst, + jitify::experimental::KernelLauncher launcher, + cudaStream_t stream) + : m_inst(std::move(inst)), m_launcher(std::move(launcher)), m_stream(stream) {} + + CUresult launch(std::vector arg_ptrs = {}, std::vector arg_types = {}) const { + CUresult result = m_launcher.launch(std::move(arg_ptrs), std::move(arg_types)); + if (result == CUDA_SUCCESS) { + const cudaError_t sync_result = deme::DebugSyncStream(m_stream); + if (sync_result != cudaSuccess) { + throw std::runtime_error(std::string("GPU error after JIT kernel launch: ") + + cudaGetErrorString(sync_result)); + } + } + return result; + } + + template + CUresult launch(const ArgTypes&... args) const { + return launch(std::vector({(void*)&args...}), {jitify::reflection::reflect()...}); + } + + void safe_launch(std::vector arg_ptrs = {}, std::vector arg_types = {}) const { + m_launcher.safe_launch(std::move(arg_ptrs), std::move(arg_types)); + const cudaError_t sync_result = deme::DebugSyncStream(m_stream); + if (sync_result != cudaSuccess) { + throw std::runtime_error(std::string("GPU error after JIT kernel launch: ") + + cudaGetErrorString(sync_result)); + } + } + + template + void safe_launch(const ArgTypes&... args) const { + safe_launch(std::vector({(void*)&args...}), {jitify::reflection::reflect()...}); + } + + private: + std::shared_ptr m_inst; + jitify::experimental::KernelLauncher m_launcher; + cudaStream_t m_stream; +}; + +template +inline JitHelper::CachedProgram::Kernel::KernelInstantiation JitHelper::CachedProgram::Kernel::instantiate( + TemplateArgs... targs) const { + return instantiate(std::vector({jitify::reflection::reflect(targs)...})); +} + +inline JitHelper::CachedProgram::Kernel::KernelInstantiation::KernelLauncher +JitHelper::CachedProgram::Kernel::KernelInstantiation::configure(dim3 grid, + dim3 block, + unsigned int smem, + cudaStream_t stream) const { + return KernelLauncher(m_impl, m_impl->configure(grid, block, smem, stream), stream); +} + +inline JitHelper::CachedProgram::Kernel::KernelInstantiation::KernelLauncher +JitHelper::CachedProgram::Kernel::KernelInstantiation::configure_1d_max_occupancy(int max_block_size, + unsigned int smem, + CUoccupancyB2DSize smem_callback, + cudaStream_t stream, + unsigned int flags) const { + return KernelLauncher( + m_impl, m_impl->configure_1d_max_occupancy(max_block_size, smem, smem_callback, stream, flags), stream); +} + #endif diff --git a/src/core/utils/JitKernel.h b/src/core/utils/JitKernel.h deleted file mode 100644 index 1b5bb7d5..00000000 --- a/src/core/utils/JitKernel.h +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// Copyright (c) 2026, Advanced Micro Devices, Inc. -// -// SPDX-License-Identifier: BSD-3-Clause - -// Unified runtime kernel compilation API for DEM-Engine. -// Abstracts over jitify v1 (CUDA/NVRTC) and hiprtc (ROCm), enabling both -// platforms to use the same fluent kernel launch syntax. - -#ifndef DEME_JIT_KERNEL_H -#define DEME_JIT_KERNEL_H - -#include -#include -#include -#include -#include - -#ifdef USE_HIP - #include -using gpuStream_t = hipStream_t; -#else - #include -using gpuStream_t = cudaStream_t; -#endif - -namespace deme { -namespace jit { - -class Kernel; -class Program; - -// Fluent kernel launch builder - matches existing jitify v1 usage pattern. -// Usage: program->kernel("name")().instantiate().configure(...).launch(args...) -class KernelLauncher { - public: - // For kernels without template parameters - KernelLauncher& instantiate(); - - // For kernels with template parameters. - // type_names: comma-separated type names as they appear in the kernel template. - // On hiprtc, these get baked into the mangled kernel name. - KernelLauncher& instantiate(const std::string& type_names); - - // Configure launch parameters - KernelLauncher& configure(dim3 grid, dim3 block, size_t shared_mem = 0, gpuStream_t stream = 0); - - // Launch with arguments - variadic template forwards to backend - // The implementation is in the .inl file for HIP, or calls jitify for CUDA - template - void launch(Args... args); - - // Non-templated launch for backend implementations - void launchRaw(void** kernel_args, size_t num_args); - - // Kernel can create KernelLaunchers - explicit KernelLauncher(Kernel* k); - - private: - friend class Kernel; - - Kernel* kernel_; - dim3 grid_{1, 1, 1}; - dim3 block_{1, 1, 1}; - size_t shared_mem_ = 0; - gpuStream_t stream_ = 0; - std::string instantiation_; - bool instantiated_ = false; -}; - -// Handle to a kernel function within a compiled program. -// Created by Program::kernel(name). -class Kernel { - public: - ~Kernel(); - Kernel(Kernel&&) noexcept; - Kernel& operator=(Kernel&&) noexcept; - - // Deleted copy operations - kernel handles are move-only - Kernel(const Kernel&) = delete; - Kernel& operator=(const Kernel&) = delete; - - // Start a launch chain. Returns a KernelLauncher for fluent chaining. - KernelLauncher operator()(); - - // Direct instantiate() method to match jitify v1 API. - // Usage: program->kernel("name").instantiate().configure(...).launch(...) - KernelLauncher& instantiate(); - KernelLauncher& instantiate(const std::string& type_names); - - // Get kernel name (for debugging/error messages) - const std::string& name() const; - - private: - friend class Program; - friend class KernelLauncher; - Kernel(); - - struct Impl; - std::unique_ptr impl_; - std::unique_ptr launcher_; // For instantiate() return reference -}; - -// A compiled GPU program containing one or more kernels. -// Created by ProgramCache::program(). -class Program { - public: - ~Program(); - Program(Program&&) noexcept; - Program& operator=(Program&&) noexcept; - - // Deleted copy operations - programs are move-only - Program(const Program&) = delete; - Program& operator=(const Program&) = delete; - - // Get a kernel by name. The kernel can then be launched via: - // program->kernel("name")().instantiate().configure(...).launch(...) - Kernel kernel(const std::string& name); - - // Check if program is valid (compiled successfully) - explicit operator bool() const; - - private: - friend class ProgramCache; - Program(); - - struct Impl; - std::unique_ptr impl_; -}; - -// Caches compiled programs to avoid recompilation. -// Thread-safe: multiple threads can call program() concurrently. -class ProgramCache { - public: - ProgramCache(); - ~ProgramCache(); - - // Compile a program. - // - name: identifier for the program (typically the kernel file name) - // - source: kernel source code (prepended with name for jitify compatibility) - // - headers: additional header code as (name, source) pairs - // - flags: compiler flags (-I, -D, -arch, etc.) - // - // Returns a new Program by value. Caching is not performed at this level; - // callers typically cache via shared_ptr at the application level. - Program program(const std::string& name, - const std::string& source, - const std::vector>& headers = {}, - const std::vector& flags = {}); - - // Clear all cached programs. Invalidates all Program references. - void clear(); - - private: - struct Impl; - std::unique_ptr impl_; -}; - -// Helper to build compiler flags in a platform-agnostic way. -class CompilerFlags { - public: - // Add an include path (-I) - CompilerFlags& include(const std::string& path); - - // Add a macro definition (-D) - CompilerFlags& define(const std::string& macro, const std::string& value = ""); - - // Set target architecture (compute_75 for CUDA, gfx90a for HIP) - // The flag format is handled automatically per platform. - CompilerFlags& arch(const std::string& arch); - - // Add a raw flag (passed through unchanged) - CompilerFlags& flag(const std::string& raw_flag); - - // Build the final flags vector - std::vector build() const; - - private: - std::vector flags_; -}; - -} // namespace jit -} // namespace deme - -// Include the template implementation (must be visible at all call sites) -#include "JitKernel.inl" - -#endif // DEME_JIT_KERNEL_H diff --git a/src/core/utils/JitKernel.inl b/src/core/utils/JitKernel.inl deleted file mode 100644 index 6984dd94..00000000 --- a/src/core/utils/JitKernel.inl +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// Copyright (c) 2026, Advanced Micro Devices, Inc. -// -// SPDX-License-Identifier: BSD-3-Clause - -// Template implementation for JitKernel.h -// This file is included at the end of JitKernel.h - -#ifndef DEME_JIT_KERNEL_INL -#define DEME_JIT_KERNEL_INL - -#include -#include -#include - -namespace deme { -namespace jit { - -namespace detail { - -// Helper to pack a single argument by copying to a buffer -template -inline void packArg(std::vector& args, std::vector>& storage, T arg) { - auto buf = std::make_unique(sizeof(T)); - std::memcpy(buf.get(), &arg, sizeof(T)); - args.push_back(buf.get()); - storage.push_back(std::move(buf)); -} - -// Recursive pack for variadic args -template -inline void packArgs(std::vector& args, std::vector>& storage, T first, Rest... rest) { - packArg(args, storage, first); - if constexpr (sizeof...(rest) > 0) { - packArgs(args, storage, rest...); - } -} - -// Base case: no args -inline void packArgs(std::vector&, std::vector>&) {} - -} // namespace detail - -template -void KernelLauncher::launch(Args... args) { - // Pack each argument into a heap buffer and collect pointers to them, then - // forward to the backend's non-template raw launcher (hipModuleLaunchKernel - // on ROCm, jitify's cuLaunchKernel wrapper on CUDA). Both runtimes take the - // same "array of pointers to argument values" convention. - std::vector kernel_args; - std::vector> arg_storage; - - if constexpr (sizeof...(args) > 0) { - detail::packArgs(kernel_args, arg_storage, args...); - } - - launchRaw(kernel_args.empty() ? nullptr : kernel_args.data(), kernel_args.size()); -} - -} // namespace jit -} // namespace deme - -#endif // DEME_JIT_KERNEL_INL diff --git a/src/core/utils/JitKernel_cuda.cpp b/src/core/utils/JitKernel_cuda.cpp deleted file mode 100644 index 69cce9d1..00000000 --- a/src/core/utils/JitKernel_cuda.cpp +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// Copyright (c) 2026, Advanced Micro Devices, Inc. -// -// SPDX-License-Identifier: BSD-3-Clause - -// CUDA backend for JitKernel using jitify v1 (NVRTC) - -#ifndef USE_HIP - - #include "JitKernel.h" - - // Jitify is in the build tree at ${CMAKE_BINARY_DIR}/src/jitify/ - // Include path is added by CMake target_include_directories - #include - #include - #include - #include - -namespace deme { -namespace jit { - -// ============================================================================ -// Kernel implementation -// ============================================================================ - -struct Kernel::Impl { - std::string name; - jitify::Program* program = nullptr; // Non-owning reference to parent program - jitify::KernelInstantiation instantiation; - bool has_instantiation = false; -}; - -Kernel::Kernel() : impl_(std::make_unique()) {} -Kernel::~Kernel() = default; -Kernel::Kernel(Kernel&&) noexcept = default; -Kernel& Kernel::operator=(Kernel&&) noexcept = default; - -KernelLauncher Kernel::operator()() { - return KernelLauncher(this); -} - -KernelLauncher& Kernel::instantiate() { - launcher_ = std::make_unique(this); - return launcher_->instantiate(); -} - -KernelLauncher& Kernel::instantiate(const std::string& type_names) { - launcher_ = std::make_unique(this); - return launcher_->instantiate(type_names); -} - -const std::string& Kernel::name() const { - return impl_->name; -} - -// ============================================================================ -// KernelLauncher implementation -// ============================================================================ - -KernelLauncher::KernelLauncher(Kernel* k) : kernel_(k) {} - -KernelLauncher& KernelLauncher::instantiate() { - kernel_->impl_->instantiation = kernel_->impl_->program->kernel(kernel_->impl_->name).instantiate(); - kernel_->impl_->has_instantiation = true; - instantiated_ = true; - return *this; -} - -KernelLauncher& KernelLauncher::instantiate(const std::string& type_names) { - // Parse comma-separated type names and instantiate with them. - // Jitify uses variadic templates for instantiation, but we can pass - // the type string directly to the kernel() call via name mangling. - // For CUDA/jitify, we use the instantiate(type_name) overload. - kernel_->impl_->instantiation = kernel_->impl_->program->kernel(kernel_->impl_->name).instantiate({type_names}); - kernel_->impl_->has_instantiation = true; - instantiated_ = true; - instantiation_ = type_names; - return *this; -} - -KernelLauncher& KernelLauncher::configure(dim3 grid, dim3 block, size_t shared_mem, gpuStream_t stream) { - grid_ = grid; - block_ = block; - shared_mem_ = shared_mem; - stream_ = stream; - return *this; -} - -// ============================================================================ -// Program implementation -// ============================================================================ - -struct Program::Impl { - jitify::Program jit_program; -}; - -Program::Program() : impl_(std::make_unique()) {} -Program::~Program() = default; -Program::Program(Program&&) noexcept = default; -Program& Program::operator=(Program&&) noexcept = default; - -Program::operator bool() const { - return impl_ != nullptr; -} - -Kernel Program::kernel(const std::string& name) { - Kernel k; - k.impl_->name = name; - k.impl_->program = &impl_->jit_program; - return k; -} - -// ============================================================================ -// ProgramCache implementation -// ============================================================================ - -struct ProgramCache::Impl { - jitify::JitCache cache; -}; - -ProgramCache::ProgramCache() : impl_(std::make_unique()) {} -ProgramCache::~ProgramCache() = default; - -Program ProgramCache::program(const std::string& name, - const std::string& source, - const std::vector>& headers, - const std::vector& flags) { - // jitify::JitCache handles caching internally, so we just wrap the result - (void)name; // Name is embedded in source for jitify - - // Extract header sources (jitify v1 takes just the source strings) - std::vector header_sources; - for (const auto& h : headers) { - header_sources.push_back(h.second); - } - - Program prog; - prog.impl_->jit_program = impl_->cache.program(source, header_sources, flags); - return prog; -} - -void ProgramCache::clear() { - // No-op: jitify::JitCache doesn't expose a clear method -} - -// ============================================================================ -// CompilerFlags implementation -// ============================================================================ - -CompilerFlags& CompilerFlags::include(const std::string& path) { - flags_.push_back("-I" + path); - return *this; -} - -CompilerFlags& CompilerFlags::define(const std::string& macro, const std::string& value) { - if (value.empty()) { - flags_.push_back("-D" + macro); - } else { - flags_.push_back("-D" + macro + "=" + value); - } - return *this; -} - -CompilerFlags& CompilerFlags::arch(const std::string& arch) { - flags_.push_back("-arch=" + arch); - return *this; -} - -CompilerFlags& CompilerFlags::flag(const std::string& raw_flag) { - flags_.push_back(raw_flag); - return *this; -} - -std::vector CompilerFlags::build() const { - return flags_; -} - -// ============================================================================ -// KernelLauncher::launchRaw() - launch via jitify's cuLaunchKernel wrapper -// ============================================================================ - -// The templated launch() (in JitKernel.inl) packs each argument into a buffer -// and hands the resulting array of pointers here, identically to the HIP -// backend. jitify's launch takes the same "vector of pointers to argument -// values" convention, so we forward straight to it. -void KernelLauncher::launchRaw(void** kernel_args, size_t num_args) { - if (!kernel_->impl_->has_instantiation) { - throw std::runtime_error("Kernel '" + kernel_->impl_->name + "' not instantiated before launch"); - } - - // Skip launch if any grid dimension is zero (no work); keeps parity with the - // HIP backend, where a zero grid is an error rather than a silent no-op. - if (grid_.x == 0 || grid_.y == 0 || grid_.z == 0) { - return; - } - - std::vector arg_ptrs(kernel_args, kernel_args + num_args); - kernel_->impl_->instantiation.configure(grid_, block_, shared_mem_, stream_).safe_launch(arg_ptrs); -} - -} // namespace jit -} // namespace deme - -#endif // !USE_HIP diff --git a/src/core/utils/JitKernel_hip.cpp b/src/core/utils/JitKernel_hip.cpp deleted file mode 100644 index 88a6861a..00000000 --- a/src/core/utils/JitKernel_hip.cpp +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// Copyright (c) 2026, Advanced Micro Devices, Inc. -// -// SPDX-License-Identifier: BSD-3-Clause - -// HIP backend for JitKernel using hiprtc. -// -// Key design: hiprtc requires all name expressions (kernel names to resolve) -// be registered BEFORE compilation via hiprtcAddNameExpression(). Since the -// DEM-Engine API calls buildProgram() first, then kernel().instantiate() later, -// we use lazy compilation: defer hiprtcCompileProgram until the first kernel -// launch, by which point all kernel+instantiation requests have been collected. - -#ifdef USE_HIP - - #include "JitKernel.h" - #include - #include - #include - #include - #include - #include - #include - #include - #include - -namespace deme { -namespace jit { - -namespace { - -void checkHiprtc(hiprtcResult result, const char* msg) { - if (result != HIPRTC_SUCCESS) { - throw std::runtime_error(std::string(msg) + ": " + hiprtcGetErrorString(result)); - } -} - -void checkHip(hipError_t result, const char* msg) { - if (result != hipSuccess) { - throw std::runtime_error(std::string(msg) + ": " + hipGetErrorString(result)); - } -} - -// Build the name expression string for hiprtcAddNameExpression. -// For a templated kernel like "modifyComponents" instantiated with "deme::DEMDataKT", -// the expression is "modifyComponents". -std::string buildNameExpression(const std::string& name, const std::string& instantiation) { - if (instantiation.empty()) { - return name; - } - return name + "<" + instantiation + ">"; -} - -} // namespace - -// Forward declaration of ProgramImpl so Kernel::Impl can reference it -struct ProgramImpl { - // Source and compilation options (stored for lazy/deferred compilation) - std::string name; - std::string source; - std::vector> headers; - std::vector flags; - - // Kernel name expressions that have been requested - // Key: name expression (e.g., "modifyComponents") - // Value: lowered/mangled name after compilation - std::unordered_map loweredNames; - - // Name expressions pending addition (requested but not yet compiled) - std::unordered_set pendingNameExpressions; - - // Compiled module (nullptr until first compilation) - hipModule_t module = nullptr; - - // Compiled code buffer (kept for potential recompilation) - std::vector codeBuffer; - - // Whether we need to recompile (new name expressions added since last compile) - bool needsRecompile = true; - - std::mutex mutex; - - ~ProgramImpl() { - if (module) { - (void)hipModuleUnload(module); - } - } - - // Compile or recompile with all registered name expressions - void ensureCompiled() { - std::lock_guard lock(mutex); - - if (!needsRecompile && module != nullptr) { - return; // Already compiled with current name expressions - } - - // Unload previous module if any - if (module) { - (void)hipModuleUnload(module); - module = nullptr; - } - - // Create hiprtc program - hiprtcProgram prog; - std::vector header_names; - std::vector header_sources; - for (const auto& h : headers) { - header_names.push_back(h.first.c_str()); - header_sources.push_back(h.second.c_str()); - } - - checkHiprtc(hiprtcCreateProgram(&prog, source.c_str(), name.c_str(), static_cast(header_sources.size()), - header_sources.data(), header_names.data()), - "hiprtcCreateProgram"); - - // Register all name expressions (pending ones plus any previously resolved) - std::vector allNameExpressions; - for (const auto& pending : pendingNameExpressions) { - allNameExpressions.push_back(pending); - } - for (const auto& resolved : loweredNames) { - // Re-register previously resolved names (needed for recompilation) - if (pendingNameExpressions.find(resolved.first) == pendingNameExpressions.end()) { - allNameExpressions.push_back(resolved.first); - } - } - - for (const auto& expr : allNameExpressions) { - hiprtcResult res = hiprtcAddNameExpression(prog, expr.c_str()); - if (res != HIPRTC_SUCCESS) { - hiprtcDestroyProgram(&prog); - throw std::runtime_error("hiprtcAddNameExpression failed for '" + expr + - "': " + hiprtcGetErrorString(res)); - } - } - - // Convert flags to const char* array - std::vector options; - for (const auto& f : flags) { - options.push_back(f.c_str()); - } - - // Compile - hiprtcResult compileResult = hiprtcCompileProgram(prog, static_cast(options.size()), options.data()); - - if (compileResult != HIPRTC_SUCCESS) { - size_t logSize; - hiprtcGetProgramLogSize(prog, &logSize); - std::string log(logSize, '\0'); - hiprtcGetProgramLog(prog, log.data()); - hiprtcDestroyProgram(&prog); - throw std::runtime_error("hiprtc compilation failed for '" + name + "':\n" + log); - } - - // Get lowered names for all expressions - for (const auto& expr : allNameExpressions) { - const char* lowered = nullptr; - hiprtcResult res = hiprtcGetLoweredName(prog, expr.c_str(), &lowered); - if (res != HIPRTC_SUCCESS) { - hiprtcDestroyProgram(&prog); - throw std::runtime_error("hiprtcGetLoweredName failed for '" + expr + - "': " + hiprtcGetErrorString(res)); - } - loweredNames[expr] = std::string(lowered); - } - - // Clear pending set - all are now resolved - pendingNameExpressions.clear(); - - // Get compiled code - size_t codeSize; - checkHiprtc(hiprtcGetCodeSize(prog, &codeSize), "hiprtcGetCodeSize"); - codeBuffer.resize(codeSize); - checkHiprtc(hiprtcGetCode(prog, codeBuffer.data()), "hiprtcGetCode"); - - hiprtcDestroyProgram(&prog); - - // Load module - checkHip(hipModuleLoadData(&module, codeBuffer.data()), "hipModuleLoadData"); - - needsRecompile = false; - } - - // Register a name expression (kernel + optional instantiation) - // Returns the lowered name, triggering compilation if needed - std::string getLoweredName(const std::string& nameExpr) { - { - std::lock_guard lock(mutex); - - // Check if already resolved - auto it = loweredNames.find(nameExpr); - if (it != loweredNames.end() && !needsRecompile) { - return it->second; - } - - // Add to pending if not already there - if (loweredNames.find(nameExpr) == loweredNames.end()) { - pendingNameExpressions.insert(nameExpr); - needsRecompile = true; - } - } - - // Compile (releases and re-acquires lock internally) - ensureCompiled(); - - // Now look up the lowered name - std::lock_guard lock(mutex); - auto it = loweredNames.find(nameExpr); - if (it == loweredNames.end()) { - throw std::runtime_error("Internal error: lowered name not found for '" + nameExpr + "'"); - } - return it->second; - } -}; - -// ============================================================================ -// Kernel implementation -// ============================================================================ - -struct Kernel::Impl { - std::string name; - std::string instantiation; - hipFunction_t func = nullptr; - ProgramImpl* program = nullptr; // Non-owning reference to parent program -}; - -Kernel::Kernel() : impl_(std::make_unique()) {} -Kernel::~Kernel() = default; -Kernel::Kernel(Kernel&&) noexcept = default; -Kernel& Kernel::operator=(Kernel&&) noexcept = default; - -KernelLauncher Kernel::operator()() { - return KernelLauncher(this); -} - -KernelLauncher& Kernel::instantiate() { - launcher_ = std::make_unique(this); - return launcher_->instantiate(); -} - -KernelLauncher& Kernel::instantiate(const std::string& type_names) { - launcher_ = std::make_unique(this); - return launcher_->instantiate(type_names); -} - -const std::string& Kernel::name() const { - return impl_->name; -} - -// ============================================================================ -// KernelLauncher implementation -// ============================================================================ - -KernelLauncher::KernelLauncher(Kernel* k) : kernel_(k) {} - -KernelLauncher& KernelLauncher::instantiate() { - return instantiate(""); -} - -KernelLauncher& KernelLauncher::instantiate(const std::string& type_names) { - kernel_->impl_->instantiation = type_names; - instantiation_ = type_names; - - if (kernel_->impl_->program) { - // Build the name expression and register it - std::string nameExpr = buildNameExpression(kernel_->impl_->name, type_names); - - // Get the lowered name (this triggers compilation if needed) - std::string loweredName = kernel_->impl_->program->getLoweredName(nameExpr); - - // Ensure module is compiled - kernel_->impl_->program->ensureCompiled(); - - // Now get the function handle - hipError_t err = - hipModuleGetFunction(&kernel_->impl_->func, kernel_->impl_->program->module, loweredName.c_str()); - if (err != hipSuccess) { - throw std::runtime_error("hipModuleGetFunction failed for '" + nameExpr + "' (lowered: " + loweredName + - "): " + hipGetErrorString(err)); - } - } - instantiated_ = true; - return *this; -} - -KernelLauncher& KernelLauncher::configure(dim3 grid, dim3 block, size_t shared_mem, gpuStream_t stream) { - grid_ = grid; - block_ = block; - shared_mem_ = shared_mem; - stream_ = stream; - return *this; -} - -// ============================================================================ -// Program implementation - wraps ProgramImpl -// ============================================================================ - -struct Program::Impl : public ProgramImpl {}; - -Program::Program() : impl_(std::make_unique()) {} -Program::~Program() = default; -Program::Program(Program&&) noexcept = default; -Program& Program::operator=(Program&&) noexcept = default; - -Program::operator bool() const { - return impl_ != nullptr; -} - -Kernel Program::kernel(const std::string& name) { - Kernel k; - k.impl_->name = name; - k.impl_->program = impl_.get(); - return k; -} - -// ============================================================================ -// ProgramCache implementation -// ============================================================================ - -struct ProgramCache::Impl { - // No caching at this level; callers cache via shared_ptr -}; - -ProgramCache::ProgramCache() : impl_(std::make_unique()) {} -ProgramCache::~ProgramCache() = default; - -Program ProgramCache::program(const std::string& name, - const std::string& source, - const std::vector>& headers, - const std::vector& flags) { - // Create a new Program with deferred compilation (lazy compilation on first kernel use) - Program result; - result.impl_->name = name; - result.impl_->source = source; - result.impl_->headers = headers; - result.impl_->flags = flags; - result.impl_->needsRecompile = true; - return result; -} - -void ProgramCache::clear() { - // No-op: no caching at this level -} - -// ============================================================================ -// CompilerFlags implementation -// ============================================================================ - -CompilerFlags& CompilerFlags::include(const std::string& path) { - flags_.push_back("-I" + path); - return *this; -} - -CompilerFlags& CompilerFlags::define(const std::string& macro, const std::string& value) { - if (value.empty()) { - flags_.push_back("-D" + macro); - } else { - flags_.push_back("-D" + macro + "=" + value); - } - return *this; -} - -CompilerFlags& CompilerFlags::arch(const std::string& arch) { - flags_.push_back("--gpu-architecture=" + arch); - return *this; -} - -CompilerFlags& CompilerFlags::flag(const std::string& raw_flag) { - flags_.push_back(raw_flag); - return *this; -} - -std::vector CompilerFlags::build() const { - return flags_; -} - -// ============================================================================ -// KernelLauncher::launchRaw() - non-templated launch for HIP -// ============================================================================ - -void KernelLauncher::launchRaw(void** kernel_args, size_t num_args) { - if (!kernel_->impl_->func) { - throw std::runtime_error("Kernel function '" + kernel_->impl_->name + "' not resolved"); - } - - // Skip launch if grid has zero dimension (no work to do). - // CUDA may silently accept grid=0, but HIP returns hipErrorInvalidValue. - if (grid_.x == 0 || grid_.y == 0 || grid_.z == 0) { - return; - } - - hipError_t err = hipModuleLaunchKernel(kernel_->impl_->func, grid_.x, grid_.y, grid_.z, block_.x, block_.y, - block_.z, shared_mem_, stream_, kernel_args, nullptr); - - checkHip(err, ("hipModuleLaunchKernel failed for '" + kernel_->impl_->name + "'").c_str()); - (void)num_args; -} - -} // namespace jit -} // namespace deme - -#endif // USE_HIP diff --git a/src/core/utils/Logger.hpp b/src/core/utils/Logger.hpp new file mode 100644 index 00000000..c49bb81b --- /dev/null +++ b/src/core/utils/Logger.hpp @@ -0,0 +1,328 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef DEME_LOGGER_HPP +#define DEME_LOGGER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "BaseClasses.hpp" +#include "CudaDebugSync.hpp" +// #include "../../DEM/Defines.h" + +namespace deme { + +// Verbosity +typedef int verbosity_t; +const verbosity_t VERBOSITY_QUIET = 0; +const verbosity_t VERBOSITY_ERROR = 1; +const verbosity_t VERBOSITY_WARNING = 2; +const verbosity_t VERBOSITY_INFO = 3; +const verbosity_t VERBOSITY_METRIC = 4; +const verbosity_t VERBOSITY_DEBUG = 5; + +// ----------------------------- +// Logging types and structure +// ----------------------------- + +enum class MessageType { Info, Warning, Error, Status }; + +struct LogMessage { + MessageType type; + std::string source; + std::string message; + std::string file; + int line; + std::string identifier; +}; + +// ----------------------------- +// Logger and exception class (thread-safe, singleton) +// ----------------------------- + +class SolverException : public std::runtime_error { + public: + SolverException(const std::string& msg) : std::runtime_error(msg) {} +}; + +class Logger : private NonCopyable, public Singleton { + public: + void SetVerbosity(verbosity_t level) { + std::lock_guard lock(mutex_); + verbosity = level; + } + + void Log(MessageType type, const std::string& source, const std::string& msg, const std::string& file, int line) { + std::lock_guard lock(mutex_); + LogMessage Log{type, source, msg, file, line}; + logs.push_back(Log); + + if (should_print_immediately(type)) { + std::cout << format(Log) << std::endl; + } + } + + // snprintf version of logging with format arguments + template + std::string Logf(MessageType type, const char* func, const char* file, int line, const char* fmt, Args&&... args) { + constexpr size_t BUF_SIZE = 2048; + char buffer[BUF_SIZE]; + std::snprintf(buffer, BUF_SIZE, fmt, std::forward(args)...); + std::string message(buffer); + Log(type, func, message, file, line); + return message; + } + + // const char* version without format arguments (avoids -Wformat-security warning) + std::string Logf(MessageType type, const char* func, const char* file, int line, const char* message) { + Log(type, func, std::string(message), file, line); + return std::string(message); + } + + // std::string version of logging + std::string Logf(MessageType type, const char* func, const char* file, int line, const std::string& message) { + Log(type, func, message, file, line); + return message; + } + + // LogStatus with format arguments + template + void LogStatusf(const std::string& identifier, + const char* func, + const char* file, + int line, + const char* fmt, + Args&&... args) { + constexpr size_t BUF_SIZE = 2048; + char buffer[BUF_SIZE]; + std::snprintf(buffer, BUF_SIZE, fmt, std::forward(args)...); + LogStatus(identifier, func, std::string(buffer), file, line); + } + + // LogStatus without format arguments (avoids -Wformat-security warning) + void LogStatusf(const std::string& identifier, const char* func, const char* file, int line, const char* message) { + LogStatus(identifier, func, std::string(message), file, line); + } + + void LogStatus(const std::string& identifier, + const std::string& func, + const std::string& msg, + const std::string& file, + int line) { + std::lock_guard lock(mutex_); + + LogMessage Log{MessageType::Status, func, msg, file, line, identifier}; + status_messages[identifier] = Log; + + if (should_print_immediately(MessageType::Status)) { + std::cout << format(Log) << std::endl; + } + } + + void PrintAll(std::ostream& os = std::cout) const { + std::lock_guard lock(mutex_); + for (const auto& Log : logs) { + os << format(Log) << std::endl; + } + } + + void PrintWarningsAndErrors(std::ostream& os = std::cout) const { + std::lock_guard lock(mutex_); + for (const auto& Log : logs) { + if (Log.type == MessageType::Warning || Log.type == MessageType::Error) { + os << format(Log) << std::endl; + } + } + } + + void PrintStatusMessages(std::ostream& os = std::cout) const { + std::lock_guard lock(mutex_); + for (const auto& pair : status_messages) { + os << format(pair.second) << std::endl; + } + } + + bool HasErrors() const { + std::lock_guard lock(mutex_); + for (const auto& Log : logs) { + if (Log.type == MessageType::Error) + return true; + } + return false; + } + + bool HasWarnings() const { + std::lock_guard lock(mutex_); + for (const auto& Log : logs) { + if (Log.type == MessageType::Warning) + return true; + } + return false; + } + + void Clear() { + std::lock_guard lock(mutex_); + logs.clear(); + status_messages.clear(); + } + + verbosity_t GetVerbosity() const { return verbosity; } + + private: + friend class Singleton; // Allows Singleton base to construct it + + Logger() : verbosity(VERBOSITY_WARNING) {} + ~Logger() {} + + bool should_print_immediately(MessageType type) const { + switch (type) { + case MessageType::Error: + return verbosity >= VERBOSITY_ERROR; + case MessageType::Warning: + return verbosity >= VERBOSITY_WARNING; + case MessageType::Info: + return verbosity >= VERBOSITY_INFO; + case MessageType::Status: + return verbosity >= VERBOSITY_METRIC; + // Debug level will just immediately output all previous ones + default: + return false; + } + } + + std::string format(const LogMessage& Log) const { + std::ostringstream oss; + switch (Log.type) { + case MessageType::Error: + oss << "[ERROR] "; + break; + case MessageType::Warning: + oss << "[WARNING] "; + break; + case MessageType::Info: + oss << "[INFO] "; + break; + case MessageType::Status: + oss << "[STATUS] "; + break; + } + oss << Log.source << ": " << Log.message; + if (Log.type == MessageType::Error) + oss << " (" << Log.file << ":" << Log.line << ")"; + if (!Log.identifier.empty() && Log.type == MessageType::Status) + oss << " [id: " << Log.identifier << "]"; + return oss.str(); + } + + mutable std::mutex mutex_; + std::vector logs; + std::unordered_map status_messages; + verbosity_t verbosity; +}; + +// ----------------------------- +// Logging utils for easy usage +// ----------------------------- + +#define DEME_GET_VERBOSITY() deme::Logger::GetInstance().GetVerbosity() + +#define DEME_ERROR(...) \ + throw deme::SolverException( \ + deme::Logger::GetInstance().Logf(deme::MessageType::Error, __func__, __FILE__, __LINE__, __VA_ARGS__)) + +#define DEME_ERROR_NOTHROW(...) \ + deme::Logger::GetInstance().Logf(deme::MessageType::Error, __func__, __FILE__, __LINE__, __VA_ARGS__) + +#define DEME_WARNING(...) \ + deme::Logger::GetInstance().Logf(deme::MessageType::Warning, __func__, __FILE__, __LINE__, __VA_ARGS__) + +#define DEME_INFO(...) \ + deme::Logger::GetInstance().Logf(deme::MessageType::Info, __func__, __FILE__, __LINE__, __VA_ARGS__) + +#define DEME_STATUS(identifier, ...) \ + deme::Logger::GetInstance().LogStatusf(identifier, __func__, __FILE__, __LINE__, __VA_ARGS__) + +#define DEME_GPU_CALL(code) \ + { \ + cudaError_t res = (code); \ + if (res != cudaSuccess) { \ + DEME_ERROR( \ + "GPU Error: %s\nYou can check out the troubleshoot section of DEME to see if it helps.\nIf nothing " \ + "works, you can discuss this on forum https://groups.google.com/g/projectchrono, and please include " \ + "a visual rendering of the simulation before crash in the post.\n", \ + cudaGetErrorString(res)); \ + } \ + } + +// Use this for CUDA calls that enqueue work on a known stream. In debug-sync mode it reports asynchronous failures at +// the operation that caused them, while preserving the normal asynchronous behavior when debug-sync mode is disabled. +#define DEME_GPU_CALL_ASYNC(code, stream) \ + { \ + DEME_GPU_CALL(code); \ + DEME_GPU_CALL(deme::DebugSyncStream(stream)); \ + } + +#define DEME_GPU_DEBUG_SYNC(stream) DEME_GPU_CALL(deme::DebugSyncStream(stream)) + +#define DEME_GPU_CALL_NOTHROW(code) \ + { \ + cudaError_t res = (code); \ + if (res != cudaSuccess) { \ + DEME_ERROR_NOTHROW("GPU Error: %s", cudaGetErrorString(res)); \ + } \ + } + +#define DEME_GPU_CALL_WATCH_BETA(res) \ + { gpu_assert_watch_beta((res), __FILE__, __LINE__, *(stateParams.maxVel), *(stateParams.maxAngVel), true); } + +inline bool gpu_assert_watch_beta(cudaError_t code, + const char* filename, + int line, + float max_vel, + float max_ang_vel, + bool except = true) { + if (code != cudaSuccess) { + if (except) { + std::stringstream out_msg; + out_msg << "\nGPU Assertion: " << cudaGetErrorString(code) << ". This happened in " << filename << ":" + << line << "\n"; + out_msg << "\n-------- Simulation crashed \"potentially\" due to too many geometries in a bin --------\n"; + out_msg << "The dT reported max velocity is " << max_vel << " and max angular velocity is " << max_ang_vel + << "\n"; + out_msg << "------------------------------------\n"; + out_msg << "If the velocity is huge, then the simulation probably diverged due to encountering " + "large particle velocities.\nDecreasing the step size could help, and remember to check if your " + "simulation objects are initially within the domain you specified.\n"; + out_msg << "------------------------------------\n"; + out_msg + << "If the velocity is fair, and you *are* using a custom force model, one thing to do is to " + "SetForceCalcThreadsPerBlock to a small number like 128 (see README.md troubleshooting for " + "details).\nIf you are not using a custom model, one thing to do is to ensure the simulation " + "world size (InstructBoxDomainDimension) is not orders of magnitude larger than the actual " + "space the simulation entities take up.\nIf none works and you are going to discuss this on forum " + "https://groups.google.com/g/projectchrono, " + "please include a visual rendering of the simulation before crash in the post.\n\n"; + std::cerr << out_msg.str(); + std::stringstream out; + out << "GPU Assertion: " << cudaGetErrorString(code) << ". This happened in " << filename << ":" << line + << "\n"; + throw std::runtime_error(out.str()); + } + return false; + } + return true; +} + +} // namespace deme + +#endif diff --git a/src/core/utils/ManagedMemory.hpp b/src/core/utils/ManagedMemory.hpp deleted file mode 100644 index 71bed2c7..00000000 --- a/src/core/utils/ManagedMemory.hpp +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// -// SPDX-License-Identifier: BSD-3-Clause - -#ifndef DEME_MANAGED_MEM_H -#define DEME_MANAGED_MEM_H - -#include - -#include "CudaAllocator.hpp" - -// Convenience functions to help with Managed Memory (allocated using ManagedAllocator, typically) -namespace deme { - -#if !defined(USE_HIP) && CUDART_VERSION >= 13000 -inline cudaMemLocation make_device_location(int device) { - cudaMemLocation loc{}; - loc.type = cudaMemLocationTypeDevice; - loc.id = device; - return loc; -} -#endif - -// Underlying implementation -template -inline void __migrate_impl(T* data, std::size_t size, int device, cudaStream_t stream = 0) { -#if defined(USE_HIP) - // HIP uses the 4-arg form: hipMemPrefetchAsync(const void*, size_t, int dstDevice, hipStream_t) - cudaMemPrefetchAsync(static_cast(data), size * sizeof(T), device, stream); -#elif CUDART_VERSION >= 13000 - // CUDA 13.0+: cudaMemPrefetchAsync(const void*, size_t, cudaMemLocation, unsigned int flags, cudaStream_t) - auto loc = make_device_location(device); - unsigned int flags = 0; - cudaMemPrefetchAsync(static_cast(data), size * sizeof(T), loc, flags, stream); -#else - // CUDA <= 12.x: cudaMemPrefetchAsync(const void*, size_t, int dstDevice, cudaStream_t) - cudaMemPrefetchAsync(static_cast(data), size * sizeof(T), device, stream); -#endif -} - -// Pointer syntax, basically the same as cudaMemPrefetchAsync(...) -template -void migrate(T* data, std::size_t size, int device, cudaStream_t stream = 0) { - __migrate_impl(data, size, device, stream); -} - -// Inadviseable, but necessary to force transfer of const arrays to device -// template -// void migrate(const T* data, std::size_t size, int device, cudaStream_t stream = 0) { -// __migrate_impl(std::const_cast(data), size, device, stream); -// } - -// Migrate a single class or value -template -void migrate(T& datum, int device, cudaStream_t stream = 0) { - __migrate_impl(&datum, 1, device, stream); -} - -// Migrate a single const class or value -template -void migrate(const T& datum, int device, cudaStream_t stream = 0) { - __migrate_impl(&datum, 1, device, stream); -} - -// Migrate the data contained in a vector -template -void migrate(std::vector& data, int device, cudaStream_t stream = 0) { - __migrate_impl(data.data(), data.size(), device, stream); -} - -// Migrate the data contained in a const vector -template -void migrate(const std::vector& data, int device, cudaStream_t stream = 0) { - __migrate_impl(data.data(), data.size(), device, stream); -} - -// Aliases for cudaMemoryAdvise constants -enum class ManagedAdvice { -#if defined(USE_HIP) - READ_MOSTLY = hipMemAdviseSetReadMostly, - PREFERRED_LOC = hipMemAdviseSetPreferredLocation, - ACCESSED_BY = hipMemAdviseSetAccessedBy, - UNSET_READ_MOSTLY = hipMemAdviseUnsetReadMostly, - UNSET_PREFERRED_LOC = hipMemAdviseUnsetPreferredLocation, - UNSET_ACCESSED_BY = hipMemAdviseUnsetAccessedBy -#else - READ_MOSTLY = cudaMemAdviseSetReadMostly, - PREFERRED_LOC = cudaMemAdviseSetPreferredLocation, - ACCESSED_BY = cudaMemAdviseSetAccessedBy, - UNSET_READ_MOSTLY = cudaMemAdviseUnsetReadMostly, - UNSET_PREFERRED_LOC = cudaMemAdviseUnsetPreferredLocation, - UNSET_ACCESSED_BY = cudaMemAdviseUnsetAccessedBy -#endif -}; - -// Underlying implementation -template -void __advise_impl(const T* data, std::size_t size, ManagedAdvice advice, int device) { -#if defined(USE_HIP) - // HIP uses the 4-arg form: hipMemAdvise(const void*, size_t, hipMemoryAdvise, int) - hipMemAdvise(static_cast(data), size * sizeof(T), static_cast(advice), device); -#elif CUDART_VERSION >= 13000 - cudaMemLocation loc{}; - loc.type = cudaMemLocationTypeDevice; - loc.id = device; - // CUDA 13.0+: cudaMemAdvise(const void*, size_t, cudaMemoryAdvise, cudaMemLocation) - cudaMemAdvise(static_cast(data), size * sizeof(T), static_cast(advice), loc); -#else - // Older toolkits: cudaMemAdvise(const void*, size_t, cudaMemoryAdvise, int) - cudaMemAdvise(static_cast(data), size * sizeof(T), static_cast(advice), device); -#endif -} - -// Advice for raw pointer -template -void advise(const T* data, std::size_t size, ManagedAdvice advice, int device) { - __advise_impl(data, size, advice, device); -} - -// Advice for single class or value -template -void advise(const T& datum, ManagedAdvice advice, int device) { - __advise_impl(&datum, 1, advice, device); -} - -// Advice for underlying vector storage -template -void advise(const std::vector& data, ManagedAdvice advice, int device) { - __advise_impl(data.data(), data.size(), advice, device); -} - -} // END namespace deme - -#endif diff --git a/src/core/utils/RuntimeData.cpp.in b/src/core/utils/RuntimeData.cpp.in index f7177c0a..339e17a6 100644 --- a/src/core/utils/RuntimeData.cpp.in +++ b/src/core/utils/RuntimeData.cpp.in @@ -14,7 +14,10 @@ std::filesystem::path DEMERuntimeDataHelper::data_path = std::filesystem::path(D // This include path is for the included headers in the kernels that need jitification std::filesystem::path DEMERuntimeDataHelper::include_path = std::filesystem::path(DEME_RUNTIME_INCLUDE_DIRECTORY); -void DEMERuntimeDataHelper::SetPathPrefix(const std::filesystem::path& path) { - data_path = path / "share/DEME"; - include_path = path / "include"; +// p should be something like /path/to/pip/prefix/bin/../lib/python${PY_VER}/site-packages +// can get path from python's sys.executable, then run os.path.dirname on it to strip away the binary name + +void DEMERuntimeDataHelper::SetPathPrefix(const std::filesystem::path& p) { + data_path = p / "share/DEME"; + include_path = p / "include"; } diff --git a/src/core/utils/RuntimeData.h b/src/core/utils/RuntimeData.h index 0690f527..aabee648 100644 --- a/src/core/utils/RuntimeData.h +++ b/src/core/utils/RuntimeData.h @@ -23,7 +23,8 @@ class DEMERuntimeDataHelper_API DEMERuntimeDataHelper { static std::filesystem::path data_path; static std::filesystem::path include_path; - static void SetPathPrefix(const std::filesystem::path& path); + // This is for Python package's use only + static void SetPathPrefix(const std::filesystem::path& p); }; #endif diff --git a/src/core/utils/ThreadManager.h b/src/core/utils/ThreadManager.h index cfa95cdd..11e7df40 100644 --- a/src/core/utils/ThreadManager.h +++ b/src/core/utils/ThreadManager.h @@ -48,9 +48,9 @@ class ThreadManager { std::atomic kinematicIngredProdDateStamp; // dT tags this when sending it to kT std::atomic kinematicMaxFutureDrift; // kT tags this to its produce before shipping + // Single-producer/single-consumer freshness flags and locks for the legacy dT/kT buffer handoff. std::atomic dynamicOwned_Prod2ConsBuffer_isFresh; std::atomic kinematicOwned_Cons2ProdBuffer_isFresh; - std::mutex dynamicOwnedBuffer_AccessCoordination; std::mutex kinematicOwnedBuffer_AccessCoordination; std::mutex kinematicCanProceed; @@ -78,8 +78,8 @@ class ThreadManager { kinematicIngredProdDateStamp = -1; currentStampOfDynamic = 0; dynamicDone = false; - dynamicOwned_Prod2ConsBuffer_isFresh = false; - kinematicOwned_Cons2ProdBuffer_isFresh = false; + dynamicOwned_Prod2ConsBuffer_isFresh.store(false, std::memory_order_relaxed); + kinematicOwned_Cons2ProdBuffer_isFresh.store(false, std::memory_order_relaxed); } ~ThreadManager() {} diff --git a/src/core/utils/Timer.hpp b/src/core/utils/Timer.hpp index 141be69d..1747da2d 100644 --- a/src/core/utils/Timer.hpp +++ b/src/core/utils/Timer.hpp @@ -56,6 +56,11 @@ class Timer { m_total += m_end - m_start; } + /// Manually accumulate a duration (in seconds) that was measured elsewhere. + void addDuration(seconds_type duration_seconds) { + m_total += std::chrono::duration(duration_seconds); + } + /// Reset the total accumulated time (when repeating multiple start() stop() start() stop() ) void reset() { m_total = std::chrono::duration(0); } diff --git a/src/core/utils/cuda_to_hip.h b/src/core/utils/cuda_to_hip.h deleted file mode 100644 index 646f1b7d..00000000 --- a/src/core/utils/cuda_to_hip.h +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2021, SBEL GPU Development Team -// Copyright (c) 2021, University of Wisconsin - Madison -// Copyright (c) 2026, Advanced Micro Devices, Inc. -// -// SPDX-License-Identifier: BSD-3-Clause - -// CUDA-to-HIP compatibility header for DEM-Engine. -// On ROCm this aliases CUDA runtime API symbols to their HIP equivalents. -// On CUDA this is a passthrough to the standard CUDA runtime. - -#ifndef DEME_CUDA_TO_HIP_H -#define DEME_CUDA_TO_HIP_H - -#if defined(USE_HIP) || defined(__HIP_PLATFORM_AMD__) - - #include - - // Runtime API types - #define cudaStream_t hipStream_t - #define cudaEvent_t hipEvent_t - #define cudaError_t hipError_t - #define cudaDeviceProp hipDeviceProp_t - - // Error codes - #define cudaSuccess hipSuccess - #define cudaErrorInvalidValue hipErrorInvalidValue - #define cudaErrorMemoryAllocation hipErrorMemoryAllocation - #define cudaErrorNotSupported hipErrorNotSupported - - // Memory management - #define cudaMalloc hipMalloc - #define cudaFree hipFree - #define cudaMallocManaged hipMallocManaged - #define cudaMallocHost hipHostMalloc - #define cudaHostAlloc hipHostMalloc - #define cudaFreeHost hipHostFree - #define cudaMemcpy hipMemcpy - #define cudaMemcpyAsync hipMemcpyAsync - #define cudaMemset hipMemset - #define cudaMemsetAsync hipMemsetAsync - #define cudaMemPrefetchAsync hipMemPrefetchAsync - - // Memory copy kinds - #define cudaMemcpyHostToHost hipMemcpyHostToHost - #define cudaMemcpyHostToDevice hipMemcpyHostToDevice - #define cudaMemcpyDeviceToHost hipMemcpyDeviceToHost - #define cudaMemcpyDeviceToDevice hipMemcpyDeviceToDevice - #define cudaMemcpyDefault hipMemcpyDefault - - // Memory attach flags - #define cudaMemAttachGlobal hipMemAttachGlobal - - // Host alloc flags - #define cudaHostAllocDefault hipHostMallocDefault - - // Stream management - #define cudaStreamCreate hipStreamCreate - #define cudaStreamDestroy hipStreamDestroy - #define cudaStreamSynchronize hipStreamSynchronize - - // Device management - #define cudaGetDevice hipGetDevice - #define cudaSetDevice hipSetDevice - #define cudaGetDeviceCount hipGetDeviceCount - #define cudaGetDeviceProperties hipGetDeviceProperties - #define cudaDeviceSynchronize hipDeviceSynchronize - #define cudaDeviceReset hipDeviceReset - - // Error handling - #define cudaGetLastError hipGetLastError - #define cudaGetErrorString hipGetErrorString - #define cudaPeekAtLastError hipPeekAtLastError - - // Pointer attributes - #define cudaPointerAttributes hipPointerAttribute_t - #define cudaPointerGetAttributes hipPointerGetAttributes - - // Memory types - use a namespace alias to preserve scoped access syntax. - // In hiprtc JIT kernels, hipMemoryType may not be available (device-only runtime). - // Provide a minimal fallback. - #ifdef __HIPCC_RTC__ -// hiprtc device-only context: define our own enum for compatibility -enum hipMemoryType { - hipMemoryTypeUnregistered = 0, - hipMemoryTypeHost = 1, - hipMemoryTypeDevice = 2, - hipMemoryTypeManaged = 3 -}; - #endif - -namespace cudaMemoryType { -constexpr hipMemoryType cudaMemoryTypeUnregistered = hipMemoryTypeUnregistered; -constexpr hipMemoryType cudaMemoryTypeHost = hipMemoryTypeHost; -constexpr hipMemoryType cudaMemoryTypeDevice = hipMemoryTypeDevice; -constexpr hipMemoryType cudaMemoryTypeManaged = hipMemoryTypeManaged; -} // namespace cudaMemoryType - - // Memory advise - #define cudaMemAdvise hipMemAdvise - #define cudaMemoryAdvise hipMemoryAdvise - - // CUB -> hipCUB - #define cub hipcub - - // cuRAND device headers -> hipRAND (not needed in this shim, handled in API.h) - - // CUDA floating-point intrinsics with directed rounding. - // HIP/AMD does not provide exact equivalents; use standard operations. - // The _ru suffix means "round up" (toward +infinity), _rd means "round down", - // _rn means "round to nearest", _rz means "round toward zero". - // For most physics simulations, the difference is negligible. - #if defined(__HIP_DEVICE_COMPILE__) || defined(__HIPCC__) -__device__ inline float __frcp_ru(float x) { - return 1.0f / x; -} -__device__ inline double __drcp_ru(double x) { - return 1.0 / x; -} -__device__ inline float __fmul_ru(float x, float y) { - return x * y; -} -__device__ inline double __dmul_ru(double x, double y) { - return x * y; -} -__device__ inline float __fadd_ru(float x, float y) { - return x + y; -} -__device__ inline double __dadd_ru(double x, double y) { - return x + y; -} - #endif - -#else // CUDA - - #include - -#endif // USE_HIP - -#endif // DEME_CUDA_TO_HIP_H diff --git a/src/demo/CMakeLists.txt b/src/demo/CMakeLists.txt index 80e70e18..a80fc80d 100644 --- a/src/demo/CMakeLists.txt +++ b/src/demo/CMakeLists.txt @@ -19,27 +19,37 @@ SET(DEMOS DEMdemo_Plow DEMdemo_Sieve DEMdemo_SingleSphereCollide + DEMdemo_MeshCollide + DEMdemo_MeshFalling DEMdemo_TestPack + DEMdemo_TestRestart DEMdemo_RotatingDrum DEMdemo_Centrifuge + DEMdemo_DrumCubes + DEMdemo_DrumCubesSmall + DEMdemo_ResponseAngleMesh DEMdemo_GameOfLife DEMdemo_BallDrop DEMdemo_BallDrop2D DEMdemo_Mixer + DEMdemo_MixerCubes + DEMdemo_PolydisperseGeneration + DEMdemo_RoverWheel DEMdemo_GRCPrep_Part1 DEMdemo_GRCPrep_Part2 DEMdemo_GRCPrep_Part3 + DEMdemo_PlateSinkage DEMdemo_ConePenetration DEMdemo_Shake DEMdemo_WheelDP DEMdemo_WheelDPSimplified DEMdemo_WheelSlopeSlip DEMdemo_Indentation - DEMdemo_PlateSinkage DEMdemo_SolarSystem DEMdemo_Electrostatic DEMdemo_FlexibleMesh DEMdemo_HopperSphereCylinder + DEMdemo_HopperSphereMeshedCylinder DEMdemo_FractureBox ) @@ -80,13 +90,22 @@ FOREACH(PROGRAM ${DEMOS}) PUBLIC ${LIBRARIES} PUBLIC ${EXTERNAL_LIBRARIES} ) + if(DEME_BUILD_VISUALIZER AND + (PROGRAM STREQUAL "DEMdemo_SingleSphereCollide" OR PROGRAM STREQUAL "DEMdemo_MeshCollide" OR + PROGRAM STREQUAL "DEMdemo_MeshFalling")) + target_link_libraries(${PROGRAM} PUBLIC DEMEVisualizer) + target_compile_definitions(${PROGRAM} PRIVATE DEME_HAS_VISUALIZER=1) + endif() add_dependencies(${PROGRAM} ${LIBRARIES}) + add_dependencies(${PROGRAM} sync_kernels) - set_target_properties( - ${PROGRAM} PROPERTIES - CXX_STANDARD ${CXXSTD_SUPPORTED} - ) + if (CXXSTD_SUPPORTED) + set_target_properties( + ${PROGRAM} PROPERTIES + CXX_STANDARD ${CXXSTD_SUPPORTED} + ) + endif() # install(TARGETS ${PROGRAM} DESTINATION ${DEME_INSTALL_DEMO}) diff --git a/src/demo/DEMdemo_BallDrop.cpp b/src/demo/DEMdemo_BallDrop.cpp index 1bf1bacb..694e7c1a 100644 --- a/src/demo/DEMdemo_BallDrop.cpp +++ b/src/demo/DEMdemo_BallDrop.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,14 +21,9 @@ using namespace deme; using namespace std::filesystem; -double randomBetween0and1() { - static std::mt19937 gen(std::random_device{}()); // Random number generator - static std::uniform_real_distribution distribution(0.0, 1.0); // Uniform distribution between 0 and 1 - - return distribution(gen); -} - int main() { + std::cout << "==== DEME demo/test: DEMdemo_BallDrop ====" << std::endl; + std::cout << "========================================" << std::endl; float ball_densities[] = {2.2e3, 3.8e3, 7.8e3, 15e3}; float Hs[] = {0.05, 0.1, 0.2}; double R = 0.0254 / 2.; @@ -145,7 +139,7 @@ int main() { auto max_z_finder = DEMSim.CreateInspector("clump_max_z"); auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetMaxVelocity(30.); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); diff --git a/src/demo/DEMdemo_BallDrop2D.cpp b/src/demo/DEMdemo_BallDrop2D.cpp index 8b52ffb2..f0ccf5d3 100644 --- a/src/demo/DEMdemo_BallDrop2D.cpp +++ b/src/demo/DEMdemo_BallDrop2D.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,14 +21,9 @@ using namespace deme; using namespace std::filesystem; -double randomBetween0and1() { - static std::mt19937 gen(std::random_device{}()); // Random number generator - static std::uniform_real_distribution distribution(0.0, 1.0); // Uniform distribution between 0 and 1 - - return distribution(gen); -} - int main() { + std::cout << "==== DEME demo/test: DEMdemo_BallDrop2D ====" << std::endl; + std::cout << "========================================" << std::endl; float ball_density = 6.2e3; float H = 0.1; double R = 0.0254 / 2.; @@ -42,6 +36,7 @@ int main() { DEMSim.SetOutputFormat("CSV"); DEMSim.SetOutputContent({"ABSV"}); DEMSim.SetMeshOutputFormat("VTK"); + // DEMSim.SetSimplePatchCombination(true); path out_dir = current_path(); out_dir /= "DemoOutput_BallDrop2D"; @@ -114,7 +109,7 @@ int main() { auto max_z_finder = DEMSim.CreateInspector("clump_max_z"); auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetMaxVelocity(30.); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); diff --git a/src/demo/DEMdemo_Centrifuge.cpp b/src/demo/DEMdemo_Centrifuge.cpp index b226101b..9a1093fc 100644 --- a/src/demo/DEMdemo_Centrifuge.cpp +++ b/src/demo/DEMdemo_Centrifuge.cpp @@ -13,9 +13,10 @@ #include #include #include -#include #include +#include +#include #include #include #include @@ -24,11 +25,12 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Centrifuge ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // Output family numbers (used to identify the centrifuging effect) DEMSim.SetOutputContent(OUTPUT_CONTENT::FAMILY); - // DEMSim.SetVerbosity(STEP_METRIC); // If you don't need individual force information, then this option makes the solver run a bit faster. DEMSim.SetNoForceRecord(); @@ -95,7 +97,8 @@ int main() { unsigned int drum_family = 100; Drum->SetFamily(drum_family); // The drum rotates (facing Z direction) - DEMSim.SetFamilyPrescribedAngVel(drum_family, "0", "0", "6.0"); + float DurmRotVel = 6.0; + DEMSim.SetFamilyPrescribedAngVel(drum_family, "0", "0", to_string_with_precision(DurmRotVel)); // Then add planes to `close up' the drum. We add it as another object b/c we want to track the force on it // separately. auto top_bot_planes = DEMSim.AddExternalObject(); @@ -115,8 +118,10 @@ int main() { auto input_material_xyz = DEMBoxGridSampler(sample_center, make_float3(sample_halfwidth, sample_halfwidth, sample_halfheight), scaling * std::cbrt(2.0) * 2.1, scaling * std::cbrt(2.0) * 2.1, scaling * 2 * 2.1); + // Keep the legacy non-periodic path exactly intact. input_xyz.insert(input_xyz.end(), input_material_xyz.begin(), input_material_xyz.end()); unsigned int num_clumps = input_material_xyz.size(); + // Casually select from generated clump types for (unsigned int i = 0; i < num_clumps; i++) { input_template_type.push_back(clump_types.at(i % clump_types.size())); @@ -133,14 +138,16 @@ int main() { // Make the domain large enough DEMSim.InstructBoxDomainDimension(5, 5, 5); float step_size = 5e-6; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGPUTimersEnabled(true); + // DEMSim.UseHertzConstTimeStep(); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.SetExpandSafetyType("auto"); // If there is a velocity that an analytical object (i.e. the drum) has that you'd like the solver to take into // account in consideration of adding contact margins, you have to specify it here, since the solver's automatic max // velocity derivation algorithm currently cannot take analytical object's angular velocity-induced velocity into // account. - DEMSim.SetExpandSafetyAdder(6.0); + DEMSim.SetExpandSafetyAdder(DurmRotVel * CylRad); DEMSim.Initialize(); path out_dir = current_path(); @@ -149,41 +156,37 @@ int main() { float time_end = 20.0; unsigned int fps = 20; - unsigned int out_steps = (unsigned int)(1.0 / (fps * step_size)); + float frame_time = 1.0 / fps; std::cout << "Output at " << fps << " FPS" << std::endl; unsigned int currframe = 0; - unsigned int curr_step = 0; std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); - for (double t = 0; t < (double)time_end; t += step_size, curr_step++) { - if (curr_step % out_steps == 0) { - std::cout << "Frame: " << currframe << std::endl; - DEMSim.ShowThreadCollaborationStats(); - char filename[100]; - sprintf(filename, "DEMdemo_output_%04d.csv", currframe); - DEMSim.WriteSphereFile(out_dir / filename); - currframe++; - max_v = max_v_finder->GetValue(); - std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; - - // Torque on the side walls are? - float3 drum_moi = Drum_tracker->MOI(); - float3 drum_acc = Drum_tracker->ContactAngAccLocal(); - float3 drum_torque = drum_acc * drum_moi; - std::cout << "Contact torque on the side walls is " << drum_torque.x << ", " << drum_torque.y << ", " - << drum_torque.z << std::endl; - - // The force on the bottom plane? - float3 force_on_BC = planes_tracker->ContactAcc() * planes_tracker->Mass(); - std::cout << "Contact force on bottom plane is " << force_on_BC.z << std::endl; - } - - DEMSim.DoDynamics(step_size); + for (double t = 0; t < (double)time_end; t += frame_time) { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[100]; + sprintf(filename, "DEMdemo_output_%04d.csv", currframe); + DEMSim.WriteSphereFile(out_dir / filename); + currframe++; + max_v = max_v_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + + // Torque on the side walls are? + float3 drum_moi = Drum_tracker->MOI(); + float3 drum_acc = Drum_tracker->ContactAngAccLocal(); + float3 drum_torque = drum_acc * drum_moi; + std::cout << "Contact torque on the side walls is " << drum_torque.x << ", " << drum_torque.y << ", " + << drum_torque.z << std::endl; + + // The force on the bottom plane? + float3 force_on_BC = planes_tracker->ContactAcc() * planes_tracker->Mass(); + std::cout << "Contact force on bottom plane is " << force_on_BC.z << std::endl; + + DEMSim.DoDynamics(frame_time); } std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); - std::cout << (time_sec.count()) / time_end * 10.0 << " seconds (wall time) to finish 10 seconds' simulation" - << std::endl; + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; DEMSim.ShowThreadCollaborationStats(); DEMSim.ClearThreadCollaborationStats(); diff --git a/src/demo/DEMdemo_ConePenetration.cpp b/src/demo/DEMdemo_ConePenetration.cpp index 35ce1482..c18d3f49 100644 --- a/src/demo/DEMdemo_ConePenetration.cpp +++ b/src/demo/DEMdemo_ConePenetration.cpp @@ -11,7 +11,6 @@ // ============================================================================= #include -#include #include #include @@ -26,8 +25,10 @@ using namespace deme; const double math_PI = 3.14159; int main() { + std::cout << "==== DEME demo/test: DEMdemo_ConePenetration ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); @@ -78,19 +79,17 @@ int main() { DEMSim.AddClumps(my_template, input_xyz); std::cout << "Total num of particles: " << input_xyz.size() << std::endl; - // Load in the cone used for this penetration test. The penetrating tip is an analytical cone segment so the contact - // normal is exact and does not depend on the resolution of a triangle mesh. - float tip_height = std::sqrt(3.); + // Use an exact analytical cone side for the penetrating tip; the attached cylindrical body remains a mesh. + const float tip_height = std::sqrt(3.0f); const float cone_radius = cone_diameter / 2; const float cone_height = cone_radius * tip_height; auto cone_tip = DEMSim.AddExternalObject(); - cone_tip->AddConeSegment(make_float3(0, 0, -3.0 / 4.0 * cone_height), make_float3(0, 0, 1), 1.0 / tip_height, 0.0, - cone_height, mat_type_cone, ENTITY_NORMAL_OUTWARD); + cone_tip->AddConeSegment(make_float3(0, 0, -3.0f / 4.0f * cone_height), make_float3(0, 0, 1), 1.0f / tip_height, + 0.0f, cone_height, mat_type_cone, ENTITY_NORMAL_OUTWARD); auto cone_body = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/cyl_r1_h2.obj"), mat_type_cone); std::cout << "Total num of mesh triangles: " << cone_body->GetNumTriangles() << std::endl; - // Then set mass properties for the analytical cone segment. Its owner frame is placed at the solid cone centroid, - // so the local apex is at -3H/4 and the base rim at H/4. + // Set the analytical owner's mass properties about a solid cone centroid located 3H/4 above its apex. float cone_mass = 7.8e3 * math_PI * cone_radius * cone_radius * cone_height / 3; cone_tip->SetMass(cone_mass); cone_tip->SetMOI( @@ -131,7 +130,7 @@ int main() { // -0.3);"); auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // CD freq will be auto-adapted so it does not matter much here. DEMSim.SetCDUpdateFreq(20); @@ -213,11 +212,11 @@ int main() { // Put the cone in place double starting_height = terrain_max_z + 0.03; // Its initial position should be right above the cone tip... - body_tracker->SetPos(make_float3(0, 0, 0.5 + cone_height / 4 + starting_height)); + body_tracker->SetPos(make_float3(0, 0, 0.5 + (cone_diameter / 2 / 4 * tip_height) + starting_height)); // Note that position of objects is always the location of their centroid tip_tracker->SetPos(make_float3(0, 0, starting_height)); // The tip location, used to measure penetration length - double tip_z = -3.0 / 4.0 * cone_height + starting_height; + double tip_z = -cone_diameter / 2 * 3 / 4 * tip_height + starting_height; // Enable cone DEMSim.ChangeFamily(2, 1); @@ -234,7 +233,8 @@ int main() { for (float t = 0; t < sim_end; t += frame_time) { // float terrain_max_z = max_z_finder->GetValue(); float3 forces = tip_tracker->ContactAcc(); - // ContactAcc returns acceleration for the analytical cone owner, so multiply by its assigned mass. + // Note cone_mass is not the true mass, b/c we scaled the the cone tip! So we use true mass by using + // cone_tip->mass. forces *= cone_tip->mass; float pressure = std::abs(forces.z) / cone_surf_area; if (pressure > 1e-4 && !hit_terrain) { diff --git a/src/demo/DEMdemo_ContactChain.cpp b/src/demo/DEMdemo_ContactChain.cpp index 0886840b..d583046c 100644 --- a/src/demo/DEMdemo_ContactChain.cpp +++ b/src/demo/DEMdemo_ContactChain.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -77,7 +76,7 @@ int main() { // Loading the position of the spheres from an external file. //! Note that this list does not include the particle located at (0.0,0.0). - auto data_xyz = DEMSim.ReadClumpXyzFromCsv(GetDEMEDataFile("clumps/ContactChain_initial.csv")); + auto data_xyz = DEMSim.ReadClumpXyzFromCsv("../data/clumps/ContactChain_initial.csv"); std::vector input_xyz; std::vector> input_pile_template_type; @@ -113,7 +112,7 @@ int main() { std::cout << "Total num of particles: " << (int)input_pile_template_type.size() + 1 << "." << std::endl; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetMaxVelocity(30.); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -gravityMagnitude)); diff --git a/src/demo/DEMdemo_DrumCubes.cpp b/src/demo/DEMdemo_DrumCubes.cpp new file mode 100644 index 00000000..2275df79 --- /dev/null +++ b/src/demo/DEMdemo_DrumCubes.cpp @@ -0,0 +1,188 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Rotating drum centrifuge demo with only cube mesh particles. +// Matches the output style of DEMdemo_Centrifuge but uses 4 mm cubes by default +// (12-triangle mesh) inside an analytically defined cylinder and lids. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_DrumCubes ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("METRIC"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::FAMILY); + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetMeshParticlesLowPoly(true); + + auto mat_type_cube = DEMSim.LoadMaterial({{"E", 1e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.01}}); + auto mat_type_drum = DEMSim.LoadMaterial({{"E", 2e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.01}}); + DEMSim.SetMaterialPropertyPair("mu", mat_type_cube, mat_type_drum, 0.5); + + const float cube_size = 0.004f; + const float cube_density = 2600.0f; + const float cube_mass = cube_density * cube_size * cube_size * cube_size; + const float cube_moi = cube_mass * cube_size * cube_size / 6.0f; + const float half_diag = 0.5f * cube_size * std::sqrt(3.0f); + + // Load cube mesh template (12 triangles) and scale to desired cube size + auto cube_template = DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type_cube, true, false); + cube_template->Scale(cube_size); + cube_template->SetConvex(true); + cube_template->SetNeverWinner(true); + + // Drum definition + float3 CylCenter = make_float3(0, 0, 0); + float3 CylAxis = make_float3(0, 0, 1); + float CylRad = 0.08f; + float CylHeight = 0.2f; + float CylMass = 1.0f; + float safe_delta = 0.003f; + float IZZ = CylMass * CylRad * CylRad / 2; + float IYY = (CylMass / 12) * (3 * CylRad * CylRad + CylHeight * CylHeight); + auto Drum = DEMSim.AddExternalObject(); + Drum->AddCylinder(CylCenter, CylAxis, CylRad, mat_type_drum, ENTITY_NORMAL_INWARD); + Drum->SetMass(CylMass); + Drum->SetMOI(make_float3(IYY, IYY, IZZ)); + auto Drum_tracker = DEMSim.Track(Drum); + unsigned int drum_family = 100; + Drum->SetFamily(drum_family); + const float rpm = 200.0f; + const float drum_ang_vel = rpm * 2.0f * PI / 60.0f; + DEMSim.SetFamilyPrescribedAngVel(drum_family, "0", "0", to_string_with_precision(drum_ang_vel)); + auto top_bot_planes = DEMSim.AddExternalObject(); + top_bot_planes->AddPlane(make_float3(0, 0, CylHeight / 2. - safe_delta), make_float3(0, 0, -1), mat_type_drum); + top_bot_planes->AddPlane(make_float3(0, 0, -CylHeight / 2. + safe_delta), make_float3(0, 0, 1), mat_type_drum); + top_bot_planes->SetFamily(drum_family); + auto planes_tracker = DEMSim.Track(top_bot_planes); + + // Place cubes on a grid inside the drum, limited to the periodic wedge (mit Randabstand) + const unsigned int target_cubes = 5000; + float sample_radius = CylRad - half_diag - safe_delta; + float sample_halfheight = CylHeight / 2.0f - half_diag - safe_delta; + float fill_spacing = cube_size * 1.25f; // leave gap so meshes don't start in contact + std::mt19937 rng(42); + unsigned int created = 0; + + std::vector candidate_positions; + for (float z = -sample_halfheight; z <= sample_halfheight; z += fill_spacing) { + for (float y = -sample_radius; y <= sample_radius; y += fill_spacing) { + for (float x = -sample_radius; x <= sample_radius; x += fill_spacing) { + const float r2 = x * x + y * y; + if (r2 > sample_radius * sample_radius) + continue; + candidate_positions.push_back(make_float3(x, y, z)); + } + } + } + std::shuffle(candidate_positions.begin(), candidate_positions.end(), rng); + + const unsigned int n_to_place = + std::min(target_cubes, static_cast(candidate_positions.size())); + std::vector> cube_trackers; + cube_trackers.reserve(n_to_place); + for (unsigned int i = 0; i < n_to_place; i++) { + const float3 pos = candidate_positions[i]; + auto cube = DEMSim.AddMeshFromTemplate(cube_template, pos); + cube->SetFamily(1); + cube->SetMass(cube_mass); + cube->SetMOI(make_float3(cube_moi, cube_moi, cube_moi)); + cube->SetInitQuat(make_float4(0.f, 0.f, 0.f, 1.0f)); + cube_trackers.push_back(DEMSim.Track(cube)); + } + created = n_to_place; + if (n_to_place < target_cubes) { + std::cout << "Warning: target_cubes=" << target_cubes << " exceeds available non-overlapping slots (" + << n_to_place << ")." << std::endl; + } + std::cout << "Placed " << created << " cubes inside the drum." << std::endl; + + auto max_v_finder = DEMSim.CreateInspector("max_absv"); + float max_v = 0.f; + + DEMSim.InstructBoxDomainDimension(0.4, 0.4, 0.4); + float step_size = 2e-6f; + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGPUTimersEnabled(true); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetExpandSafetyAdder(drum_ang_vel * CylRad); + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir /= "DemoOutput_DrumCubes"; + create_directory(out_dir); + + float time_end = 3.0f; + unsigned int fps = 20; + float frame_time = 1.0f / fps; + + std::cout << "Output at " << fps << " FPS" << std::endl; + std::cout << "DrumCubes config: cube_size=" << cube_size << "m, target_cubes=" << target_cubes << std::endl; + unsigned int currframe = 0; + unsigned int curr_step = 0; + std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + double dynamics_wall = 0.0; + for (double t = 0; t < (double)time_end; t += frame_time, curr_step++) { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[100]; + sprintf(filename, "DEMdemo_output_%04d.vtk", currframe); + DEMSim.WriteMeshFile(out_dir / filename); + currframe++; + max_v = max_v_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + float3 drum_moi = Drum_tracker->MOI(); + float3 drum_acc = Drum_tracker->ContactAngAccLocal(); + float3 drum_torque = drum_acc * drum_moi; + std::cout << "Contact torque on the side walls is " << drum_torque.x << ", " << drum_torque.y << ", " + << drum_torque.z << std::endl; + + float3 force_on_BC = planes_tracker->ContactAcc() * planes_tracker->Mass(); + std::cout << "Contact force on bottom plane is " << std::abs(force_on_BC.z) << std::endl; + + auto dyn_start = std::chrono::high_resolution_clock::now(); + DEMSim.DoDynamics(frame_time); + auto dyn_end = std::chrono::high_resolution_clock::now(); + dynamics_wall += std::chrono::duration_cast>(dyn_end - dyn_start).count(); + } + std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; + std::cout << dynamics_wall << " seconds (wall time, DoDynamics only) to finish the simulation" << std::endl; + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ClearThreadCollaborationStats(); + + DEMSim.ShowTimingStats(); + std::cout << "----------------------------------------" << std::endl; + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + + std::cout << "DEMdemo_DrumCubes exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_DrumCubesSmall.cpp b/src/demo/DEMdemo_DrumCubesSmall.cpp new file mode 100644 index 00000000..ccbd6c11 --- /dev/null +++ b/src/demo/DEMdemo_DrumCubesSmall.cpp @@ -0,0 +1,165 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Rotating drum centrifuge demo with only cube mesh particles. +// Matches the output style of DEMdemo_Centrifuge but uses 10 mm cubes +// (12-triangle mesh) inside an analytically defined cylinder and lids. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_DrumCubesSmall ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("METRIC"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::FAMILY); + DEMSim.SetNoForceRecord(); + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetMeshParticlesLowPoly(true); + + auto mat_type_cube = DEMSim.LoadMaterial({{"E", 1e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.01}}); + auto mat_type_drum = DEMSim.LoadMaterial({{"E", 2e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.01}}); + DEMSim.SetMaterialPropertyPair("mu", mat_type_cube, mat_type_drum, 0.5); + + const float cube_size = 0.01f; + const float cube_density = 2600.0f; + const float cube_mass = cube_density * cube_size * cube_size * cube_size; + const float cube_moi = cube_mass * cube_size * cube_size / 6.0f; + const float half_diag = 0.5f * cube_size * std::sqrt(3.0f); + + // Load cube mesh template (12 triangles) and scale to 10 mm + auto cube_template = DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type_cube, true, false); + cube_template->Scale(cube_size); + + // Drum definition + float3 CylCenter = make_float3(0, 0, 0); + float3 CylAxis = make_float3(0, 0, 1); + float CylRad = 0.08f; + float CylHeight = 0.2f; + float CylMass = 1.0f; + float safe_delta = 0.003f; + float IZZ = CylMass * CylRad * CylRad / 2; + float IYY = (CylMass / 12) * (3 * CylRad * CylRad + CylHeight * CylHeight); + auto Drum = DEMSim.AddExternalObject(); + // Drum->AddCylinder(CylCenter, CylAxis, CylRad, mat_type_drum, 0); + Drum->AddPlane(make_float3(CylRad, 0, 0), make_float3(-1, 0, 0), mat_type_drum); + Drum->AddPlane(make_float3(-CylRad, 0, 0), make_float3(1, 0, 0), mat_type_drum); + Drum->AddPlane(make_float3(0, CylRad, 0), make_float3(0, -1, 0), mat_type_drum); + Drum->AddPlane(make_float3(0, -CylRad, 0), make_float3(0, 1, 0), mat_type_drum); + Drum->SetMass(CylMass); + Drum->SetMOI(make_float3(IYY, IYY, IZZ)); + auto Drum_tracker = DEMSim.Track(Drum); + unsigned int drum_family = 100; + Drum->SetFamily(drum_family); + const float rpm = 200.0f; + const float drum_ang_vel = rpm * 2.0f * PI / 60.0f; + DEMSim.SetFamilyPrescribedAngVel(drum_family, "0", "0", to_string_with_precision(drum_ang_vel)); + auto top_bot_planes = DEMSim.AddExternalObject(); + top_bot_planes->AddPlane(make_float3(0, 0, CylHeight / 2. - safe_delta), make_float3(0, 0, -1), mat_type_drum); + top_bot_planes->AddPlane(make_float3(0, 0, -CylHeight / 2. + safe_delta), make_float3(0, 0, 1), mat_type_drum); + top_bot_planes->SetFamily(drum_family); + auto planes_tracker = DEMSim.Track(top_bot_planes); + + // Place 1000 cubes on a grid inside the drum + const unsigned int target_cubes = 1000; + float sample_radius = CylRad - half_diag - safe_delta; + float sample_halfheight = CylHeight / 2.0f - half_diag - safe_delta; + float fill_spacing = cube_size * 1.25f; // leave gap so meshes don't start in contact + std::mt19937 rng(42); + unsigned int created = 0; + for (float z = -sample_halfheight; z <= sample_halfheight && created < target_cubes; z += fill_spacing) { + for (float y = -sample_radius; y <= sample_radius && created < target_cubes; y += fill_spacing) { + for (float x = -sample_radius; x <= sample_radius && created < target_cubes; x += fill_spacing) { + if (x * x + y * y > sample_radius * sample_radius) { + continue; + } + auto cube = DEMSim.AddMeshFromTemplate(cube_template, make_float3(x, y, z)); + cube->SetFamily(1); + cube->SetMass(cube_mass); + cube->SetMOI(make_float3(cube_moi, cube_moi, cube_moi)); + cube->SetInitQuat(make_float4(0.f, 0.f, 0.f, 1.0f)); + created++; + } + } + } + std::cout << "Placed " << created << " cubes inside the drum." << std::endl; + + auto max_v_finder = DEMSim.CreateInspector("max_absv"); + auto max_angv_finder = DEMSim.CreateInspector("max_absangvel"); + float max_v, max_angv; + + DEMSim.InstructBoxDomainDimension(0.4, 0.4, 0.4); + float step_size = 1e-4f; + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetExpandSafetyAdder(drum_ang_vel * CylRad); + DEMSim.SetErrorOutAvgContacts(20000); + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir /= "DemoOutput_DrumCubesSmall"; + create_directory(out_dir); + + float time_end = 3.0f; + unsigned int fps = 100; + float frame_time = 1.0f / fps; + + std::cout << "Output at " << fps << " FPS" << std::endl; + unsigned int currframe = 0; + unsigned int curr_step = 0; + std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + for (double t = 0; t < (double)time_end; t += frame_time, curr_step++) { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[100]; + sprintf(filename, "DEMdemo_output_%04d.vtk", currframe); + DEMSim.WriteMeshFile(out_dir / filename); + currframe++; + max_v = max_v_finder->GetValue(); + max_angv = max_angv_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + std::cout << "Max angular velocity of any point in simulation is " << max_angv << std::endl; + + float3 drum_moi = Drum_tracker->MOI(); + float3 drum_acc = Drum_tracker->ContactAngAccLocal(); + float3 drum_torque = drum_acc * drum_moi; + std::cout << "Contact torque on the side walls is " << drum_torque.x << ", " << drum_torque.y << ", " + << drum_torque.z << std::endl; + + float3 force_on_BC = planes_tracker->ContactAcc() * planes_tracker->Mass(); + std::cout << "Contact force on bottom plane is " << std::abs(force_on_BC.z) << std::endl; + + DEMSim.DoDynamics(frame_time); + } + std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ClearThreadCollaborationStats(); + + DEMSim.ShowTimingStats(); + std::cout << "----------------------------------------" << std::endl; + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + + std::cout << "DemoOutput_DrumCubesSmall exiting..." << std::endl; + return 0; +} \ No newline at end of file diff --git a/src/demo/DEMdemo_Electrostatic.cpp b/src/demo/DEMdemo_Electrostatic.cpp index ff904b43..9d85a5fd 100644 --- a/src/demo/DEMdemo_Electrostatic.cpp +++ b/src/demo/DEMdemo_Electrostatic.cpp @@ -7,9 +7,17 @@ // This demo lets a rod with some electric charges stick into a pile of granular // material that is also charged. The electrostatic force shows its effect. The // electric charges are even moving during the simulation. This is done through -// `Geometry Wildcard', where the amount of charges is associated with each -// sphere component (of clumps) and triangles (of meshes). Then a custom force -// model is used to derive the electrostatic force in addition to contact forces. +// owner wildcards, where the amount of charge is associated with each owner. +// Then a custom force model is used to derive the electrostatic force in +// addition to contact forces. +// +// This demo uses the "combined owner" approach: each particle is represented by +// 6 spheres (from spiky_sphere.csv), and each sphere is its own owner in a +// combined group. This gives each sphere its own electric charge wildcard (Q), +// enabling fine-grained charge distribution across the particle's shape. +// The meshed rod likewise uses 10 short cylinders combined rigidly, +// each carrying its own charge, to better emulate the distribution of charges +// on the rod. // // NOTE: If you want to create your own force model, it's probably a good idea // to understand the default model in the file FullHertzianForceModel.cu @@ -18,7 +26,6 @@ // ============================================================================= #include -#include #include #include @@ -35,6 +42,8 @@ const double math_PI = 3.1415927; std::string force_model(); int main() { + std::cout << "==== DEME demo/test: DEMdemo_Electrostatic ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat("CSV"); @@ -53,9 +62,8 @@ int main() { DEMSim.SetMaterialPropertyPair("mu", mat_type_rod, mat_type_terrain, 0.7); // We can specify the force model using a string. // This force model is the standard Hertzian--Mindlin model, plus an electrostatic force, based on something - // called a `geometry wildcard'. This is an extra property that we can associate with each geometry entity, - // such as triangle and sphere. We use this value to derive the electrostatic force. But first, we need to - // declare in the force model that a geometry wildcard is in use... + // called an owner wildcard. This is an extra property that we can associate with each owner. + // We use this value to derive the electrostatic force. auto my_force_model = DEMSim.DefineContactForceModel(force_model()); // auto my_force_model = DEMSim.ReadContactForceModel("ForceModelWithElectrostatic.cu"); @@ -64,9 +72,7 @@ int main() { my_force_model->SetMustPairwiseMatProp({"CoR", "mu", "Crr"}); my_force_model->SetPerContactWildcards({"delta_time", "delta_tan_x", "delta_tan_y", "delta_tan_z"}); // Use variable name `Q' for the amount of electrc charge. - // NOTE! If you call it Q here, then you can refer to this wildcard array using variable names Q_A amd Q_B in - // your custom force model. - my_force_model->SetPerGeometryWildcards({"Q"}); + my_force_model->SetPerOwnerWildcards({"Q"}); float init_charge = 2e-8; // Coulomb as the unit... float cone_speed = 0.1; @@ -86,19 +92,34 @@ int main() { walls->AddPlane(make_float3(0, 0, bottom), make_float3(0, 0, 1), mat_type_terrain); walls->AddPlane(make_float3(0, 0, world_size / 2. - world_size / 20.), make_float3(0, 0, -1), mat_type_terrain); - // Define the terrain particle templates - // Calculate its mass and MOI + // Define the terrain particle templates using combined owners. + // Each sphere from the spiky_sphere.csv file becomes its own owner in a combined group. + // This way, each sphere has its own electric charge wildcard, giving fine charge distribution. float terrain_density = 2.6e3; - double clump_vol = 4. / 3. * math_PI; - float mass = terrain_density * clump_vol; - float3 MOI = make_float3(2. / 5.) * mass; - // Then load it to system - std::shared_ptr my_template = - DEMSim.LoadClumpType(mass, MOI, GetDEMEDataFile("clumps/spiky_sphere.csv"), mat_type_terrain); - my_template->SetVolume(clump_vol); - // Decide the scalings of the templates we just created (so that they are... like particles, not rocks) double scale = 0.01; - my_template->Scale(scale); + + // The spiky_sphere.csv defines 6 component spheres. We create individual sphere templates for each, + // then combine them into a single combined-owner template. Each sphere is an independent owner with + // its own charge Q and a fixed position relative to the combined group. + // Sphere data from spiky_sphere.csv: x, y, z, r + std::vector sphere_positions = {make_float3(0, 0, 0), make_float3(0, -0.5, 0.5), + make_float3(0.65, 0.2, 0.55), make_float3(-0.45, 0.25, 0.65), + make_float3(0, 0.7, 0.2), make_float3(0.5, 0.2, -0.5)}; + std::vector sphere_radii = {0.9f, 0.5f, 0.25f, 0.45f, 0.35f, 0.35f}; + + // Scale relative positions and radii + std::vector scaled_positions(6); + std::vector> component_templates(6); + for (int i = 0; i < 6; i++) { + scaled_positions[i] = sphere_positions[i] * (float)scale; + float r = sphere_radii[i] * (float)scale; + float vol = 4. / 3. * math_PI * r * r * r; + float m = terrain_density * vol; + component_templates[i] = DEMSim.LoadSphereType(m, r, mat_type_terrain); + } + + // Create combined template: 6 spheres as 6 owners combined rigidly + auto combined_type = DEMSim.LoadCombinedClumpType(component_templates, scaled_positions, {}, 0); // Sampler to sample GridSampler sampler(scale * 2.4); @@ -106,35 +127,55 @@ int main() { float3 fill_center = make_float3(0, 0, bottom + fill_height / 2); const float fill_radius = soil_bin_diameter / 2. - scale * 3.; auto input_xyz = sampler.SampleCylinderZ(fill_center, fill_radius, fill_height / 2 - scale * 2.); - auto particles = DEMSim.AddClumps(my_template, input_xyz); - std::cout << "Total num of particles: " << particles->GetNumClumps() << std::endl; - std::cout << "Total num of spheres: " << particles->GetNumSpheres() << std::endl; - // Add electric charge Q to each sphere. It is important to add it to each sphere, not each clump. This - // is because the contact pairs are resolved between geometries, not clumps. Using clumps leads to - // double-count or triple-count or... - particles->AddGeometryWildcard("Q", std::vector(particles->GetNumSpheres(), init_charge)); - - // Load in the cone used for this penetration test - auto rod_body = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/cyl_r1_h2.obj"), mat_type_rod); - unsigned int num_tri = rod_body->GetNumTriangles(); - std::cout << "Total num of triangles: " << num_tri << std::endl; - - // The define the properties of the rod + auto particles = DEMSim.AddCombinedFromTemplate(combined_type, input_xyz); + std::cout << "Total num of combined particles: " << input_xyz.size() << std::endl; + std::cout << "Total num of member owners: " << particles->GetNumOwners() << std::endl; + // Add electric charge Q to each member owner. With combined owners, each of the 6 spheres + // per particle gets its own charge, enabling fine-grained electrostatic distribution. + particles->AddOwnerWildcard("Q", init_charge); + + // Build the rod from short mesh owners so charge is distributed along its length. + constexpr size_t num_rod_sections = 10; + const double section_length = rod_length / num_rod_sections; + auto rod_section = DEMSim.LoadMeshType(GetDEMEDataFile("mesh/cyl_r1_h2.obj"), mat_type_rod); + std::cout << "Total num of triangles: " << num_rod_sections * rod_section->GetNumTriangles() << std::endl; + + // Set the reference cylinder mass, then scale it with the volume of each short section. float body_mass = 7.8e3 * math_PI; - rod_body->SetMass(body_mass); - rod_body->SetMOI(make_float3(body_mass * 7 / 12, body_mass * 7 / 12, body_mass / 2)); + rod_section->SetMass(body_mass); // This cyl mesh (h = 2m, r = 1m) has its center at the origin. So the following call actually has no effect... - rod_body->InformCentroidPrincipal(make_float3(0, 0, 0), make_float4(0, 0, 0, 1)); - rod_body->Scale(make_float3(rod_diameter / 2., rod_diameter / 2., rod_length / 2.)); - rod_body->SetFamily(1); + rod_section->InformCentroidPrincipal(make_float3(0, 0, 0), make_float4(0, 0, 0, 1)); + rod_section->Scale(make_float3(rod_diameter / 2., rod_diameter / 2., section_length / 2.)); + // Nonuniform Scale only estimates inertia; use the short cylinder's centroidal inertia explicitly. + const double radius_squared = rod_diameter * rod_diameter / 4.; + const double transverse_moi = + rod_section->GetMass() * (3. * radius_squared + section_length * section_length) / 12.; + rod_section->SetMOI(make_float3(transverse_moi, transverse_moi, rod_section->GetMass() * radius_squared / 2.)); + rod_section->SetFamily(1); + + // The bottom section is the master. Combined template poses are relative to that section's center, + // so its initial center is half a section above the assembled rod's lower end at -rod_length / 2. + std::vector> rod_templates(num_rod_sections, rod_section); + std::vector section_offsets(num_rod_sections); + for (size_t i = 0; i < num_rod_sections; i++) { + section_offsets[i] = make_float3(0, 0, i * section_length); + } + auto rod_type = DEMSim.LoadCombinedMeshType(rod_templates, section_offsets); + auto rod_body = DEMSim.AddCombinedFromTemplate(rod_type, make_float3(0, 0, (section_length - rod_length) / 2.)); // Just fix it: We will manually impose its motion later. DEMSim.SetFamilyFixed(1); - // We can set the geometry wildcard Q here. But we'll instead show how to modify that using a tracker. - // rod_body->AddGeometryWildcard("Q", std::vector(num_tri, init_charge)); - - // Track the rod auto rod_tracker = DEMSim.Track(rod_body); + // A combined tracker spans member owners; scalar setters affect only one member. Move all sections + // together so contact detection and output see the complete pose before the next rigid re-imposition. + std::vector rod_positions(num_rod_sections); + auto set_rod_bottom = [&](double height) { + for (size_t i = 0; i < num_rod_sections; i++) { + rod_positions[i] = make_float3(0, 0, height + (i + 0.5) * section_length); + } + rod_tracker->SetPos(rod_positions); + }; + // Some inspectors auto max_z_finder = DEMSim.CreateInspector("clump_max_z"); @@ -147,7 +188,7 @@ int main() { // that the solver does not error out when checking it. DEMSim.SetErrorOutAvgContacts(100); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); // In this test we don't want dT to drift too much ahead of kT, or the contact margin could be too large, resulting // in too many false positive contacts, making the solver slower. DEMSim.SetCDMaxUpdateFreq(60); @@ -177,23 +218,23 @@ int main() { DEMSim.WriteSphereFile(out_dir / filename); DEMSim.WriteMeshFile(out_dir / meshname); DEMSim.ShowThreadCollaborationStats(); + DEMSim.ShowMemStats(); DEMSim.DoDynamics(frame_time); } DEMSim.DoDynamicsThenSync(0); - // Put the cone in place + // Put the rod in place float terrain_max_z = max_z_finder->GetValue(); double current_height = terrain_max_z + 0.03; - // Its initial position should be right above the granular material (but accounting for the fact that the coordinate - // system center of the rod is in its middle) - rod_tracker->SetPos(make_float3(0, 0, rod_length / 2. + current_height)); + // Place the bottom of the assembled rod just above the granular material. + set_rod_bottom(current_height); DEMSim.EnableContactBetweenFamilies(0, 1); - // We demonstrate using trackers to set a geometry wildcard. Q is now set for each triangle facet, and it's - // the opposite charge to the particles. So the rod should attract the particles. - rod_tracker->SetGeometryWildcardValues("Q", std::vector(num_tri, -10. * init_charge)); + // Give each rod section an initial charge of -1000 * init_charge (-2e-5 C). + // The total rod charge is -2e-4 C, with opposite sign to the terrain particles so it attracts them. + rod_tracker->SetOwnerWildcardValues("Q", std::vector(num_rod_sections, -1000.f * init_charge)); std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); for (float t = 0; t < sim_end; t += step_size, step_count++) { @@ -205,6 +246,7 @@ int main() { DEMSim.WriteSphereFile(out_dir / filename); DEMSim.WriteMeshFile(out_dir / meshname); DEMSim.ShowThreadCollaborationStats(); + DEMSim.ShowMemStats(); } // Means advance simulation by one time step @@ -216,7 +258,7 @@ int main() { } else if (t < 2. / 3. * sim_end) { current_height += cone_speed * step_size; } // else the rod does not move - rod_tracker->SetPos(make_float3(0, 0, rod_length / 2. + current_height)); + set_rod_bottom(current_height); } std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); @@ -264,12 +306,12 @@ std::string force_model() { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -289,8 +331,10 @@ std::string force_model() { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Derive the equivalent contact radius from the actual overlap geometry so non-spherical contacts + // use their measured overlap area rather than an inaccurate analytical radius. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -307,7 +351,7 @@ std::string force_model() { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -338,7 +382,7 @@ std::string force_model() { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); @@ -377,23 +421,22 @@ std::string force_model() { const float k = 8.99e9; const double ABdist2 = dot(bodyAPos - bodyBPos, bodyAPos - bodyBPos); // If Q_A and Q_B are the same sign, then the force pushes A away from B, so B2A is the direction. - force += k * Q_A[AGeo] * Q_B[BGeo] / ABdist2 * (B2A); + force += k * Q[AOwner] * Q[BOwner] / ABdist2 * (B2A); // Fun part: we can modify the electric charge on the fly. But we have to use atomic, since multiple contacts // can modify the same Q. // But this is not recommend unless you understand what you are doing, and there are a lot of details related to it. - // For example, although the charges transfer between geometries, the geometries within one clump cannot - // re-distribute elec charges among them, since no contact among geometries in one clump. Still, you could write - // your own subroutine to further modify those geometry and/or own wildcards in your script, or within the force - // model. + // You could write your own subroutine to further modify those wildcards in your script, or within the force + // model. // On the other hand, if you do not need to modify the wildcards, you just need to use them for calculating // the force, then that is probably easier and with less strings attached to it. I can see this being more // useful. if (overlapDepth > 0) { // Exchange the elec charge only in physical contact - float avg_Q = (Q_A[AGeo] + Q_B[BGeo]) / 2.; - float A_change_dir = (abs(avg_Q - Q_A[AGeo]) > 1e-11) ? (avg_Q - Q_A[AGeo]) / abs(avg_Q - Q_A[AGeo]) : 0.; + float avg_Q = (Q[AOwner] + Q[BOwner]) / 2.; + float A_change_dir = + (abs(avg_Q - Q[AOwner]) > 1e-11) ? (avg_Q - Q[AOwner]) / abs(avg_Q - Q[AOwner]) : 0.; // Modify the charge they carry... the rate is 1e-8 per second - atomicAdd(Q_A + AGeo, A_change_dir * 1e-8 * ts); - atomicAdd(Q_B + BGeo, -A_change_dir * 1e-8 * ts); + atomicAdd(Q + AOwner, A_change_dir * 1e-8 * ts); + atomicAdd(Q + BOwner, -A_change_dir * 1e-8 * ts); } } )V0G0N"; diff --git a/src/demo/DEMdemo_FlexibleMesh.cpp b/src/demo/DEMdemo_FlexibleMesh.cpp index 4f3c00ec..7681ec88 100644 --- a/src/demo/DEMdemo_FlexibleMesh.cpp +++ b/src/demo/DEMdemo_FlexibleMesh.cpp @@ -12,7 +12,6 @@ // ============================================================================= #include -#include #include #include @@ -34,6 +33,8 @@ void writeFloat3VectorsToCSV(const std::string& header, size_t num_items); int main() { + std::cout << "==== DEME demo/test: DEMdemo_FlexibleMesh ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat("CSV"); @@ -56,7 +57,7 @@ int main() { {-world_size / 2, world_size / 2}, {0, world_size}); */ - // No need to add simulation `world' boundaries, b/c we'll add a cylinderical container manually + // No need to add simulation `world' boundaries, b/c we'll add a box-shaped container manually DEMSim.InstructBoxDomainBoundingBC("none", mat_type_mesh); // Now manually add boundaries (you can choose to add them automatically using InstructBoxDomainBoundingBC, too) auto walls = DEMSim.AddExternalObject(); @@ -116,9 +117,9 @@ int main() { // If you call SetFamilyPrescribedPosition and SetFamilyPrescribedQuaternion without specifying what position it // actually take, then its position is kept `as is' during simulation, without being affected by physics. It's // similar to fixing it but allows you manually impose velocities (which may have implications on your force model), - // even though the velocity won't change its location. If you prescribe position by do not prescribe velocities, it + // even though the velocity won't change its location. If you prescribe position but do not prescribe velocities, it // may make the object accumulate `phantom' velocity and de-stabilize the simulation. Fixing both position and - // velocity is equivalent to fixing the family. + // velocity is equivalent to fixing the family, which is slightly different from the `as-is' style we did here. DEMSim.SetFamilyPrescribedPosition(1); DEMSim.SetFamilyPrescribedQuaternion(1); DEMSim.SetFamilyPrescribedLinVel(1); @@ -132,7 +133,7 @@ int main() { auto max_z_finder = DEMSim.CreateInspector("clump_max_z"); float step_size = 5e-6; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Mesh has user-enforced deformation that the solver won't expect, so it can be better to allow larger safety // adder. diff --git a/src/demo/DEMdemo_FractureBox.cpp b/src/demo/DEMdemo_FractureBox.cpp index 668f27e8..341240f8 100644 --- a/src/demo/DEMdemo_FractureBox.cpp +++ b/src/demo/DEMdemo_FractureBox.cpp @@ -8,7 +8,6 @@ // ============================================================================= #include -#include #include #include @@ -23,15 +22,17 @@ using namespace deme; const double math_PI = 3.1415927; int main() { + std::cout << "==== DEME demo/test: DEMdemo_FractureBox ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::VEL); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); DEMSim.SetContactOutputContent(OWNER | FORCE | CNT_WILDCARD); // This demo could lead to large numbers of per-sphere contacts, so to be safe... - DEMSim.SetErrorOutAvgContacts(200); + DEMSim.SetErrorOutAvgContacts(400); // E, nu, CoR, mu, Crr... auto mat_type_container = @@ -67,7 +68,7 @@ int main() { double bottom = 0; double top = 0.10; - auto walls = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/funnel_left.obj"), mat_type_container); + auto walls = DEMSim.AddWavefrontMeshObject("../data/mesh/funnel_left.obj", mat_type_container); float3 move = make_float3(0.05, 0.00, 0 - 2 * sphere_rad); // z float4 rot = make_float4(0.7071, 0, 0, 0.7071); walls->Scale(make_float3(0.8, 0.1, 2.0)); @@ -117,7 +118,7 @@ int main() { std::filesystem::path out_dir = std::filesystem::current_path(); std::string nameOutFolder = "R" + std::to_string(sphere_rad) + "_Int" + std::to_string(fact_radius) + ""; - out_dir /= "DemoOutput_FractureBox_" + nameOutFolder; + out_dir /= "DemoOutput_Fracture_" + nameOutFolder; remove_all(out_dir); create_directory(out_dir); @@ -127,7 +128,7 @@ int main() { DEMSim.SetFamilyExtraMargin(1, fact_radius * sphere_rad); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // The `dry-run' option is on in this demo, which establishes the initial contact pairs while initializing. This is // needed in this demo specifically, as we'll soon modify the contact wildcards associated with these contacts. @@ -151,7 +152,7 @@ int main() { double L0; double stress; std::string nameOutFile = "data_R" + std::to_string(sphere_rad) + "_Int" + std::to_string(fact_radius) + ".csv"; - std::ofstream csvFile(out_dir / nameOutFile); + std::ofstream csvFile(nameOutFile); DEMSim.SetFamilyContactWildcardValueBoth(1, "initialLength", 0.0); // DEMSim.SetFamilyContactWildcardValueBoth(1, "damage", 0.0); @@ -217,6 +218,6 @@ int main() { DEMSim.ShowMemStats(); std::cout << "----------------------------------------" << std::endl; DEMSim.ShowTimingStats(); - std::cout << "DEMdemo_FractureBox exiting..." << std::endl; + std::cout << "Fracture demo exiting..." << std::endl; return 0; } diff --git a/src/demo/DEMdemo_GRCPrep_Part1.cpp b/src/demo/DEMdemo_GRCPrep_Part1.cpp index fb8d1fa3..6e22857f 100644 --- a/src/demo/DEMdemo_GRCPrep_Part1.cpp +++ b/src/demo/DEMdemo_GRCPrep_Part1.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -28,8 +27,10 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_GRCPrep_Part1 ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ); // Let the contact output file include "GEO_ID" geoA and geoB (not just owner IDs A and B, but their components) @@ -105,7 +106,7 @@ int main() { // Make ready for simulation float step_size = 1e-6; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't @@ -150,7 +151,7 @@ int main() { // Give ground particles a small initial velocity so they `collapse' at the start of the simulation heap_particles->SetVel(make_float3(0.00, 0, -0.05)); heap_particles->SetFamilies(heap_family); - DEMSim.UpdateClumps(); + DEMSim.Update(); std::cout << "Current number of clumps: " << DEMSim.GetNumClumps() << std::endl; // Allow for some settling diff --git a/src/demo/DEMdemo_GRCPrep_Part2.cpp b/src/demo/DEMdemo_GRCPrep_Part2.cpp index 95abae05..c1a3afa9 100644 --- a/src/demo/DEMdemo_GRCPrep_Part2.cpp +++ b/src/demo/DEMdemo_GRCPrep_Part2.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -28,8 +27,10 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_GRCPrep_Part2 ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // DEMSim.SetOutputContent(OUTPUT_CONTENT::FAMILY); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ); @@ -84,13 +85,10 @@ int main() { } // Now we load part1 clump locations from a part1 output file - const auto part1_dir = std::filesystem::current_path() / "DemoOutput_GRCPrep_Part1"; - const auto part1_clump_file = (part1_dir / "GRC_3e5.csv").string(); - const auto part1_contact_file = (part1_dir / "Contact_pairs_3e5.csv").string(); - auto part1_clump_xyz = DEMSim.ReadClumpXyzFromCsv(part1_clump_file); - auto part1_clump_quaternion = DEMSim.ReadClumpQuatFromCsv(part1_clump_file); - auto part1_pairs = DEMSim.ReadContactPairsFromCsv(part1_contact_file); - auto part1_wcs = DEMSim.ReadContactWildcardsFromCsv(part1_contact_file); + auto part1_clump_xyz = DEMSim.ReadClumpXyzFromCsv("./DemoOutput_GRCPrep_Part1/GRC_3e5.csv"); + auto part1_clump_quaternion = DEMSim.ReadClumpQuatFromCsv("./DemoOutput_GRCPrep_Part1/GRC_3e5.csv"); + auto part1_pairs = DEMSim.ReadContactPairsFromCsv("./DemoOutput_GRCPrep_Part1/Contact_pairs_3e5.csv"); + auto part1_wcs = DEMSim.ReadContactWildcardsFromCsv("./DemoOutput_GRCPrep_Part1/Contact_pairs_3e5.csv"); std::vector in_xyz; std::vector in_quat; @@ -178,7 +176,7 @@ int main() { // Make ready for simulation float step_size = 1e-6; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Error out vel is used to force the simulation to abort when something goes wrong. DEMSim.SetErrorOutVelocity(15.); diff --git a/src/demo/DEMdemo_GRCPrep_Part3.cpp b/src/demo/DEMdemo_GRCPrep_Part3.cpp index 7246d1cf..3e15b27e 100644 --- a/src/demo/DEMdemo_GRCPrep_Part3.cpp +++ b/src/demo/DEMdemo_GRCPrep_Part3.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -20,8 +19,10 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_GRCPrep_Part3 ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ); @@ -85,14 +86,10 @@ int main() { } // Now we load part2 clump locations from a part1 output file - const auto part2_clump_file = - (std::filesystem::current_path() / "DemoOutput_GRCPrep_Part2" / "GRC_3e6.csv").string(); - auto part2_clump_xyz = DEMSim.ReadClumpXyzFromCsv(part2_clump_file); - auto part2_clump_quaternion = DEMSim.ReadClumpQuatFromCsv(part2_clump_file); - // const auto part2_contact_file = - // (std::filesystem::current_path() / "DemoOutput_GRCPrep_Part2" / "Contact_pairs_3e6.csv").string(); - // auto part2_pairs = DEMSim.ReadContactPairsFromCsv(part2_contact_file); - // auto part2_wcs = DEMSim.ReadContactWildcardsFromCsv(part2_contact_file); + auto part2_clump_xyz = DEMSim.ReadClumpXyzFromCsv("./DemoOutput_GRCPrep_Part2/GRC_3e6.csv"); + auto part2_clump_quaternion = DEMSim.ReadClumpQuatFromCsv("./DemoOutput_GRCPrep_Part2/GRC_3e6.csv"); + // auto part2_pairs = DEMSim.ReadContactPairsFromCsv("./DemoOutput_GRCPrep_Part2/Contact_pairs_3e6.csv"); + // auto part2_wcs = DEMSim.ReadContactWildcardsFromCsv("./DemoOutput_GRCPrep_Part2/Contact_pairs_3e6.csv"); std::vector in_xyz; std::vector in_quat; std::vector> in_types; @@ -165,7 +162,7 @@ int main() { // Make ready for simulation double step_size = 2e-6; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.SetErrorOutVelocity(20.); // DEMSim.SetInitBinSize(scales.at(1)); @@ -184,8 +181,8 @@ int main() { DEMSim.DoDynamicsThenSync(0.3); // Doing this won't change the step size (and we don't need to in this demo), // but if the physics of the simulation changes significantly at some point, - // you can UpdateStepSize after a DoDynamicsThenSync call. - DEMSim.UpdateStepSize(step_size); + // You can change the step size after a DoDynamicsThenSync call. + DEMSim.SetTimeStepSize(step_size); // Now compress it DEMSim.EnableContactBetweenFamilies(0, 1); diff --git a/src/demo/DEMdemo_GameOfLife.cpp b/src/demo/DEMdemo_GameOfLife.cpp index 0e7644c1..2a839e58 100644 --- a/src/demo/DEMdemo_GameOfLife.cpp +++ b/src/demo/DEMdemo_GameOfLife.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,8 +21,10 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_GameOfLife ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); // Output as CSV so no post-processing is needed DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ); @@ -119,7 +120,7 @@ int main() { // init bin size may save the simulation. // DEMSim.SetInitBinSize(world_size / 100.); - DEMSim.SetInitTimeStep(1.); + DEMSim.SetTimeStepSize(1.); DEMSim.SetCDUpdateFreq(0); // Must disable this if you want to run dT and kT synchronizely, or the solver will automatically find a non-zero // update frequency that it sees fit to run it in an async fashion. @@ -132,7 +133,7 @@ int main() { create_directory(out_dir); std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); - for (int i = 0; i < 3000; i++) { + for (int i = 0; i < 1000; i++) { char filename[100]; sprintf(filename, "DEMdemo_output_%04d.csv", i); DEMSim.WriteSphereFile(out_dir / filename); diff --git a/src/demo/DEMdemo_HopperSphereCylinder.cpp b/src/demo/DEMdemo_HopperSphereCylinder.cpp index 850fecea..446066ec 100644 --- a/src/demo/DEMdemo_HopperSphereCylinder.cpp +++ b/src/demo/DEMdemo_HopperSphereCylinder.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -23,9 +22,11 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_HopperSphereCylinder ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.UseFrictionalHertzianModel(); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ | OUTPUT_CONTENT::VEL | OUTPUT_CONTENT::FAMILY); DEMSim.EnsureKernelErrMsgLineNum(); @@ -34,7 +35,6 @@ int main() { DEMSim.SetCollectAccRightAfterForceCalc(true); DEMSim.SetErrorOutAvgContacts(80); - // DEMSim.SetExpandSafetyAdder(0.5); int totalCyl = 10500; int totalSph = 7000 + totalCyl; @@ -95,7 +95,7 @@ int main() { float step_size = 5.0e-6; DEMSim.InstructBoxDomainDimension({-0.10, 0.10}, {-0.02, 0.02}, {-0.50, 1.0}); DEMSim.InstructBoxDomainBoundingBC("top_open", mat_type_walls); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't @@ -105,16 +105,16 @@ int main() { // Loaded meshes are by-default fixed - auto fixed_left = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/funnel_left.obj"), mat_type_flume); + auto fixed_left = DEMSim.AddWavefrontMeshObject("../data/mesh/funnel_left.obj", mat_type_flume); float3 move = make_float3(-hopperW / 2.0, 0, -0.01); float4 rot = make_float4(0.7071, 0, 0, 0.7071); fixed_left->Move(move, rot); - auto fixed_right = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/funnel_left.obj"), mat_type_flume); + auto fixed_right = DEMSim.AddWavefrontMeshObject("../data/mesh/funnel_left.obj", mat_type_flume); move = make_float3(gateWidth + hopperW / 2.0, 0, -0.01); fixed_right->Move(move, rot); - auto gate = DEMSim.AddWavefrontMeshObject(GetDEMEDataFile("mesh/funnel_left.obj"), mat_type_flume); + auto gate = DEMSim.AddWavefrontMeshObject("../data/mesh/funnel_left.obj", mat_type_flume); gate->Move(make_float3(gateWidth / 2, 0, -0.011), rot); fixed_left->SetFamily(10); @@ -265,7 +265,7 @@ int main() { the_pile->SetVel(make_float3(-0.00, 0.0, -0.80)); the_pile->SetFamily(100); - DEMSim.UpdateClumps(); + DEMSim.Update(); std::cout << "Total num of particles: " << (int)DEMSim.GetNumClumps() << std::endl; actualTotalSpheres = (int)DEMSim.GetNumClumps(); @@ -353,7 +353,7 @@ int main() { the_pile->SetVel(make_float3(-0.00, 0.0, -0.80)); the_pile->SetFamily(99); - DEMSim.UpdateClumps(); + DEMSim.Update(); std::cout << "Total num of particles: " << (int)DEMSim.GetNumClumps() << std::endl; actualTotalSpheres = (int)DEMSim.GetNumClumps(); diff --git a/src/demo/DEMdemo_HopperSphereMeshedCylinder.cpp b/src/demo/DEMdemo_HopperSphereMeshedCylinder.cpp new file mode 100644 index 00000000..a617b7da --- /dev/null +++ b/src/demo/DEMdemo_HopperSphereMeshedCylinder.cpp @@ -0,0 +1,243 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Hopper discharge with analytical sphere particles and low-poly mesh cylinders. +// Cylinders have an octagonal cross section, flat ends, and 28 triangles each. +// Spheres and cylinders settle together above a fixed mesh plug. Disabling the +// plug's contacts and output opens the outlet for discharge into the lower bin. +// Run with --smoke-test for a small bed and a short, self-checking discharge. +// ============================================================================= + +#include "DEM/API.h" +#include "DEM/utils/Samplers.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace deme; + +int main(int argc, char* argv[]) { + const bool smoke_test = argc == 2 && std::string(argv[1]) == "--smoke-test"; + if (argc > 1 && !smoke_test) { + std::cout << "Usage: DEMdemo_HopperSphereMeshedCylinder [--smoke-test]" << std::endl; + return std::string(argv[1]) == "--help" ? 0 : 1; + } + std::cout << "==== DEME demo/test: DEMdemo_HopperSphereMeshedCylinder ====" << std::endl; + + DEMSolver DEMSim; + DEMSim.UseFrictionalHertzianModel(); + DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat("CSV"); + DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ | OUTPUT_CONTENT::VEL | OUTPUT_CONTENT::FAMILY); + DEMSim.SetMeshOutputFormat("VTK"); + // Attach each triangle's owner family so cylinders, hopper walls, and the gate can be colored separately. + DEMSim.SetMeshOutputContent({"FAMILY"}); + DEMSim.SetNoForceRecord(); + // Cylinders collide with spheres, other cylinders, the mesh gate, and analytical bin walls. + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetMeshParticlesLowPoly(true); + DEMSim.SetErrorOutAvgContacts(80); + + const size_t total_cylinders = smoke_test ? 24 : 5250; + const size_t total_spheres = smoke_test ? 16 : 3500; + const double radius_cylinder = 0.002; + const double cylinder_length = 0.008; + const double density_cylinder = 1128; + const double radius_sphere = 0.003; + const double density_sphere = 1592; + const float step_size = 5e-6f; + const double hopper_width = 0.04; + const double gate_width = 0.1295; + constexpr unsigned int cylinder_family = 100; + constexpr unsigned int sphere_family = 99; + constexpr unsigned int fixed_family = 10; + constexpr unsigned int closed_gate_family = 3; + constexpr unsigned int disabled_gate_family = 4; + + auto mat_flume = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}, {"CoR", 0.60}}); + auto mat_walls = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}, {"CoR", 0.60}}); + auto mat_spheres = DEMSim.LoadMaterial({{"E", 1e6}, {"nu", 0.35}, {"CoR", 0.20}, {"mu", 0.40}, {"Crr", 0.04}}); + auto mat_cylinders = DEMSim.LoadMaterial({{"E", 1e6}, {"nu", 0.35}, {"CoR", 0.20}, {"mu", 0.30}, {"Crr", 0.03}}); + // Apply the wall/flume contact coefficients to both particle materials. + for (const auto& wall : {mat_walls, mat_flume}) { + for (const auto& particle : {mat_spheres, mat_cylinders}) { + DEMSim.SetMaterialPropertyPair("CoR", wall, particle, 0.20); + DEMSim.SetMaterialPropertyPair("Crr", wall, particle, 0.05); + DEMSim.SetMaterialPropertyPair("mu", wall, particle, 0.30); + } + } + + DEMSim.InstructBoxDomainDimension({-0.10, 0.10}, {-0.02, 0.02}, {-0.50, 1.0}); + DEMSim.InstructBoxDomainBoundingBC("top_open", mat_walls); + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetMaxVelocity(25.); + DEMSim.SetInitBinSize(2 * radius_cylinder); + DEMSim.SetFamilyFixed(fixed_family); + DEMSim.SetFamilyFixed(closed_gate_family); + // Keep the disabled plug stationary, non-contacting, and absent from output for every family in the solver. + DEMSim.SetFamilyFixed(disabled_gate_family); + DEMSim.DisableFamilyOutput(disabled_gate_family); + for (unsigned int family = 0; family < NUM_AVAL_FAMILIES; family++) { + DEMSim.DisableContactBetweenFamilies(disabled_gate_family, family); + } + + // The three rectangular mesh pieces form the hopper floor and its removable central plug. + const auto funnel_file = GetDEMEDataFile("mesh/funnel_left.obj"); + const float4 funnel_rotation = make_float4(std::sqrt(0.5f), 0, 0, std::sqrt(0.5f)); + auto left = DEMSim.AddWavefrontMeshObject(funnel_file, mat_flume); + left->Move(make_float3(-hopper_width / 2, 0, -0.01), funnel_rotation); + left->SetFamily(fixed_family); + auto right = DEMSim.AddWavefrontMeshObject(funnel_file, mat_flume); + right->Move(make_float3(gate_width + hopper_width / 2, 0, -0.01), funnel_rotation); + right->SetFamily(fixed_family); + auto gate = DEMSim.AddWavefrontMeshObject(funnel_file, mat_flume); + gate->Move(make_float3(gate_width / 2, 0, -0.011), funnel_rotation); + gate->SetFamily(closed_gate_family); + auto gate_tracker = DEMSim.Track(gate); + // The floor pieces overlap the closed gate; their mutual contacts do not contribute to the bed dynamics. + DEMSim.DisableContactBetweenFamilies(fixed_family, closed_gate_family); + DEMSim.DisableContactBetweenFamilies(fixed_family, fixed_family); + + // The template is centered on its principal axes, with its long axis along X. + // Explicit circular-cylinder mass properties describe the nominal particle dimensions and density. + auto cylinder_type = DEMSim.LoadMeshType(GetDEMEDataFile("mesh/cyl_x_r1_h2_8sides.obj"), mat_cylinders, false); + cylinder_type->Scale(make_float3(cylinder_length / 2, radius_cylinder, radius_cylinder)); + const float cylinder_mass = PI * radius_cylinder * radius_cylinder * cylinder_length * density_cylinder; + const float axial_moi = 0.5 * cylinder_mass * radius_cylinder * radius_cylinder; + const float transverse_moi = + cylinder_mass * (3 * radius_cylinder * radius_cylinder + cylinder_length * cylinder_length) / 12; + cylinder_type->SetMass(cylinder_mass); + cylinder_type->SetMOI(make_float3(axial_moi, transverse_moi, transverse_moi)); + cylinder_type->SetConvex(true); + cylinder_type->SetFamily(cylinder_family); + + const float sphere_mass = 4. / 3. * PI * radius_sphere * radius_sphere * radius_sphere * density_sphere; + auto sphere_type = DEMSim.LoadSphereType(sphere_mass, radius_sphere, mat_spheres); + + // Build the complete bed before Initialize so mesh topology and mass tables are available during JIT compilation. + // An anisotropic grid leaves gaps between horizontal cylinders. Sorting by height fills the lower layers first. + const float half_width = smoke_test ? 0.02f : 0.10f; + const float half_depth = smoke_test ? 0.013f : 0.02f; + GridSampler cylinder_sampler(make_float3(1.1 * cylinder_length, 2.2 * radius_cylinder, 2.2 * radius_cylinder)); + auto cylinder_positions = cylinder_sampler.SampleBox( + make_float3(0, 0, 0.16), + make_float3(half_width - 0.55 * cylinder_length, half_depth - 1.1 * radius_cylinder, 0.14)); + std::stable_sort(cylinder_positions.begin(), cylinder_positions.end(), + [](const float3& a, const float3& b) { return a.z < b.z; }); + if (cylinder_positions.size() < total_cylinders) { + DEME_ERROR("Cylinder loading region is too small for %zu particles.", total_cylinders); + } + cylinder_positions.resize(total_cylinders); + std::shared_ptr first_cylinder_tracker; + for (const auto& pos : cylinder_positions) { + auto cylinder = DEMSim.AddMeshFromTemplate(cylinder_type, pos); + if (!first_cylinder_tracker) { + first_cylinder_tracker = DEMSim.Track(cylinder); + } + } + + // Place analytical spheres above the cylinders with a gap; both particle types move from the first step. + const float sphere_bottom = cylinder_positions.back().z + radius_cylinder + radius_sphere + 0.01; + const float sphere_top = 0.60f; + GridSampler sphere_sampler(2.2 * radius_sphere); + auto sphere_positions = + sphere_sampler.SampleBox(make_float3(0, 0, (sphere_bottom + sphere_top) / 2), + make_float3(half_width - 1.1 * radius_sphere, half_depth - 1.1 * radius_sphere, + (sphere_top - sphere_bottom) / 2)); + std::stable_sort(sphere_positions.begin(), sphere_positions.end(), + [](const float3& a, const float3& b) { return a.z < b.z; }); + if (sphere_positions.size() < total_spheres) { + DEME_ERROR("Sphere loading region is too small for %zu particles.", total_spheres); + } + sphere_positions.resize(total_spheres); + auto spheres = DEMSim.AddClumps(sphere_type, sphere_positions); + spheres->SetFamily(sphere_family); + spheres->SetVel(make_float3(0, 0, -0.8)); + auto max_speed = DEMSim.CreateInspector("max_absv"); + DEMSim.Initialize(); + + // Mesh instances were added consecutively; a single bulk velocity update initializes their downward motion. + const auto first_cylinder = first_cylinder_tracker->GetOwnerID(); + DEMSim.SetOwnerVelocity(first_cylinder, std::vector(total_cylinders, make_float3(0, 0, -0.8))); + const float gate_initial_z = gate_tracker->Pos().z; + std::cout << "Particles: " << total_cylinders << " meshed cylinders (" << cylinder_type->GetNumTriangles() + << " triangles each), " << total_spheres << " analytical spheres" << std::endl; + + auto out_dir = std::filesystem::current_path() / "DemoOutput_HopperSphereMeshedCylinder"; + if (smoke_test) { + out_dir += "_smoke"; + } + std::filesystem::create_directories(out_dir); + unsigned int frame = 0; + double elapsed = 0; + // Write matching CSV and VTK frames through every phase; the disabled plug is filtered from mesh output. + auto write_frame = [&]() { + char sphere_file[100], mesh_file[100]; + std::snprintf(sphere_file, sizeof(sphere_file), "DEMdemo_spheres_%04u.csv", frame); + std::snprintf(mesh_file, sizeof(mesh_file), "DEMdemo_mesh_%04u.vtk", frame); + DEMSim.WriteSphereFile(out_dir / sphere_file); + DEMSim.WriteMeshFile(out_dir / mesh_file); + std::cout << "Frame " << frame++ << ": t = " << elapsed << " s, max speed = " << max_speed->GetValue() + << std::endl; + // Report host and device memory at output intervals throughout settling and discharge. + DEMSim.ShowMemStats(); + }; + // Advance each phase in integer step counts, with output at regular simulation-time intervals. + const unsigned int output_steps = static_cast(std::llround(0.01 / step_size)); + auto run_phase = [&](const char* name, unsigned int steps) { + std::cout << name << std::endl; + while (steps > 0) { + const auto chunk = std::min(steps, output_steps); + const double duration = chunk * static_cast(step_size); + DEMSim.DoDynamicsThenSync(duration); + elapsed += duration; + steps -= chunk; + write_frame(); + } + }; + auto steps_for = [&](double duration) { return static_cast(std::llround(duration / step_size)); }; + const auto start = std::chrono::high_resolution_clock::now(); + write_frame(); + run_phase("Settling spheres and cylinders", steps_for(smoke_test ? 0.16 : 0.70)); + + const auto discharge_steps = steps_for(smoke_test ? 0.14 : 7.50); + // Settling ends synchronized. Switch the plug to its disabled family before any discharge step or output. + DEMSim.ChangeFamily(closed_gate_family, disabled_gate_family); + write_frame(); + run_phase("Discharging with the plug disabled", discharge_steps); + DEMSim.WaitForPendingOutput(); + + if (smoke_test) { + // Check the representation, free cylinder motion, finite particle states, and stationary disabled plug. + bool valid = DEMSim.GetNumClumps() == total_spheres && cylinder_type->GetNumTriangles() == 28; + const auto positions = DEMSim.GetOwnerPosition(0, static_cast(DEMSim.GetNumOwners())); + const auto velocities = DEMSim.GetOwnerVelocity(0, static_cast(DEMSim.GetNumOwners())); + for (size_t i = 0; i < positions.size(); i++) { + valid = valid && std::isfinite(positions[i].x) && std::isfinite(positions[i].y) && + std::isfinite(positions[i].z) && std::isfinite(velocities[i].x) && std::isfinite(velocities[i].y) && + std::isfinite(velocities[i].z); + } + valid = valid && gate_tracker->GetFamily() == disabled_gate_family && + std::abs(gate_tracker->Pos().z - gate_initial_z) < 1e-4 && + first_cylinder_tracker->Pos().z < cylinder_positions.front().z - radius_cylinder; + std::cout << (valid ? "PASS" : "FAIL") << ": hopper mesh-cylinder smoke test" << std::endl; + if (!valid) { + return 1; + } + } + const std::chrono::duration wall_time = std::chrono::high_resolution_clock::now() - start; + std::cout << "Simulated " << elapsed << " s in " << wall_time.count() << " s wall time" << std::endl; + DEMSim.ShowTimingStats(); + DEMSim.ShowMemStats(); + std::cout << "DEMdemo_HopperSphereMeshedCylinder exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_Indentation.cpp b/src/demo/DEMdemo_Indentation.cpp index ceeb436c..edd91c77 100644 --- a/src/demo/DEMdemo_Indentation.cpp +++ b/src/demo/DEMdemo_Indentation.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -71,8 +70,10 @@ inline void buildContactMap(std::vector>& map, } int main() { + std::cout << "==== DEME demo/test: DEMdemo_Indentation ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); @@ -158,7 +159,7 @@ int main() { std::cout << "Total num of particles: " << num_particles << std::endl; auto particle_tracker = DEMSim.Track(particles); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.SetCDUpdateFreq(20); // You usually don't have to worry about initial bin size. But sometimes if you can set the init bin size so that diff --git a/src/demo/DEMdemo_MeshCollide.cpp b/src/demo/DEMdemo_MeshCollide.cpp new file mode 100644 index 00000000..fa32d2ba --- /dev/null +++ b/src/demo/DEMdemo_MeshCollide.cpp @@ -0,0 +1,145 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// A small-scale demo that tests meshed particle collisions. +// ============================================================================= + +#include +#include +#include +#include +#ifdef DEME_HAS_VISUALIZER + #include "VisualizerDemoLoop.h" +#endif + +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_MeshCollide ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("DEBUG"); + // DEMSim.UseFrictionlessHertzianModel(); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT", "NORMAL", "TORQUE", "CNT_WILDCARD"}); + DEMSim.InstructBoxDomainDimension(10, 10, 4); + // Note!! If you want meshes to have contacts, set this to true!! + // If not, meshes will not have contacts with each other or analytical boundaries (but still have contacts with + // clumps). + DEMSim.SetMeshUniversalContact(true); + // DEMSim.SetSimplePatchCombination(true); + + // Special material: has a cohesion param + auto mat_type_1 = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.3}, {"Crr", 0.0}}); + auto mat_type_2 = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.4}, {"CoR", 0.4}, {"mu", 0.3}, {"Crr", 0.0}}); + // If you don't have this line, then CoR between thw 2 materials will take average when they are in contact + DEMSim.SetMaterialPropertyPair("CoR", mat_type_1, mat_type_2, 0.6); + + // Add a bottom plane + DEMSim.AddBCPlane(make_float3(0, 0, -1.8), make_float3(0, 0, 1), mat_type_2); + + auto particle1 = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type_1); + particle1->SetFamily(0); + particle1->SetInitPos(make_float3(-0.4, 0, -1.25)); + particle1->SetMass(10000.); + particle1->SetMOI(make_float3(2000., 2000., 2000.)); + auto tracker1 = DEMSim.Track(particle1); + + auto particle2 = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type_1); + particle2->SetFamily(0); + particle2->SetInitPos(make_float3(0.4, 0, 0)); + particle2->SetMass(10000.); + particle2->SetMOI(make_float3(2000., 2000., 2000.)); + auto tracker2 = DEMSim.Track(particle2); + + float step_time = 1e-3; + DEMSim.SetTimeStepSize(step_time); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetExpandSafetyType("auto"); + // DEMSim.DisableAdaptiveBinSize(); + // DEMSim.SetInitBinSize(10); + DEMSim.Initialize(); + +#ifdef DEME_HAS_VISUALIZER + // Display cadence follows wall time so camera input stays responsive between simulation output frames. + DEMVisualizer visualizer(DEMSim); + visualizer.SetCameraPosition(make_float3(5.f, 5.f, 3.f)); + visualizer.SetCameraTarget(make_float3(0.f, 0.f, -0.5f)); + visualizer.Initialize(); + VisualizerDemoLoop viewer_loop; +#endif + + // Ready simulation + path out_dir = current_path(); + out_dir /= "DemoOutput_MeshCollide"; + create_directory(out_dir); + + unsigned int ID1 = tracker1->GetOwnerIDs()[0]; + unsigned int ID2 = tracker2->GetOwnerIDs()[0]; + + float frame_time = 1e-2; + int frame = 0; + int frame_step = (int)(frame_time / step_time); + double final_time = 1.; + for (int i = 0; i < (int)(final_time / step_time); i++) { + if (i % frame_step == 0) { + frame++; + std::cout << "Frame: " << frame << std::endl; + + char meshfilename[100]; + sprintf(meshfilename, "DEMdemo_mesh_%04d.vtk", frame); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + // Test if family changer works + float3 pos1 = tracker1->Pos(); + float3 pos2 = tracker2->Pos(); + float3 vel1 = tracker1->Vel(); + float3 vel2 = tracker2->Vel(); + float3 angvel1 = tracker1->AngVelGlobal(); + float3 angvel2 = tracker2->AngVelGlobal(); + + // Test getting all contact force pairs concerning different trackers + std::vector forces_mesh, torques_mesh, points_mesh; + // GetOwnerContactForces is another way to query the contact forces, if you don't want to write them to + // files. If a contact involves at least one of the owner IDs provided as the first arg of + // GetOwnerContactForces, it will be outputted. Note if a contact involves two IDs of the user-provided + // list, then the force for that contact will be given as the force experienced by whichever owner that + // appears earlier in the ID list. + DEMSim.GetOwnerContactForces({ID1, ID2}, points_mesh, forces_mesh, torques_mesh); + + std::cout << "----------------------------------------" << std::endl; + std::cout << "Particle 1 Z coord is " << pos1.z << std::endl; + std::cout << "Particle 2 Z coord is " << pos2.z << std::endl; + std::cout << "Particle 1 ang vel is " << angvel1.x << ", " << angvel1.y << ", " << angvel1.z << std::endl; + std::cout << "Particle 2 ang vel is " << angvel2.x << ", " << angvel2.y << ", " << angvel2.z << std::endl; + if (points_mesh.size() > 0) { + std::cout << "Two meshes collide, one contact is at (" << points_mesh[0].x << ", " << points_mesh[0].y + << ", " << points_mesh[0].z << ")." << std::endl; + std::cout << "The contact force is (" << forces_mesh[0].x << ", " << forces_mesh[0].y << ", " + << forces_mesh[0].z << ")." << std::endl; + std::cout << "The contact torque is (" << torques_mesh[0].x << ", " << torques_mesh[0].y << ", " + << torques_mesh[0].z << "). We expect this torque to be 0 since Crr is 0." << std::endl; + } + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + } + +#ifdef DEME_HAS_VISUALIZER + viewer_loop.Update(visualizer); +#endif + DEMSim.DoDynamics(step_time); + } + + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ShowTimingStats(); + std::cout << "DEMdemo_MeshCollide exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_MeshFalling.cpp b/src/demo/DEMdemo_MeshFalling.cpp new file mode 100644 index 00000000..9d95dd0c --- /dev/null +++ b/src/demo/DEMdemo_MeshFalling.cpp @@ -0,0 +1,225 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// A demo featuring multiple particles (boxes, spheres, cones, cylinders) +// falling onto an analytical plane, testing triangle-triangle contacts +// between diverse mesh types and mesh-plane contacts. +// ============================================================================= + +#include +#include +#include +#include +#ifdef DEME_HAS_VISUALIZER + #include "VisualizerDemoLoop.h" +#endif + +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +const double math_PI = 3.1415927; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_MeshFalling ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT", "NORMAL", "TORQUE", "CNT_WILDCARD"}); + DEMSim.InstructBoxDomainDimension(20, 20, 15); + + DEMSim.SetGPUTimersEnabled(true); + + // Enable mesh-mesh contacts so boxes can collide with each other + DEMSim.SetMeshUniversalContact(true); + + // Define material properties + auto mat_box = DEMSim.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.4}, {"mu", 0.4}, {"Crr", 0.1}}); + auto mat_plane = DEMSim.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.4}, {"mu", 0.3}, {"Crr", 0.1}}); + + // Add a bottom plane at z = 0 + DEMSim.AddBCPlane(make_float3(0, 0, 0), make_float3(0, 0, 1), mat_plane); + + // Create multiple particles in a grid pattern above the plane + const int num_particles_x = 6; + const int num_particles_y = 6; + const float particle_spacing = 2.0; + const float initial_height = 2.0; + const float base_size = 0.5; // Base scale for all meshes + const float cylinder_scale_factor = 0.5; // Cylinders scaled down since they're taller + + std::vector> trackers; + + // Random number generator for variations + const unsigned int seed = 12345u; + std::mt19937 gen(seed); + std::uniform_real_distribution<> pos_dist(-0.1, 0.1); + std::uniform_real_distribution<> rot_dist(-0.2, 0.2); + std::uniform_real_distribution<> scale_dist(0.8, 2.0); // Scale variation + std::uniform_int_distribution<> mesh_type_dist(0, 3); // 4 mesh types + + for (int i = 0; i < num_particles_x; i++) { + for (int j = 0; j < num_particles_y; j++) { + for (int k = 0; k < 2; k++) { + float x = (i - num_particles_x / 2.0 + 0.5) * particle_spacing + pos_dist(gen); + float y = (j - num_particles_y / 2.0 + 0.5) * particle_spacing + pos_dist(gen); + float z = initial_height + (i + j) * 0.5 + pos_dist(gen) * 2 + k * 2.5; + + // Select mesh type randomly: 0=cube, 1=sphere, 2=cone, 3=cylinder + int mesh_type = mesh_type_dist(gen); + std::shared_ptr particle; + + if (mesh_type == 0) { + // Cube with non-uniform scaling + particle = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_box); + float scale_x = base_size * scale_dist(gen); + float scale_y = base_size * scale_dist(gen); + float scale_z = base_size * scale_dist(gen); + particle->Scale(make_float3(scale_x, scale_y, scale_z)); // Non-uniform scaling + + // Set mass and MOI for the box (approximate as uniform density) + float mass = 1000.0 * scale_x * scale_y * scale_z; + float moi_x = mass * (scale_y * scale_y + scale_z * scale_z) / 12.0; + float moi_y = mass * (scale_x * scale_x + scale_z * scale_z) / 12.0; + float moi_z = mass * (scale_x * scale_x + scale_y * scale_y) / 12.0; + particle->SetMass(mass); + particle->SetMOI(make_float3(moi_x, moi_y, moi_z)); + } else if (mesh_type == 1) { + // Sphere (unit sphere in mesh) + particle = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/sphere.obj").string(), mat_box); + float scale = base_size * scale_dist(gen); + particle->Scale(scale); + + // Set mass and MOI for sphere + float mass = 1000.0 * (4.0 / 3.0) * math_PI * scale * scale * scale; + float moi = 0.4 * mass * scale * scale; // MOI for sphere + particle->SetMass(mass); + particle->SetMOI(make_float3(moi, moi, moi)); + } else if (mesh_type == 2) { + // Cone (height ~1, radius ~1 in mesh) + particle = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cone.obj").string(), mat_box); + // Unit cone's CoM is at this location... + particle->InformCentroidPrincipal(make_float3(0, 0, 3. / 4.), make_float4(0, 0, 0, 1)); + float scale = base_size * scale_dist(gen); + particle->Scale(scale); + + // Set mass and MOI for cone (approximate) + float mass = 1000.0 * (1.0 / 3.0) * math_PI * scale * scale * scale; + float moi_base = 0.3 * mass * scale * scale; // Approximate MOI + float moi_height = 0.15 * mass * scale * scale; + particle->SetMass(mass); + particle->SetMOI(make_float3(moi_base, moi_base, moi_height)); + } else { + // Cylinder (radius ~1, height ~2 in mesh) + particle = + DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cyl_r1_h2.obj").string(), mat_box); + float scale = base_size * cylinder_scale_factor * scale_dist(gen); + particle->Scale(scale); + + // Set mass and MOI for cylinder + float radius = scale; + float height = 2.0 * scale; + float mass = 1000.0 * math_PI * radius * radius * height; + float moi_radial = mass * (3.0 * radius * radius + height * height) / 12.0; + float moi_axial = 0.5 * mass * radius * radius; + particle->SetMass(mass); + particle->SetMOI(make_float3(moi_radial, moi_radial, moi_axial)); + } + + particle->SetFamily(0); + particle->SetInitPos(make_float3(x, y, z)); + + // Add small initial rotation for more interesting dynamics + particle->SetInitQuat(make_float4(rot_dist(gen), rot_dist(gen), rot_dist(gen), 1.0)); + + auto tracker = DEMSim.Track(particle); + trackers.push_back(tracker); + } + } + } + + float step_time = 2e-4; + DEMSim.SetTimeStepSize(step_time); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.Initialize(); + +#ifdef DEME_HAS_VISUALIZER + // Display cadence follows wall time so camera input stays responsive between simulation output frames. + DEMVisualizer visualizer(DEMSim); + visualizer.SetCameraPosition(make_float3(12.f, 12.f, 10.f)); + visualizer.SetCameraTarget(make_float3(0.f, 0.f, 3.f)); + visualizer.Initialize(); + VisualizerDemoLoop viewer_loop; +#endif + + // Setup output directory + path out_dir = current_path(); + out_dir /= "DemoOutput_MeshFalling"; + create_directory(out_dir); + + float frame_time = 1e-2; + int frame = 0; + int frame_step = (int)(frame_time / step_time); + double final_time = 3.0; // 3 seconds of simulation + + std::cout << "Starting simulation with " << num_particles_x * num_particles_y << " particles (diverse meshes)" + << std::endl; + std::cout << "Frame time: " << frame_time << " s, Time step: " << step_time << " s" << std::endl; + std::cout << "Total frames: " << (int)(final_time / frame_time) << std::endl; + + for (int i = 0; i < (int)(final_time / step_time); i++) { + if (i % frame_step == 0) { + frame++; + std::cout << "Frame: " << frame << " (t = " << i * step_time << " s)" << std::endl; + + char meshfilename[100]; + sprintf(meshfilename, "DEMdemo_meshes_%04d.vtk", frame); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + // Print some statistics + float avg_height = 0.0; + double sum_speed = 0.0; + double max_speed = 0.0; + for (size_t k = 0; k < trackers.size(); k++) { + float3 pos = trackers[k]->Pos(); + float3 vel = trackers[k]->Vel(); + double speed = std::sqrt(static_cast(vel.x) * vel.x + static_cast(vel.y) * vel.y + + static_cast(vel.z) * vel.z); + avg_height += pos.z; + sum_speed += speed; + if (speed > max_speed) { + max_speed = speed; + } + } + double mean_speed = (trackers.empty()) ? 0.0 : (sum_speed / trackers.size()); + avg_height /= trackers.size(); + + std::cout << " Average height: " << avg_height << " m" << std::endl; + std::cout << " Mean speed: " << mean_speed << " m/s" << std::endl; + std::cout << " Max speed: " << max_speed << " m/s" << std::endl; + + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + } + +#ifdef DEME_HAS_VISUALIZER + viewer_loop.Update(visualizer); +#endif + DEMSim.DoDynamics(step_time); + } + + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ShowTimingStats(); + std::cout << "DEMdemo_MeshFalling exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_Mixer.cpp b/src/demo/DEMdemo_Mixer.cpp index 2e2810cb..70f9b254 100644 --- a/src/demo/DEMdemo_Mixer.cpp +++ b/src/demo/DEMdemo_Mixer.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,13 +21,16 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Mixer ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(STEP_METRIC); - // For general use cases, you want to set the verbosity to INFO: It's also a bit faster than STEP_METRIC. - // DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("METRIC"); + // For general use cases, you want to set the verbosity to INFO: It's also a bit faster than "METRIC". + // DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); + // DEMSim.SetSimplePatchCombination(true); // If you don't need individual force information, then this option makes the solver run a bit faster. DEMSim.SetNoForceRecord(); @@ -82,21 +84,18 @@ int main() { DEMSim.AddClumps(template_granular, input_xyz); std::cout << "Total num of particles: " << input_xyz.size() << std::endl; - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.SetCDUpdateFreq(40); - // Mixer has a big angular velocity-contributed linear speed at its blades, this is something the solver do not - // account for, for now. And that means it needs to be added as an estimated value. - DEMSim.SetExpandSafetyAdder(2.0); + DEMSim.SetErrorOutVelocity(20.); // You usually don't have to worry about initial bin size. In very rare cases, init bin size is so bad that auto bin // size adaption is effectless, and you should notice in that case kT runs extremely slow. Then in that case setting // init bin size may save the simulation. // DEMSim.SetInitBinSize(25 * granular_rad); + // The following parameters control how the solver adapts its update frequency in response to max velocity changes. + // For most end users, using the default is fine; here we just show an example. DEMSim.SetCDNumStepsMaxDriftMultipleOfAvg(1.2); DEMSim.SetCDNumStepsMaxDriftAheadOfAvg(6); - DEMSim.SetSortContactPairs(true); - // DEMSim.DisableAdaptiveBinSize(); - DEMSim.SetErrorOutVelocity(20.); // Force the solver to error out if something went crazy. A good practice to add them, but not necessary. DEMSim.SetErrorOutAvgContacts(50); @@ -127,11 +126,13 @@ int main() { std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); for (float t = 0; t < sim_end; t += frame_time) { std::cout << "Frame: " << currframe << std::endl; - char filename[100], meshfilename[100], cnt_filename[100]; + char filename[100], analytical_filename[100], meshfilename[100], cnt_filename[100]; sprintf(filename, "DEMdemo_output_%04d.csv", currframe); + sprintf(analytical_filename, "DEMdemo_analytical_%04d.vtk", currframe); sprintf(meshfilename, "DEMdemo_mesh_%04d.vtk", currframe); sprintf(cnt_filename, "Contact_pairs_%04d.csv", currframe++); DEMSim.WriteSphereFile(out_dir / filename); + DEMSim.WriteAnalyticalFile(out_dir / analytical_filename); DEMSim.WriteMeshFile(out_dir / meshfilename); // DEMSim.WriteContactFile(out_dir / cnt_filename); diff --git a/src/demo/DEMdemo_MixerCubes.cpp b/src/demo/DEMdemo_MixerCubes.cpp new file mode 100644 index 00000000..0f1e4001 --- /dev/null +++ b/src/demo/DEMdemo_MixerCubes.cpp @@ -0,0 +1,189 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// This demo is the mixer example with the granular clumps replaced by +// cube-shaped mesh particles. Mesh universal contact is enabled so the cubes +// can contact the mixer, the cylindrical chamber, and each other as meshes. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_MixerCubes ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("METRIC"); + // For general use cases, you want to set the verbosity to INFO: It's also a bit faster than "METRIC". + // DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); + DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); + DEMSim.SetMeshOutputContent(MESH_OUTPUT_CONTENT::ABSV | MESH_OUTPUT_CONTENT::OWNER); + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetMeshParticlesLowPoly(true); + DEMSim.SetSimplePatchCombination(true); + + // If you don't need individual force information, then this option makes the solver run a bit faster. + DEMSim.SetNoForceRecord(); + + // E, nu, CoR, mu, Crr... + auto mat_type_mixer = DEMSim.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.0}}); + auto mat_type_granular = DEMSim.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.2}, {"Crr", 0.0}}); + // If you don't have this line, then mu between mixer material and granular material will be 0.35 (average of the + // two). + DEMSim.SetMaterialPropertyPair("mu", mat_type_mixer, mat_type_granular, 0.5); + + float step_size = 5e-6; + const double world_size = 1; + const float chamber_height = world_size / 3.; + const float fill_height = chamber_height; + const float chamber_bottom = -world_size / 2.; + const float fill_bottom = chamber_bottom + chamber_height; + + DEMSim.InstructBoxDomainDimension(world_size, world_size, world_size); + DEMSim.InstructBoxDomainBoundingBC("all", mat_type_granular); + + // Now add a cylindrical boundary + auto walls = DEMSim.AddExternalObject(); + walls->AddCylinder(make_float3(0), make_float3(0, 0, 1), world_size / 2., mat_type_mixer, 0); + + auto mixer = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/internal_mixer.obj").string(), mat_type_mixer); + std::cout << "Total num of mixer triangles: " << mixer->GetNumTriangles() << std::endl; + mixer->Scale(make_float3(world_size / 2, world_size / 2, chamber_height)); + mixer->SetFamily(10); + // Define the prescribed motion of mixer + const float mixer_ang_vel = 3.14159f; + DEMSim.SetFamilyPrescribedAngVel(10, "0", "0", to_string_with_precision(mixer_ang_vel)); + + const float granular_rad = 0.005f; + const float cube_size = 2.f * granular_rad; + const float cube_density = 2.6e3f; + const float cube_mass = cube_density * cube_size * cube_size * cube_size; + const float cube_moi = cube_mass * cube_size * cube_size / 6.f; + const float cube_half_diag = 0.5f * cube_size * std::sqrt(3.f); + const float fill_spacing = 3.f * granular_rad; + + // Load a 12-triangle unit cube once, then instantiate it many times. Marking it convex and never-winner follows the + // mesh-particle demos and reduces unnecessary contact-island work for these tiny rigid particles. + auto cube_template = + DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type_granular, true, false); + cube_template->Scale(cube_size); + cube_template->SetConvex(true); + cube_template->SetNeverWinner(true); + + // Track the mixer + auto mixer_tracker = DEMSim.Track(mixer); + + // Sampler to use. The original mixer uses this same center, height, and nominal spacing; the tighter radial + // clearance accounts for the cube's half diagonal so cubes do not start outside the cylindrical chamber. + HCPSampler sampler(fill_spacing); + float3 fill_center = make_float3(0, 0, fill_bottom + fill_height / 2); + const float fill_radius = world_size / 2. - 2.f * cube_half_diag; + auto input_xyz = sampler.SampleCylinderZ(fill_center, fill_radius, fill_height / 2); + + // A full one-for-one mesh replacement of the original fill would instantiate more than a million moving triangles. + // Keep the same filled volume but cap the count so this remains a practical universal-mesh-contact demo. + const unsigned int target_cubes = 45000; + std::mt19937 rng(42); + std::shuffle(input_xyz.begin(), input_xyz.end(), rng); + if (input_xyz.size() > target_cubes) { + input_xyz.resize(target_cubes); + } + + for (const auto& pos : input_xyz) { + auto cube = DEMSim.AddMeshFromTemplate(cube_template, pos); + cube->SetFamily(1); + cube->SetMass(cube_mass); + cube->SetMOI(make_float3(cube_moi, cube_moi, cube_moi)); + cube->SetInitQuat(make_float4(0.f, 0.f, 0.f, 1.0f)); + } + std::cout << "Total num of cube particles: " << input_xyz.size() << std::endl; + std::cout << "Cube edge length: " << cube_size << " m, mass: " << cube_mass << " kg" << std::endl; + + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetErrorOutVelocity(40.); + // Mesh universal contact uses triangle primitives, so let the safety margin follow the measured mesh velocity and + // seed it with the rotating mixer tip speed. + DEMSim.SetExpandSafetyType("auto"); + // Force the solver to error out if something went crazy. A good practice to add them, but not necessary. + DEMSim.SetErrorOutAvgContacts(50); + + // The two following methods set how proactive the solver is in avoiding having its bins (for contact detection) too + // large or too small, and numbers close to 1 means more proactive. Usually, the user do not have to manually set it + // and the default values work fine. + DEMSim.SetAdaptiveBinSizeUpperProactivity(0.5); + DEMSim.SetAdaptiveBinSizeLowerProactivity(0.15); + + // Initialize the simulation system + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir /= "DemoOutput_MixerCubes"; + create_directory(out_dir); + + float sim_end = 10.0; + unsigned int fps = 20; + float frame_time = 1.0 / fps; + + // Keep tab of the max velocity in simulation. Mesh particles need the generic inspector, not the clump-only one. + auto max_v_finder = DEMSim.CreateInspector("max_absv"); + + std::cout << "Output at " << fps << " FPS" << std::endl; + unsigned int currframe = 0; + + mixer_tracker->SetPos(make_float3(0, 0, chamber_bottom + chamber_height / 2.0)); + std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + for (float t = 0; t < sim_end; t += frame_time) { + std::cout << "Frame: " << currframe << std::endl; + char analytical_filename[100], meshfilename[100], cnt_filename[100]; + sprintf(analytical_filename, "DEMdemo_analytical_%04d.vtk", currframe); + sprintf(meshfilename, "DEMdemo_mesh_%04d.vtk", currframe); + sprintf(cnt_filename, "Contact_pairs_%04d.csv", currframe++); + DEMSim.WriteAnalyticalFile(out_dir / analytical_filename); + DEMSim.WriteMeshFile(out_dir / meshfilename); + // DEMSim.WriteContactFile(out_dir / cnt_filename); + + float max_v = max_v_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + std::cout << "Solver's current update frequency (auto-adapted): " << DEMSim.GetUpdateFreq() << std::endl; + std::cout << "Average contacts each primitive has: " << DEMSim.GetAvgPrimitiveContacts() << std::endl; + + float3 mixer_moi = mixer_tracker->MOI(); + float3 mixer_acc = mixer_tracker->ContactAngAccLocal(); + float3 mixer_torque = mixer_acc * mixer_moi; + std::cout << "Contact torque on the mixer is " << mixer_torque.x << ", " << mixer_torque.y << ", " + << mixer_torque.z << std::endl; + + DEMSim.DoDynamics(frame_time); + DEMSim.ShowThreadCollaborationStats(); + } + std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; + + DEMSim.ShowTimingStats(); + + std::cout << "----------------------------------------" << std::endl; + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + + std::cout << "DEMdemo_MixerCubes exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_PlateSinkage.cpp b/src/demo/DEMdemo_PlateSinkage.cpp index 5b8ac15c..5ce15fad 100644 --- a/src/demo/DEMdemo_PlateSinkage.cpp +++ b/src/demo/DEMdemo_PlateSinkage.cpp @@ -29,11 +29,11 @@ // the pairwise material property "Cohesion". // ============================================================================= -#include -#include -#include -#include -#include +#include "core/ApiVersion.h" +#include "core/utils/ThreadManager.h" +#include "DEM/API.h" +#include "DEM/utils/HostSideHelpers.hpp" +#include "DEM/utils/Samplers.hpp" #include #include @@ -50,7 +50,7 @@ int main() { // --------------------------------------------------------------------- // Everything a user might want to change, in one place. // --------------------------------------------------------------------- - const float plate_diam = 0.35; // plate diameter, m + const float plate_diam = 0.35; // plate diameter, m const float plate_thickness = 0.02; const float plate_speed = 0.05; // press speed, m/s; slow it down for quasi-static work const float target_sinkage = 0.06; // stop once the plate is this deep, m @@ -80,13 +80,14 @@ int main() { // --------------------------------------------------------------------- { DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // QUAT matters: the saved file must carry orientations or the reload // in stage 2 cannot restore the packing's interlocking. DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ | OUTPUT_CONTENT::QUAT | OUTPUT_CONTENT::VEL); - auto mat_type_terrain = DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}, {"CoR", 0.4}, {"mu", 0.5}, {"Crr", 0.01}}); + auto mat_type_terrain = + DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}, {"CoR", 0.4}, {"mu", 0.5}, {"Crr", 0.01}}); const double world_size = 2; DEMSim.InstructBoxDomainDimension(world_size, world_size, world_size); @@ -118,8 +119,8 @@ int main() { auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); // Accumulate contact forces onto owners inside the force kernel rather - // than in the default separate pass over the contact array. Measured on - // this demo at 150k to 600k grains: +21% throughput on a Blackwell GPU, + // than in the default separate pass over the contact array. Upstream main + // measured this demo at 150k to 600k grains: +21% throughput on a Blackwell GPU, // +96% to +204% on an MI350X (see SetCollectAccRightAfterForceCalc). // This demo reads the plate load through ContactAcc, which is unaffected; // only tracker force-pair queries become unavailable. @@ -146,7 +147,7 @@ int main() { // --------------------------------------------------------------------- { DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::XYZ | OUTPUT_CONTENT::ABSV); @@ -207,9 +208,10 @@ int main() { plate->Scale(make_float3(plate_diam / 2., plate_diam / 2., plate_thickness / 2.)); float plate_mass = 7.8e3 * math_PI * (plate_diam / 2.) * (plate_diam / 2.) * plate_thickness; plate->SetMass(plate_mass); - plate->SetMOI(make_float3(plate_mass * (3 * plate_diam * plate_diam / 4. + plate_thickness * plate_thickness) / 12., - plate_mass * (3 * plate_diam * plate_diam / 4. + plate_thickness * plate_thickness) / 12., - plate_mass * plate_diam * plate_diam / 8.)); + plate->SetMOI( + make_float3(plate_mass * (3 * plate_diam * plate_diam / 4. + plate_thickness * plate_thickness) / 12., + plate_mass * (3 * plate_diam * plate_diam / 4. + plate_thickness * plate_thickness) / 12., + plate_mass * plate_diam * plate_diam / 8.)); float park_z = bed_surface + plate_thickness / 2. + 0.01; plate->SetInitPos(make_float3(0, 0, park_z)); plate->SetFamily(2); @@ -244,6 +246,10 @@ int main() { const float contact_threshold = 1e3; // Pa const float sample_every = 2e-3; // s std::ofstream curve(curve_file); + if (!curve) { + std::cerr << "Cannot open " << curve_file << std::endl; + return 1; + } curve << "sinkage_m,pressure_Pa" << std::endl; double contact_z = 0.; bool in_contact = false; @@ -259,6 +265,15 @@ int main() { float3 plate_force = plate_tracker->ContactAcc() * plate_mass; float pressure = plate_force.z / plate_area; + // Stop before the plate reaches the floor, even if the bed never develops the threshold pressure. + // This bounds the experiment when users change the fill or material parameters. + if (!std::isfinite(pressure) || !std::isfinite(plate_pos.z) || + plate_pos.z - plate_thickness / 2. <= bottom) { + std::cerr << "Plate test stopped: non-finite state or floor reached before target sinkage." + << std::endl; + DEMSim.DoDynamicsThenSync(0.); + return 1; + } if (!in_contact) { if (pressure >= contact_threshold) { in_contact = true; diff --git a/src/demo/DEMdemo_Plow.cpp b/src/demo/DEMdemo_Plow.cpp index 6c08c3d7..903d4ef9 100644 --- a/src/demo/DEMdemo_Plow.cpp +++ b/src/demo/DEMdemo_Plow.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -46,9 +45,11 @@ void AdvanceSimulation(DEMSolver& DEMSim, } int main() { + std::cout << "==== DEME demo/test: DEMdemo_Plow ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.UseFrictionalHertzianModel(); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // Scale-defining numbers of this simulation. @@ -128,7 +129,7 @@ int main() { DEMSim.InstructBoxDomainDimension({-world_halfsize, world_halfsize}, {-world_halfsize, world_halfsize}, {-world_halfsize, world_halfsize}); DEMSim.InstructBoxDomainBoundingBC("top_open", mat_type_walls); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.Initialize(); diff --git a/src/demo/DEMdemo_PolydisperseGeneration.cpp b/src/demo/DEMdemo_PolydisperseGeneration.cpp new file mode 100644 index 00000000..7ee64530 --- /dev/null +++ b/src/demo/DEMdemo_PolydisperseGeneration.cpp @@ -0,0 +1,158 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_PolydisperseGeneration ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); + + // Shrink both mass and stiffness to make the settling not challenging + float shrink_factor = 3e3; + auto mat_type_ball = DEMSim.LoadMaterial({{"E", 1e9 / shrink_factor}, {"nu", 0.3}, {"CoR", 0.3}}); + auto mat_type_terrain = DEMSim.LoadMaterial({{"E", 1e9 / shrink_factor}, {"nu", 0.3}, {"CoR", 0.2}}); + + float ball_rad = 0.2; + auto template_ball = + DEMSim.LoadSphereType(ball_rad * ball_rad * ball_rad * 7e3 * 4 / 3 * 3.14, ball_rad, mat_type_ball); + + float terrain_rad = 0.02; + auto template_terrain = DEMSim.LoadSphereType(terrain_rad * terrain_rad * terrain_rad * 2.6e3 * 4 / 3 * 3.14, + terrain_rad, mat_type_terrain); + + float step_size = 5e-6; + double world_size = 1.5; + + float3 sample_center = make_float3(0, 0, -world_size / 4); + float sample_halfheight = world_size / 4 * 0.95; + float sample_halfwidth = world_size / 2 * 0.95; + auto input_xyz = DEMBoxHCPSampler(sample_center, make_float3(sample_halfwidth, sample_halfwidth, sample_halfheight), + 2.01 * terrain_rad); + auto terrain_particles = DEMSim.AddClumps(template_terrain, input_xyz); + auto terrain_tracker = DEMSim.Track(terrain_particles); + unsigned int nTerrainParticles = input_xyz.size(); + + // Create a inspector to find out the highest point of this granular pile + auto max_z_finder = DEMSim.CreateInspector("clump_max_z"); + + DEMSim.InstructBoxDomainDimension(world_size, world_size, world_size * 2); + DEMSim.InstructBoxDomainBoundingBC("all", mat_type_terrain); + DEMSim.AddBCPlane(make_float3(0, 0, -world_size / 2), make_float3(0, 0, 1), mat_type_terrain); + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8 / shrink_factor)); + // If you want to use a large UpdateFreq then you have to expand spheres to ensure safety + DEMSim.SetCDUpdateFreq(10); + DEMSim.SetMaxVelocity(2.); + DEMSim.SetExpandSafetyMultiplier(1.1); + DEMSim.SetInitBinSize(4 * terrain_rad); + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir += "/DemoOutput_PolydisperseGeneration"; + create_directory(out_dir); + + unsigned int currframe = 0; + + float high_z = max_z_finder->GetValue(); + std::cout << "Max Z is at " << high_z << std::endl; + + for (float t = 0; t < 0.1; t += 0.05) { + { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[200]; + sprintf(filename, "%s/DEMdemo_output_%04d.csv", out_dir.c_str(), currframe++); + DEMSim.WriteSphereFile(std::string(filename)); + } + DEMSim.DoDynamicsThenSync(0.05); + } + high_z = max_z_finder->GetValue(); + std::cout << "Max Z is at " << high_z << std::endl; + + /////////////////////////////////////////////////////////////// + // As a proof of concept, we enlarge some of the particles on + // the fly + /////////////////////////////////////////////////////////////// + std::random_device r_device; + std::default_random_engine r_engine(r_device()); + std::discrete_distribution discrete_dist({1, 2}); + std::vector enlarge_ids; + // Pick around 1/3 particles to enlarge + for (unsigned int i = 0; i < nTerrainParticles; i++) { + unsigned int num = std::round(discrete_dist(r_engine)); + if (num == 0) { + enlarge_ids.push_back(i); + } + } + + step_size = 1e-6; + DEMSim.SetTimeStepSize(step_size); + + float sim_end = 10.0; + unsigned int steps_togo = (unsigned int)(0.1 / step_size); + unsigned int out_steps = steps_togo * 5; + unsigned int curr_step = 0; + + float cur_terrain_rad = terrain_rad; + float totalKE; + // while (cur_terrain_rad < terrain_rad * 1.5) { + // double prev_rad = cur_terrain_rad; + // cur_terrain_rad += terrain_rad / 10; + // std::cout << "Current particle size: " << cur_terrain_rad << std::endl; + // std::vector enlarge_ratio(enlarge_ids.size(), (double)cur_terrain_rad / prev_rad); + + // do { + // // Must DoDynamicsThenSync (not DoDynamics), as adding entities to the simulation is only allowed at a + // // sync-ed point of time. + // DEMSim.DoDynamicsThenSync(step_size * steps_togo); + // if (curr_step % out_steps == 0) { + // std::cout << "Frame: " << currframe << std::endl; + // char filename[200]; + // sprintf(filename, "%s/DEMdemo_output_%04d.csv", out_dir.c_str(), currframe++); + // DEMSim.WriteSphereFile(std::string(filename)); + // } + // totalKE = DEMSim.GetTotalKineticEnergy(); + // std::cout << "Total kinematic energy now: " << totalKE << std::endl; + // curr_step += steps_togo; + // } while (totalKE > 0.5); + // } + // DEMSim.ShowThreadCollaborationStats(); + + // Finally, output a `checkpoint' + char cp_filename[200]; + sprintf(cp_filename, "%s/Final_xyz.csv", out_dir.c_str()); + DEMSim.WriteClumpFile(std::string(cp_filename)); + + // for (double t = 0; t < (double)sim_end; t += frame_time, currframe++) { + // std::cout << "Frame: " << currframe << std::endl; + // DEMSim.ShowThreadCollaborationStats(); + // char filename[200]; + // sprintf(filename, "%s/DEMdemo_output_%04d.csv", out_dir.c_str(), currframe); + // DEMSim.WriteSphereFile(std::string(filename)); + + // DEMSim.DoDynamics(frame_time); + + // // + // } + + std::cout << "DEMdemo_PolydisperseGeneration exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_Repose.cpp b/src/demo/DEMdemo_Repose.cpp index b4ccadca..8fcbede1 100644 --- a/src/demo/DEMdemo_Repose.cpp +++ b/src/demo/DEMdemo_Repose.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,9 +21,11 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Repose ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.UseFrictionalHertzianModel(); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // If you don't need individual force information, then this option makes the solver run a bit faster. @@ -135,7 +136,7 @@ int main() { DEMSim.InstructBoxDomainDimension({-10, 10}, {-10, 10}, {funnel_bottom - 10.f, funnel_bottom + 20.f}); DEMSim.InstructBoxDomainBoundingBC("top_open", mat_type_walls); - DEMSim.SetInitTimeStep(5e-6); + DEMSim.SetTimeStepSize(5e-6); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't diff --git a/src/demo/DEMdemo_Repose2D.cpp b/src/demo/DEMdemo_Repose2D.cpp index d179a805..a51b38f8 100644 --- a/src/demo/DEMdemo_Repose2D.cpp +++ b/src/demo/DEMdemo_Repose2D.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,9 +21,11 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Repose2D ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; DEMSim.UseFrictionalHertzianModel(); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); // If you don't need individual force information, then this option makes the solver run a bit faster. @@ -68,7 +69,7 @@ int main() { DEMSim.InstructBoxDomainDimension({-10, 10}, {-10, 10}, {funnel_bottom - 10.f, funnel_bottom + 20.f}); DEMSim.InstructBoxDomainBoundingBC("top_open", mat_type_walls); - DEMSim.SetInitTimeStep(5e-6); + DEMSim.SetTimeStepSize(5e-6); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't diff --git a/src/demo/DEMdemo_ResponseAngleMesh.cpp b/src/demo/DEMdemo_ResponseAngleMesh.cpp new file mode 100644 index 00000000..8a6cce8c --- /dev/null +++ b/src/demo/DEMdemo_ResponseAngleMesh.cpp @@ -0,0 +1,296 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Rotating angle of response drum demo with for mesh particles with STL inputs. +// Meshed drum put analytical planes on the side +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +namespace { + +std::string ToLower(std::string s) { + for (char& c : s) { + c = static_cast(std::tolower(static_cast(c))); + } + return s; +} + +/// Load a mesh (STL or OBJ), scale it, attach material and register it as a template. +std::shared_ptr LoadMeshTemplate(DEMSolver& sim, + const path& file, + const std::shared_ptr& mat, + float scale) { + DEMMesh mesh; + std::string ext = ToLower(file.extension().string()); + bool ok = false; + if (ext == ".stl") { + ok = mesh.LoadSTLMesh(file.string()); + } else if (ext == ".obj") { + ok = mesh.LoadWavefrontMesh(file.string()); + } else { + DEME_ERROR("Unsupported mesh format: %s (only .stl or .obj)", ext.c_str()); + } + if (!ok) { + DEME_ERROR("Failed to load mesh template %s", file.string().c_str()); + } + mesh.SetMaterial(mat); + mesh.Scale(scale); + return sim.LoadMeshType(mesh); +} + +std::pair ComputeBounds(const std::vector& vertices) { + float3 vmin = make_float3(std::numeric_limits::max()); + float3 vmax = make_float3(std::numeric_limits::lowest()); + for (const auto& v : vertices) { + vmin.x = std::min(vmin.x, v.x); + vmin.y = std::min(vmin.y, v.y); + vmin.z = std::min(vmin.z, v.z); + vmax.x = std::max(vmax.x, v.x); + vmax.y = std::max(vmax.y, v.y); + vmax.z = std::max(vmax.z, v.z); + } + return {vmin, vmax}; +} + +} // namespace + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_ResponseAngleMesh ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::FAMILY); + DEMSim.SetMeshOutputFormat("VTK"); + DEMSim.SetNoForceRecord(); + DEMSim.SetMeshUniversalContact(true); + const float mm_to_m = 0.001f; + const float drum_inner_radius = 0.1f; // 200 mm diameter + const float wall_clearance = 0.001f; // leave a small gap to the mantle + const float rpm = 40.0f; + const float drum_ang_vel = rpm * 2.0f * PI / 60.0f; + + auto mat_type_particle = DEMSim.LoadMaterial({{"E", 1e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.00}}); + auto mat_type_drum = DEMSim.LoadMaterial({{"E", 2e6}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.00}}); + DEMSim.SetMaterialPropertyPair("mu", mat_type_particle, mat_type_drum, 0.5); + + // --------------------- Particle settings block --------------------- + // Mesh file can be .stl or .obj (path is relative to data/mesh). + const path particle_mesh_file = GET_DATA_PATH() / "mesh" / "simpleTriangleShape4mm.stl"; + const float particle_mesh_scale = mm_to_m * 1.1f; // 1.0f for STLs in mm size + const unsigned int target_particles = 4000; + // ------------------------------------------------------------------- + + auto tri_template = LoadMeshTemplate(DEMSim, particle_mesh_file, mat_type_particle, particle_mesh_scale); + auto [tri_min, tri_max] = ComputeBounds(tri_template->GetCoordsVertices()); + const float3 tri_dims = tri_max - tri_min; + const float tri_diag = std::sqrt(tri_dims.x * tri_dims.x + tri_dims.y * tri_dims.y + tri_dims.z * tri_dims.z); + const float tri_radius = 0.5f * tri_diag; + const float particle_density = 2600.0f; + double tri_volume = 0.0; + float3 tri_center = make_float3(0, 0, 0); + float3 tri_inertia = make_float3(0, 0, 0); + tri_template->ComputeMassProperties(tri_volume, tri_center, tri_inertia); + // tri_template->SetConvex(true); // for convex particels only + // tri_template->SetNeverWinner(true); // if mesh is more coarse the other contacts + const float particle_mass = static_cast(tri_volume * particle_density); + const float3 particle_moi = tri_inertia * particle_density; + std::cout << "Particle volume (m^3): " << tri_volume << ", mass (kg): " << particle_mass << std::endl; + std::cout << "Particle MOI (unit density, CoM): " << tri_inertia.x << ", " << tri_inertia.y << ", " << tri_inertia.z + << std::endl; + const double cube_vol = std::pow(4.0e-3, 3); + + // Toggle drum mantle type: + // true -> STL drum mesh ("after") + // false -> analytical planar contact cylinder ("before") + const bool use_stl_drum = true; + + const float drum_mass = 1.0f; + const unsigned int drum_family = 100; + float drum_height = 0.1f; + float drum_bottom_z = 0.0f; + float drum_top_z = drum_bottom_z + drum_height; + float drum_center_z = 0.5f * (drum_bottom_z + drum_top_z); + std::shared_ptr drum_tracker; + + if (use_stl_drum) { + const path drum_mesh_file = GET_DATA_PATH() / "mesh" / "drum.stl"; + DEMMesh drum_mesh_data; + if (!drum_mesh_data.LoadSTLMesh(drum_mesh_file.string())) { + DEME_ERROR("Failed to load drum mesh %s", drum_mesh_file.string().c_str()); + } + drum_mesh_data.SetMaterial(mat_type_drum); + drum_mesh_data.Scale(mm_to_m); + auto drum = DEMSim.AddMesh(drum_mesh_data); + + auto [drum_min, drum_max] = ComputeBounds(drum->GetCoordsVertices()); + drum_bottom_z = drum_min.z; + drum_top_z = drum_max.z; + drum_height = drum_top_z - drum_bottom_z; + drum_center_z = 0.5f * (drum_bottom_z + drum_top_z); + + const float drum_outer_radius = std::max(std::max(std::abs(drum_min.x), std::abs(drum_max.x)), + std::max(std::abs(drum_min.y), std::abs(drum_max.y))); + const float IZZ = drum_mass * drum_outer_radius * drum_outer_radius / 2.0f; + const float IYY = + (drum_mass / 12.0f) * (3.0f * drum_outer_radius * drum_outer_radius + drum_height * drum_height); + + drum->SetFamily(drum_family); + drum->SetMass(drum_mass); + drum->SetMOI(make_float3(IYY, IYY, IZZ)); + drum_tracker = DEMSim.Track(drum); + } else { + auto drum = DEMSim.AddExternalObject(); + drum->AddCylinder(make_float3(0, 0, drum_center_z), make_float3(0, 0, 1), drum_inner_radius, mat_type_drum, + ENTITY_NORMAL_INWARD); + const float IZZ = drum_mass * drum_inner_radius * drum_inner_radius / 2.0f; + const float IYY = + (drum_mass / 12.0f) * (3.0f * drum_inner_radius * drum_inner_radius + drum_height * drum_height); + + drum->SetFamily(drum_family); + drum->SetMass(drum_mass); + drum->SetMOI(make_float3(IYY, IYY, IZZ)); + drum_tracker = DEMSim.Track(drum); + } + const std::string drum_ang_pre = "float3 omg_g = make_float3(0.f, 0.f, " + to_string_with_precision(drum_ang_vel) + + ");" + "applyOriQToVector3(omg_g, make_float4(-oriQx, -oriQy, -oriQz, oriQw));"; + DEMSim.SetFamilyPrescribedAngVel(drum_family, "omg_g.x", "omg_g.y", "omg_g.z", true, drum_ang_pre); + + // Add top and bottom planes. They rotate with the drum family. + auto end_caps = DEMSim.AddExternalObject(); + end_caps->AddPlane(make_float3(0, 0, drum_top_z), make_float3(0, 0, -1), mat_type_drum); + end_caps->AddPlane(make_float3(0, 0, drum_bottom_z), make_float3(0, 0, 1), mat_type_drum); + end_caps->SetFamily(drum_family); + + auto cap_tracker = DEMSim.Track(end_caps); + + // Sample particles inside the cylindrical volume with a small wall clearance. + const float r_sphere = tri_radius; // = 0.5 * tri_diag + // AABB clearance for a cylinder aligned with z: + // radial clearance uses the half-diagonal in XY; z-clearance uses half-height in Z. + const float r_xy_aabb = 0.5f * std::sqrt(tri_dims.x * tri_dims.x + tri_dims.y * tri_dims.y); + const float r_z_aabb = 0.5f * tri_dims.z; + // Spacing of the HCP lattice (center-to-center). Keep conservative spacing (uses tri_diag). + // Clearance model only changes usable container dimensions. + HCPSampler sampler(tri_diag * 1.01f); + auto sample_with_clearance = [&](float r_xy, float r_z) { + const float sample_radius = drum_inner_radius - wall_clearance - r_xy; + const float sample_halfheight = drum_height * 0.5f - wall_clearance - r_z; + // Guard against negative dimensions + if (sample_radius <= 0.f || sample_halfheight <= 0.f) { + return std::vector{}; + } + + return sampler.SampleCylinderZ(make_float3(0, 0, drum_center_z), sample_radius, sample_halfheight); + }; + // Generate both candidate sets + auto cand_sphere = sample_with_clearance(r_sphere, r_sphere); + auto cand_aabb = sample_with_clearance(r_xy_aabb, r_z_aabb); + // Pick denser (more points). If equal, prefer sphere for robustness. + bool use_aabb = cand_aabb.size() > cand_sphere.size(); + auto& candidate_pos = use_aabb ? cand_aabb : cand_sphere; + std::cout << "Sampling clearance mode: " << (use_aabb ? "AABB" : "Sphere") << " (AABB=" << cand_aabb.size() + << ", Sphere=" << cand_sphere.size() << ")\n"; + if (candidate_pos.size() < target_particles) { + DEME_WARNING("Sampler produced fewer points (%zu) than requested (%u). Using all generated points.", + candidate_pos.size(), target_particles); + } + std::mt19937 rng(42); + std::shuffle(candidate_pos.begin(), candidate_pos.end(), rng); + if (candidate_pos.size() > target_particles) { + candidate_pos.resize(target_particles); + } + + for (const auto& pos : candidate_pos) { + auto tri = DEMSim.AddMeshFromTemplate(tri_template, pos); + tri->SetFamily(1); + tri->SetMass(particle_mass); + tri->SetMOI(particle_moi); + tri->SetInitQuat(make_float4(0.f, 0.f, 0.f, 1.0f)); + } + const float total_particle_mass = particle_mass * candidate_pos.size(); + std::cout << "Placed " << candidate_pos.size() << " particles with a mass of " << total_particle_mass + << " kg inside the drum." << std::endl; + + auto max_v_finder = DEMSim.CreateInspector("max_absv"); + float max_v; + + DEMSim.InstructBoxDomainDimension(0.3, 0.3, 0.2); + float step_size = 5e-6f; + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGPUTimersEnabled(true); + DEMSim.SetGravitationalAcceleration(make_float3(0, -9.81, 0)); + DEMSim.SetExpandSafetyType("auto"); + const float vmax_grav = std::sqrt(2.0f * 9.81f * drum_inner_radius); + const float vmax_rot = drum_ang_vel * drum_inner_radius; + const float vmax = (vmax_grav > vmax_rot) ? vmax_grav : vmax_rot; + DEMSim.SetExpandSafetyAdder(vmax); + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir /= "DemoOutput_ResponseAngleMesh"; + create_directory(out_dir); + + float time_end = 3.0f; + unsigned int fps = 20; + float frame_time = 1.0f / fps; + + std::cout << "Output at " << fps << " FPS" << std::endl; + unsigned int currframe = 0; + unsigned int curr_step = 0; + std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + for (double t = 0; t < (double)time_end; t += frame_time, curr_step++) { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[100]; + sprintf(filename, "DEMdemo_output_%04d.vtk", currframe); + DEMSim.WriteMeshFile(out_dir / filename); + currframe++; + max_v = max_v_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + + float3 drum_moi = drum_tracker->MOI(); + float3 drum_acc = drum_tracker->ContactAngAccLocal(); + float3 drum_torque = drum_acc * drum_moi; + std::cout << "Contact torque on the side walls is " << drum_torque.x << ", " << drum_torque.y << ", " + << drum_torque.z << std::endl; + + float3 force_on_BC = cap_tracker->ContactAcc() * cap_tracker->Mass(); + std::cout << "Contact force on bottom plane is " << std::abs(force_on_BC.z) << std::endl; + + DEMSim.DoDynamics(frame_time); + } + std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ClearThreadCollaborationStats(); + + DEMSim.ShowTimingStats(); + std::cout << "----------------------------------------" << std::endl; + DEMSim.ShowMemStats(); + std::cout << "----------------------------------------" << std::endl; + + std::cout << "DEMdemo_ResponseAngleMesh exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_RotatingDrum.cpp b/src/demo/DEMdemo_RotatingDrum.cpp index 151db23e..0b3fe4ee 100644 --- a/src/demo/DEMdemo_RotatingDrum.cpp +++ b/src/demo/DEMdemo_RotatingDrum.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include @@ -23,8 +22,10 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_RotatingDrum ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); // You can avoid one warning message by adding this.. No big deal, it just makes the solver not jitify big clumps @@ -128,15 +129,13 @@ int main() { float step_size = 5e-6; auto max_v_finder = DEMSim.CreateInspector("clump_max_absv"); DEMSim.InstructBoxDomainDimension(5, 5, 5); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8)); // For SetCDNumStepsMaxDrift methods, you can just use defaults, and they mean this: the solver will record the // average num of steps that dT is ahead of kT, say n steps, then the solver sets the max number of steps allowed // for dT to be aahead of kT to be a * n + b, based on the inputs you give using the 2 methods. DEMSim.SetCDNumStepsMaxDriftMultipleOfAvg(1.1); DEMSim.SetCDNumStepsMaxDriftAheadOfAvg(3); - // User-given max vel with the current version of solver, are only for its reference, since they auto-adapt. - DEMSim.SetMaxVelocity(3.); DEMSim.SetInitBinNumTarget(5e5); // DEMSim.SetInitBinSizeAsMultipleOfSmallestSphere(15); DEMSim.Initialize(); @@ -178,8 +177,7 @@ int main() { } std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); - std::cout << (time_sec.count()) / time_end * 10.0 << " seconds (wall time) to finish 10 seconds' simulation" - << std::endl; + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; DEMSim.ShowThreadCollaborationStats(); DEMSim.ClearThreadCollaborationStats(); diff --git a/src/demo/DEMdemo_RoverWheel.cpp b/src/demo/DEMdemo_RoverWheel.cpp new file mode 100644 index 00000000..af6e4154 --- /dev/null +++ b/src/demo/DEMdemo_RoverWheel.cpp @@ -0,0 +1,145 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_RoverWheel ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); + std::cout << "Note: This is a relatively large demo and should take hours/days to run!!" << std::endl; + + // Define materials + auto mat_type_terrain = DEMSim.LoadMaterial({{"E", 2e9}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.01}}); + auto mat_type_wheel = DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.5}, {"Crr", 0.01}}); + + // Define the simulation world + double world_size = 1.5; + DEMSim.InstructBoxDomainDimension(2 * world_size, world_size, world_size); + // Add 5 bounding planes around the simulation world, and leave the top open + DEMSim.InstructBoxDomainBoundingBC("top_open", mat_type_terrain); + + // Define the wheel geometry + float wheel_rad = 0.25; + float wheel_width = 0.25; + float wheel_mass = 10.0; + // Our shelf wheel geometry is lying flat on ground with z being the axial direction + float wheel_IYY = wheel_mass * wheel_rad * wheel_rad / 2; + float wheel_IXX = (wheel_mass / 12) * (3 * wheel_rad * wheel_rad + wheel_width * wheel_width); + auto wheel_template = + DEMSim.LoadClumpType(wheel_mass, make_float3(wheel_IXX, wheel_IYY, wheel_IXX), + (GET_DATA_PATH() / "clumps/ViperWheelSimple.csv").string(), mat_type_wheel); + // The file contains no wheel particles size info, so let's manually set them + wheel_template->radii = std::vector(wheel_template->nComp, 0.01); + // This wheel template is `lying down', but our reported MOI info is assuming it's in a position to roll along X + // direction. Let's make it clear its principal axes is not what we used to report its component sphere relative + // positions. + wheel_template->InformCentroidPrincipal(make_float3(0), make_float4(0.7071, 0, 0, 0.7071)); + + // Then the ground particle template + DEMClumpTemplate ellipsoid_template; + ellipsoid_template.ReadComponentFromFile((GET_DATA_PATH() / "clumps/ellipsoid_2_1_1.csv").string()); + // Calculate its mass and MOI + float mass = 2.6e3 * 4. / 3. * PI * 2 * 1 * 1; + float3 MOI = make_float3(1. / 5. * mass * (1 * 1 + 2 * 2), 1. / 5. * mass * (1 * 1 + 2 * 2), + 1. / 5. * mass * (1 * 1 + 1 * 1)); + // Scale the template we just created + double scaling = 0.005; + ellipsoid_template.mass = mass * scaling * scaling * scaling; + ellipsoid_template.MOI = MOI * scaling * scaling * scaling * scaling * scaling; + std::for_each(ellipsoid_template.radii.begin(), ellipsoid_template.radii.end(), + [scaling](float& r) { r *= scaling; }); + std::for_each(ellipsoid_template.relPos.begin(), ellipsoid_template.relPos.end(), + [scaling](float3& r) { r *= scaling; }); + ellipsoid_template.materials = + std::vector>(ellipsoid_template.nComp, mat_type_terrain); + auto ground_particle_template = DEMSim.LoadClumpType(ellipsoid_template); + + // Instantiate this wheel + auto wheel = DEMSim.AddClumps(wheel_template, make_float3(-1.2, 0, 0.4)); + // Give the wheel a family number so we can potentially add prescription + wheel->SetFamily(10); + // Note that the added constant ang vel is wrt the wheel's own principal coord system + DEMSim.SetFamilyPrescribedAngVel(10, "0", "2.0", "0", false); + + // Sample and add ground particles + float3 sample_center = make_float3(0, 0, -0.3); + float sample_halfheight = 0.4; + float sample_halfwidth_x = 1.45; + float sample_halfwidth_y = 0.725; + auto ground_particles_xyz = + DEMBoxGridSampler(sample_center, make_float3(sample_halfwidth_x, sample_halfwidth_y, sample_halfheight), + scaling * std::cbrt(2.0) * 2.1, scaling * std::cbrt(2.0) * 2.1, scaling * 2 * 2.1); + auto ground_particles = DEMSim.AddClumps(ground_particle_template, ground_particles_xyz); + // Give ground particles a small initial velocity so they `collapse' at the start of the simulation + ground_particles->SetVel(make_float3(0.002, 0, 0)); + + // Create an absv inspector + auto max_v_finder = DEMSim.CreateInspector("clump_max_absv"); + + // Make ready for simulation + float step_size = 5e-6; + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8)); + // If you want to use a large UpdateFreq then you have to expand spheres to ensure safety + DEMSim.SetCDUpdateFreq(30); + DEMSim.SetMaxVelocity(5.); + DEMSim.SetExpandSafetyMultiplier(1.2); + DEMSim.SetInitBinSize(scaling * 2); + DEMSim.Initialize(); + + float time_end = 10.0; + unsigned int fps = 20; + unsigned int out_steps = (unsigned int)(1.0 / (fps * step_size)); + + path out_dir = current_path(); + out_dir += "/DemoOutput_RoverWheel"; + create_directory(out_dir); + unsigned int currframe = 0; + unsigned int curr_step = 0; + std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + for (double t = 0; t < (double)time_end; t += step_size, curr_step++) { + if (curr_step % out_steps == 0) { + std::cout << "Frame: " << currframe << std::endl; + DEMSim.ShowThreadCollaborationStats(); + char filename[100]; + sprintf(filename, "%s/DEMdemo_output_%04d.csv", out_dir.c_str(), currframe++); + DEMSim.WriteSphereFile(std::string(filename)); + float max_v = max_v_finder->GetValue(); + std::cout << "Max velocity of any point in simulation is " << max_v << std::endl; + } + + DEMSim.DoDynamics(step_size); + // We can query info out of this drum, since it is tracked + // float3 drum_pos = Drum_tracker->Pos(); + // float3 drum_angVel = Drum_tracker->AngVelLocal(); + // std::cout << "Position of the drum: " << drum_pos.x << ", " << drum_pos.y << ", " << drum_pos.z + // << std::endl; + // std::cout << "Angular velocity of the drum: " << drum_angVel.x << ", " << drum_angVel.y << ", " + // << drum_angVel.z << std::endl; + } + std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + std::chrono::duration time_sec = std::chrono::duration_cast>(end - start); + std::cout << time_sec.count() << " seconds (wall time) to finish the simulation" << std::endl; + DEMSim.ShowThreadCollaborationStats(); + DEMSim.ClearThreadCollaborationStats(); + + std::cout << "DEMdemo_RoverWheel exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_Shake.cpp b/src/demo/DEMdemo_Shake.cpp index 86bfde9e..cf1b7991 100644 --- a/src/demo/DEMdemo_Shake.cpp +++ b/src/demo/DEMdemo_Shake.cpp @@ -8,7 +8,6 @@ // ============================================================================= #include -#include #include #include @@ -23,8 +22,10 @@ using namespace deme; const double math_PI = 3.14159; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Shake ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); @@ -106,7 +107,7 @@ int main() { auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); auto max_v_finder = DEMSim.CreateInspector("clump_max_absv"); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); DEMSim.Initialize(); diff --git a/src/demo/DEMdemo_Sieve.cpp b/src/demo/DEMdemo_Sieve.cpp index e94b15a2..09f057b0 100644 --- a/src/demo/DEMdemo_Sieve.cpp +++ b/src/demo/DEMdemo_Sieve.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -22,9 +21,11 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_Sieve ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - // I generally use this demo to inspect if I have "lost contact pairs", so the verbosity is set to STEP_METRIC... - DEMSim.SetVerbosity(STEP_METRIC); + // I generally use this demo to inspect if I have "lost contact pairs", so the verbosity is set to METRIC... + DEMSim.SetVerbosity("METRIC"); // If you don't need individual force information, then this option makes the solver run a bit faster. DEMSim.SetNoForceRecord(); @@ -141,14 +142,13 @@ int main() { float step_size = 1e-5; DEMSim.InstructBoxDomainDimension(12, 12, 25); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8)); DEMSim.SetCDUpdateFreq(30); + // Usually, the end user do not have to manually set expand safety parameters. DEMSim.SetExpandSafetyMultiplier(1.0); DEMSim.SetExpandSafetyAdder(1.0); - // You usually don't have to worry about initial bin size. In very rare cases, init bin size is so bad that auto bin - // size adaption is effectless, and you should notice in that case kT runs extremely slow. Then in that case setting - // init bin size may save the simulation. + // You usually don't have to worry about initial bin size. Using the automatic adaptive size by default is fine. // DEMSim.SetInitBinSize(0.1); // DEMSim.DisableAdaptiveBinSize(); diff --git a/src/demo/DEMdemo_SingleSphereCollide.cpp b/src/demo/DEMdemo_SingleSphereCollide.cpp index 1fc8055a..81e798dd 100644 --- a/src/demo/DEMdemo_SingleSphereCollide.cpp +++ b/src/demo/DEMdemo_SingleSphereCollide.cpp @@ -10,10 +10,13 @@ #include #include #include -#include #include +#ifdef DEME_HAS_VISUALIZER + #include "VisualizerDemoLoop.h" +#endif #include +#include #include #include #include @@ -22,12 +25,19 @@ using namespace deme; using namespace std::filesystem; int main() { + std::cout << "==== DEME demo/test: DEMdemo_SingleSphereCollide ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity("STEP_METRIC"); - DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetVerbosity("METRIC"); + // Write component spheres as compact VTK point data for ParaView's Glyph filter. + DEMSim.SetOutputFormat(OUTPUT_FORMAT::VTK); DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT", "NORMAL", "TORQUE", "CNT_WILDCARD"}); DEMSim.EnsureKernelErrMsgLineNum(); - // DEMSim.SetNoForceRecord(); + DEMSim.SetPersistentContact(true); + // Note!! If you want meshes to have contacts, set this to true!! + // If not, meshes will not have contacts with each other or analytical boundaries (but still have contacts with + // clumps). + DEMSim.SetMeshUniversalContact(true); // srand(time(NULL)); srand(4150); @@ -40,6 +50,8 @@ int main() { std::shared_ptr mat_type_3 = DEMSim.Duplicate(mat_type_2); // If you don't have this line, then CoR between thw 2 materials will take average when they are in contact DEMSim.SetMaterialPropertyPair("CoR", mat_type_1, mat_type_2, 0.6); + DEMSim.SetMaterialPropertyPair("CoR", mat_type_2, mat_type_3, 0.6); + DEMSim.SetMaterialPropertyPair("CoR", mat_type_1, mat_type_3, 0.6); // Even though set elsewhere to be 50, this pairwise value takes precedence when two different materials are in // contact. DEMSim.SetMaterialPropertyPair("Cohesion", mat_type_1, mat_type_2, 100.); @@ -101,7 +113,7 @@ int main() { my_force_model->SetPerContactWildcards({"delta_time", "delta_tan_x", "delta_tan_y", "delta_tan_z"}); my_force_model->SetMustPairwiseMatProp({"CoR", "mu", "Crr", "Cohesion"}); - DEMSim.SetInitTimeStep(2e-5); + DEMSim.SetTimeStepSize(2e-5); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8)); DEMSim.SetCDUpdateFreq(10); DEMSim.SetMaxVelocity(6.); @@ -111,8 +123,9 @@ int main() { // Testing modifying jitify options and force model prerequisites auto jitify_options = DEMSim.GetJitifyOptions(); - jitify_options.pop_back(); // Remove a warning suppression option - // DEMSim.SetJitifyOptions(jitify_options); + jitify_options.pop_back(); // Remove C++ std17 option + jitify_options.push_back("-std=c++17"); // Then add it back + DEMSim.SetJitifyOptions(jitify_options); // Then set it my_force_model->DefineCustomModelPrerequisites( "float3 __device__ GetContactForce(float3 AOwner, float3 BOwner, float3 ALinVel, float3 BLinVel, " "float3 ARotVel, float3 BRotVel, float delta_time, float delta_tan_x, float delta_tan_y, " @@ -128,7 +141,16 @@ int main() { particles2->SetVel(input_vel2); particles2->SetFamily(1); auto tracker2 = DEMSim.Track(particles2); - DEMSim.UpdateClumps(); + DEMSim.Update(); + +#ifdef DEME_HAS_VISUALIZER + // Display cadence follows wall time so camera input stays responsive between simulation output frames. + DEMVisualizer visualizer(DEMSim); + visualizer.SetCameraPosition(make_float3(5.f, 5.f, 3.f)); + visualizer.SetCameraTarget(make_float3(0.f, 0.f, -0.5f)); + visualizer.Initialize(); + VisualizerDemoLoop viewer_loop; +#endif // Ready simulation path out_dir = current_path(); @@ -150,13 +172,17 @@ int main() { // } char filename[100]; - sprintf(filename, "DEMdemo_output_%04d.csv", i); + sprintf(filename, "DEMdemo_output_%04d.vtk", i); DEMSim.WriteSphereFile(out_dir / filename); char cnt_filename[100]; sprintf(cnt_filename, "Contact_pairs_%04d.csv", i); DEMSim.WriteContactFile(out_dir / cnt_filename); + char analytical_filename[100]; + sprintf(analytical_filename, "DEMdemo_analytical_%04d.vtk", i); + DEMSim.WriteAnalyticalFile(out_dir / analytical_filename); + char meshfilename[100]; sprintf(meshfilename, "DEMdemo_mesh_%04d.vtk", i); DEMSim.WriteMeshFile(out_dir / meshfilename); @@ -164,7 +190,19 @@ int main() { // Testing persistent contact functionality... DEMSim.MarkPersistentContact(); +#ifdef DEME_HAS_VISUALIZER + // Keep each solver call short enough to service mouse input; output remains at frame_time intervals. + // Match the float timestep used internally so a rounded-up double does not request an extra step. + const float dynamics_step = static_cast(DEMSim.GetTimeStepSize()); + const int steps_per_frame = static_cast(std::lround(frame_time / dynamics_step)); + for (int step = 0; step < steps_per_frame; ++step) { + viewer_loop.Update(visualizer); + DEMSim.DoDynamics(dynamics_step); + } + DEMSim.DoDynamicsThenSync(0.0); +#else DEMSim.DoDynamicsThenSync(frame_time); +#endif max_z = max_z_finder->GetValue(); max_v = max_v_finder->GetValue(); KE = KE_finder->GetValue(); @@ -196,7 +234,7 @@ int main() { std::cout << "Particle 2 X coord is " << pos2.x << std::endl; std::cout << "Particle 1 family is " << fam1 << std::endl; std::cout << "Particle 2 family is " << fam2 << std::endl; - std::cout << "Average contacts each sphere has: " << DEMSim.GetAvgSphContacts() << std::endl; + std::cout << "Average contacts each sphere has: " << DEMSim.GetAvgPrimitiveContacts() << std::endl; if (points_spheres.size() == 1) { std::cout << "Two spheres collide, the contact is at (" << points_spheres[0].x << ", " << points_spheres[0].y << ", " << points_spheres[0].z << ")." << std::endl; diff --git a/src/demo/DEMdemo_SolarSystem.cpp b/src/demo/DEMdemo_SolarSystem.cpp index 96b223f7..90b00b08 100644 --- a/src/demo/DEMdemo_SolarSystem.cpp +++ b/src/demo/DEMdemo_SolarSystem.cpp @@ -14,15 +14,12 @@ // force, you should use SetFamilyExtraMargin with a relatively small value, // since you do not want to keep element interaction still relatively local, // or you know, having n^2 contact pairs can be problematic for any simulations -// with serious sizes. Also in most cases, you just need to use geometry -// wildcards (AddGeometryWildcard) to associate extra properties to be used -// in the force model. This is because DEME resolves contact pairs based on -// geometry entities, such as spheres and triangles, so you want to attach force -// model-related quatities directly to them, rather than at clump level. +// with serious sizes. Also in most cases, you can use owner wildcards +// (AddOwnerWildcard) to associate extra properties to be used in the force +// model. // ============================================================================= #include -#include #include #include @@ -39,8 +36,10 @@ const double math_PI = 3.14159; const double G = 6.674e-11 * 86400 * 86400 / 1.496e+11 / 1.496e+11 / 1.496e+11; int main() { + std::cout << "==== DEME demo/test: DEMdemo_SolarSystem ====" << std::endl; + std::cout << "========================================" << std::endl; DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.EnsureKernelErrMsgLineNum(); @@ -53,101 +52,123 @@ int main() { // Those 2 following lines are needed. my_force_model->SetMustHaveMatProp({"E", "nu", "CoR"}); my_force_model->SetMustPairwiseMatProp({"CoR"}); - // Gravitational pull will be calcuated using this extra quatity that we associate with each sphere. - // Now, why don't we use the mass property in the clump templates? Since that is at clump level, and if - // what you have here are, say 3-sphere clumps, rather than single sphere clumps, then the gravitational - // pull may be double- or triple-counted. In the case of using 3-sphere clumps, maybe you should assign - // each sphere say 1/3 of the total clump mass, then use this geometry wildcard to calculate the gravity. + // Gravitational pull will be calculated using this extra quantity that we associate with each owner. // In the force model script ForceModelWithGravity.cu, you can now use my_mass_A and my_mass_B to refer // to this wildcard and use it in force calculations. - my_force_model->SetPerGeometryWildcards({"my_mass"}); + my_force_model->SetPerOwnerWildcards({"my_mass"}); // For all the celestial bodies, define as templates. Their radii is just for visialization. float vis_size_scaler = 3; - auto Sun_template = DEMSim.LoadSphereType(1.989e30, 0.1 * vis_size_scaler, mat_type); - auto Mercury_template = DEMSim.LoadSphereType(3.3011e23, 0.02 * vis_size_scaler, mat_type); - auto Venus_template = DEMSim.LoadSphereType(4.8675e24, 0.035 * vis_size_scaler, mat_type); + auto Sun_template = + DEMSim.LoadSphereType(1.989e30, 2.0f / 5.0f * 1.989e30f * (0.1f * vis_size_scaler) * (0.1f * vis_size_scaler), + 0.1 * vis_size_scaler, mat_type); + auto Mercury_template = DEMSim.LoadSphereType( + 3.3011e23, 2.0f / 5.0f * 3.3011e23f * (0.02f * vis_size_scaler) * (0.02f * vis_size_scaler), + 0.02 * vis_size_scaler, mat_type); + auto Venus_template = DEMSim.LoadSphereType( + 4.8675e24, 2.0f / 5.0f * 4.8675e24f * (0.035f * vis_size_scaler) * (0.035f * vis_size_scaler), + 0.035 * vis_size_scaler, mat_type); float Earth_radius = 0.04 * vis_size_scaler; float Earth_mass = 5.9724e24; - auto Earth_template = DEMSim.LoadSphereType(Earth_mass, Earth_radius, mat_type); - auto Moon_template = DEMSim.LoadSphereType(7.342e22, 0.01 * vis_size_scaler, mat_type); - auto Mars_template = DEMSim.LoadSphereType(6.4171e23, 0.03 * vis_size_scaler, mat_type); + auto Earth_template = DEMSim.LoadSphereType(Earth_mass, 2.0f / 5.0f * Earth_mass * Earth_radius * Earth_radius, + Earth_radius, mat_type); + auto Moon_template = + DEMSim.LoadSphereType(7.342e22, 2.0f / 5.0f * 7.342e22f * (0.01f * vis_size_scaler) * (0.01f * vis_size_scaler), + 0.01 * vis_size_scaler, mat_type); + auto Mars_template = DEMSim.LoadSphereType( + 6.4171e23, 2.0f / 5.0f * 6.4171e23f * (0.03f * vis_size_scaler) * (0.03f * vis_size_scaler), + 0.03 * vis_size_scaler, mat_type); float Jupiter_radius = 0.08 * vis_size_scaler; float Jupiter_mass = 1.8982e27; - auto Jupiter_template = DEMSim.LoadSphereType(Jupiter_mass, Jupiter_radius, mat_type); - auto Io_template = DEMSim.LoadSphereType(8.9319e22, 0.004 * vis_size_scaler, mat_type); - auto Europa_template = DEMSim.LoadSphereType(4.7998e22, 0.004 * vis_size_scaler, mat_type); - auto Ganymede_template = DEMSim.LoadSphereType(1.4819e23, 0.004 * vis_size_scaler, mat_type); - auto Callisto_template = DEMSim.LoadSphereType(1.0759e23, 0.004 * vis_size_scaler, mat_type); - auto Saturn_template = DEMSim.LoadSphereType(5.6834e26, 0.07 * vis_size_scaler, mat_type); - auto Uranus_template = DEMSim.LoadSphereType(8.6810e25, 0.055 * vis_size_scaler, mat_type); - auto Neptune_template = DEMSim.LoadSphereType(1.0241e26, 0.06 * vis_size_scaler, mat_type); + auto Jupiter_template = DEMSim.LoadSphereType( + Jupiter_mass, 2.0f / 5.0f * Jupiter_mass * Jupiter_radius * Jupiter_radius, Jupiter_radius, mat_type); + auto Io_template = DEMSim.LoadSphereType( + 8.9319e22, 2.0f / 5.0f * 8.9319e22f * (0.004f * vis_size_scaler) * (0.004f * vis_size_scaler), + 0.004 * vis_size_scaler, mat_type); + auto Europa_template = DEMSim.LoadSphereType( + 4.7998e22, 2.0f / 5.0f * 4.7998e22f * (0.004f * vis_size_scaler) * (0.004f * vis_size_scaler), + 0.004 * vis_size_scaler, mat_type); + auto Ganymede_template = DEMSim.LoadSphereType( + 1.4819e23, 2.0f / 5.0f * 1.4819e23f * (0.004f * vis_size_scaler) * (0.004f * vis_size_scaler), + 0.004 * vis_size_scaler, mat_type); + auto Callisto_template = DEMSim.LoadSphereType( + 1.0759e23, 2.0f / 5.0f * 1.0759e23f * (0.004f * vis_size_scaler) * (0.004f * vis_size_scaler), + 0.004 * vis_size_scaler, mat_type); + auto Saturn_template = DEMSim.LoadSphereType( + 5.6834e26, 2.0f / 5.0f * 5.6834e26f * (0.07f * vis_size_scaler) * (0.07f * vis_size_scaler), + 0.07 * vis_size_scaler, mat_type); + auto Uranus_template = DEMSim.LoadSphereType( + 8.6810e25, 2.0f / 5.0f * 8.6810e25f * (0.055f * vis_size_scaler) * (0.055f * vis_size_scaler), + 0.055 * vis_size_scaler, mat_type); + auto Neptune_template = DEMSim.LoadSphereType( + 1.0241e26, 2.0f / 5.0f * 1.0241e26f * (0.06f * vis_size_scaler) * (0.06f * vis_size_scaler), + 0.06 * vis_size_scaler, mat_type); // Now load the bodies auto Sun = DEMSim.AddClumps(Sun_template, make_float3(0, 0, 0)); // Sun is fixed Sun->SetFamily(1); DEMSim.SetFamilyFixed(1); - Sun->AddGeometryWildcard("my_mass", 1.989e30); + Sun->AddOwnerWildcard("my_mass", 1.989e30); // The rest of the bodies: Defaulted to family 0, and they are free to move auto Mercury = DEMSim.AddClumps(Mercury_template, make_float3(0, 0.3871, 0)); Mercury->SetVel(make_float3(47.87 / 1731, 0, 0)); - Mercury->AddGeometryWildcard("my_mass", 3.3011e23); + Mercury->AddOwnerWildcard("my_mass", 3.3011e23); auto Venus = DEMSim.AddClumps(Venus_template, make_float3(0, 0.7233, 0)); Venus->SetVel(make_float3(35.02 / 1731, 0, 0)); - Venus->AddGeometryWildcard("my_mass", 4.8675e24); + Venus->AddOwnerWildcard("my_mass", 4.8675e24); auto Earth = DEMSim.AddClumps(Earth_template, make_float3(0, 1, 0)); Earth->SetVel(make_float3(29.78 / 1731, 0, 0)); - Earth->AddGeometryWildcard("my_mass", Earth_mass); + Earth->AddOwnerWildcard("my_mass", Earth_mass); auto Mars = DEMSim.AddClumps(Mars_template, make_float3(0, 1.5237, 0)); Mars->SetVel(make_float3(24.08 / 1731, 0, 0)); - Mars->AddGeometryWildcard("my_mass", 6.4171e23); + Mars->AddOwnerWildcard("my_mass", 6.4171e23); float Jupiter_orbit_rad = 5.2028; auto Jupiter = DEMSim.AddClumps(Jupiter_template, make_float3(0, Jupiter_orbit_rad, 0)); Jupiter->SetVel(make_float3(13.07 / 1731, 0, 0)); - Jupiter->AddGeometryWildcard("my_mass", Jupiter_mass); + Jupiter->AddOwnerWildcard("my_mass", Jupiter_mass); auto Saturn = DEMSim.AddClumps(Saturn_template, make_float3(0, 9.5388, 0)); Saturn->SetVel(make_float3(9.69 / 1731, 0, 0)); - Saturn->AddGeometryWildcard("my_mass", 5.6834e26); + Saturn->AddOwnerWildcard("my_mass", 5.6834e26); // auto Uranus = DEMSim.AddClumps(Uranus_template, make_float3(0,19.1914,0)); // Uranus->SetVel(make_float3(6.81/1731,0,0)); - // Uranus->AddGeometryWildcard("my_mass", 8.6810e25); + // Uranus->AddOwnerWildcard("my_mass", 8.6810e25); // auto Neptune = DEMSim.AddClumps(Neptune_template, make_float3(0,30.0611,0)); // Neptune->SetVel(make_float3(5.43/1731,0,0)); - // Neptune->AddGeometryWildcard("my_mass", 1.0241e26); + // Neptune->AddOwnerWildcard("my_mass", 1.0241e26); /* // The moons of Jupiter and of Earth (too close to the main planet, cannot reasonably render) auto Io = DEMSim.AddClumps(Io_template, make_float3(0,Jupiter_orbit_rad+Jupiter_radius+0.08,0)); float Io_vel = 3.545*(Jupiter_radius+0.08); Io->SetVel(make_float3(Io_vel,0,0)); - Io->AddGeometryWildcard("my_mass", 8.9319e22); + Io->AddOwnerWildcard("my_mass", 8.9319e22); auto Europa = DEMSim.AddClumps(Europa_template, make_float3(0,Jupiter_orbit_rad+Jupiter_radius+0.16,0)); float Europa_vel = 1.766*(Jupiter_radius+0.16); Europa->SetVel(make_float3(Europa_vel,0,0)); - Europa->AddGeometryWildcard("my_mass", 4.7998e22); + Europa->AddOwnerWildcard("my_mass", 4.7998e22); auto Ganymede = DEMSim.AddClumps(Ganymede_template, make_float3(0,Jupiter_orbit_rad+Jupiter_radius+0.24,0)); float Ganymede_vel = 0.835*(Jupiter_radius+0.24); Ganymede->SetVel(make_float3(Ganymede_vel,0,0)); - Ganymede->AddGeometryWildcard("my_mass", 1.4819e23); + Ganymede->AddOwnerWildcard("my_mass", 1.4819e23); auto Callisto = DEMSim.AddClumps(Callisto_template, make_float3(0,Jupiter_orbit_rad+Jupiter_radius+0.3,0)); float Callisto_vel = 0.558*(Jupiter_radius+0.3); Callisto->SetVel(make_float3(Callisto_vel,0,0)); - Callisto->AddGeometryWildcard("my_mass", 1.0759e23); + Callisto->AddOwnerWildcard("my_mass", 1.0759e23); auto Moon = DEMSim.AddClumps(Moon_template, make_float3(0,1.2,0)); Moon->SetVel(make_float3(0.234*0.2,0,0)); // 0.234 rad/day * 0.2AU (0.2AU is large, but it's for visualization) - Moon->AddGeometryWildcard("my_mass", 7.342e22); + Moon->AddOwnerWildcard("my_mass", 7.342e22); */ - float step_size = 0.001; - DEMSim.SetInitTimeStep(step_size); + float step_size = 0.01; + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); DEMSim.InstructBoxDomainDimension({-50, 50}, {-50, 50}, {-50, 50}); DEMSim.SetInitBinNumTarget(10); @@ -160,10 +181,6 @@ int main() { DEMSim.SetFamilyExtraMargin(0, 100.); DEMSim.SetFamilyExtraMargin(1, 100.); - // If you know your extra margin policy gonna make the average number of contacts per sphere huge, then set this so - // that the solver does not error out when checking it. - DEMSim.SetErrorOutAvgContacts(200); - DEMSim.Initialize(); std::filesystem::path out_dir = std::filesystem::current_path(); @@ -192,4 +209,4 @@ int main() { DEMSim.ShowThreadCollaborationStats(); std::cout << "SolarSystem demo exiting..." << std::endl; return 0; -} \ No newline at end of file +} diff --git a/src/demo/DEMdemo_TestPack.cpp b/src/demo/DEMdemo_TestPack.cpp index 07bcbd9d..a8839fe9 100644 --- a/src/demo/DEMdemo_TestPack.cpp +++ b/src/demo/DEMdemo_TestPack.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -28,7 +27,7 @@ inline bool is_near(float a, float b, float t = 1e-6) { } void SetSolverProp(DEMSolver& DEMSim) { - DEMSim.SetVerbosity(DEBUG); + DEMSim.SetVerbosity("DEBUG"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.InstructBoxDomainDimension(25, 25, 10); @@ -41,7 +40,6 @@ void SetSolverProp(DEMSolver& DEMSim) { // Testing non-jitified clump templates here in this demo... DEMSim.DisableJitifyClumpTemplates(); DEMSim.DisableJitifyMassProperties(); - DEMSim.UseCubForceCollection(); } void EllpsiodFallingOver() { @@ -70,7 +68,7 @@ void EllpsiodFallingOver() { ellipsoid->SetVel(tang_dir * 0.3); auto ellipsoid_tracker = DEMSim.Track(ellipsoid); - DEMSim.SetInitTimeStep(1e-4); + DEMSim.SetTimeStepSize(1e-4); // DEMSim.SetIntegrator(TIME_INTEGRATOR::FORWARD_EULER); DEMSim.Initialize(); @@ -121,7 +119,7 @@ void SphereRollUpIncline() { auto sphere_tracker = DEMSim.Track(sphere); float step_time = 1e-4; - DEMSim.SetInitTimeStep(step_time); + DEMSim.SetTimeStepSize(step_time); DEMSim.Initialize(); path out_dir = current_path(); @@ -153,7 +151,7 @@ void SphereRollUpIncline() { for (float Crr = 0.0; Crr <= 0.3; Crr += 0.01) { DEMSolver DEMSim; SetSolverProp(DEMSim); - DEMSim.SetVerbosity(QUIET); + DEMSim.SetVerbosity("QUIET"); auto mat_type_1 = DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", mu}, {"Crr", Crr}}); // A ball @@ -171,7 +169,7 @@ void SphereRollUpIncline() { auto sphere_tracker = DEMSim.Track(sphere); float step_time = 1e-4; - DEMSim.SetInitTimeStep(step_time); + DEMSim.SetTimeStepSize(step_time); DEMSim.SetCDUpdateFreq(50); DEMSim.SetMaxVelocity(2.0); DEMSim.Initialize(); @@ -239,7 +237,7 @@ void SphereStack() { auto sphere_tracker = DEMSim.Track(sphere_top); float step_time = 1e-5; - DEMSim.SetInitTimeStep(step_time); + DEMSim.SetTimeStepSize(step_time); // Just do CD once and we are all good DEMSim.SetCDUpdateFreq(-1); DEMSim.SetMaxVelocity(1.0); @@ -288,6 +286,8 @@ void SphereStack() { } int main() { + std::cout << "==== DEME demo/test: DEMdemo_TestPack ====" << std::endl; + std::cout << "========================================" << std::endl; // Choose a validation test by uncommenting it SphereRollUpIncline(); // EllpsiodFallingOver(); diff --git a/src/demo/DEMdemo_TestRestart.cpp b/src/demo/DEMdemo_TestRestart.cpp new file mode 100644 index 00000000..be5ad251 --- /dev/null +++ b/src/demo/DEMdemo_TestRestart.cpp @@ -0,0 +1,98 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "==== DEME demo/test: DEMdemo_TestRestart ====" << std::endl; + std::cout << "========================================" << std::endl; + DEMSolver DEMSim; + DEMSim.SetVerbosity("DEBUG"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); + + // E, nu, CoR, mu, Crr... + auto mat_type = DEMSim.LoadMaterial({{"E", 1e10}, {"nu", 0.3}, {"CoR", 0.3}, {"mu", 0.3}, {"Crr", 0.01}}); + + // Bounding box... + DEMSim.InstructBoxDomainBoundingBC("all", mat_type); + + float step_size = 1e-4; + double x_size = 4; + double y_size = 4; + double z_size = 20; + + auto pairs = DEMSim.ReadContactPairsFromCsv((GET_DATA_PATH() / "sim_data/example_cnt_pairs.csv").string()); + auto wcs = DEMSim.ReadContactWildcardsFromCsv((GET_DATA_PATH() / "sim_data/example_cnt_pairs.csv").string()); + + float rad = 2; + auto template_sphere = DEMSim.LoadSphereType(rad * rad * rad * 2.6e3 * 4 / 3 * 3.14, rad, mat_type); + + std::vector input_xyz; + input_xyz.push_back(make_float3(0, 0, -8)); + input_xyz.push_back(make_float3(0, 0, -4)); + + // First 2 spheres... + DEMClumpBatch base_batch(2); + base_batch.SetTypes(template_sphere); + base_batch.SetPos(input_xyz); + base_batch.SetExistingContacts(pairs); + base_batch.SetExistingContactWildcards(wcs); + DEMSim.AddClumps(base_batch); + + // Add another 2 batches + DEMClumpBatch batch_2 = base_batch; + std::for_each(batch_2.xyz.begin(), batch_2.xyz.end(), [](float3& xyz) { xyz.z += 5; }); + DEMSim.AddClumps(batch_2); + + path out_dir = current_path(); + out_dir += "/DemoOutput_TestRestart"; + create_directory(out_dir); + + DEMSim.InstructBoxDomainDimension(x_size, y_size, z_size, "X"); + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.8)); + // If you want to use a large UpdateFreq then you have to expand spheres to ensure safety + DEMSim.SetCDUpdateFreq(10); + DEMSim.SetMaxVelocity(5.); + DEMSim.SetExpandSafetyMultiplier(1.1); + DEMSim.Initialize(); + + { + char filename[100]; + sprintf(filename, "%s/DEMdemo_output_0.csv", out_dir.c_str()); + DEMSim.WriteSphereFile(std::string(filename)); + + char cnt_filename[100]; + sprintf(cnt_filename, "%s/Contact_pairs_0.csv", out_dir.c_str()); + DEMSim.WriteContactFile(std::string(cnt_filename)); + } + + DEMSim.DoDynamicsThenSync(0.5); + + { + char filename[100]; + sprintf(filename, "%s/DEMdemo_output_1.csv", out_dir.c_str()); + DEMSim.WriteSphereFile(std::string(filename)); + + char cnt_filename[100]; + sprintf(cnt_filename, "%s/Contact_pairs_1.csv", out_dir.c_str()); + DEMSim.WriteContactFile(std::string(cnt_filename)); + } + + std::cout << "DEMdemo_TestRestart exiting..." << std::endl; + return 0; +} diff --git a/src/demo/DEMdemo_WheelDP.cpp b/src/demo/DEMdemo_WheelDP.cpp index 514be6de..2966c95c 100644 --- a/src/demo/DEMdemo_WheelDP.cpp +++ b/src/demo/DEMdemo_WheelDP.cpp @@ -11,7 +11,6 @@ // ============================================================================= #include -#include #include #include @@ -26,6 +25,8 @@ using namespace deme; const double math_PI = 3.1415927; int main() { + std::cout << "==== DEME demo/test: DEMdemo_WheelDP ====" << std::endl; + std::cout << "========================================" << std::endl; std::filesystem::path out_dir = std::filesystem::current_path(); out_dir /= "DemoOutput_WheelDP"; std::filesystem::create_directory(out_dir); @@ -36,7 +37,7 @@ int main() { for (float TR : TRs) { DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); @@ -74,6 +75,9 @@ int main() { wheel->SetMOI(make_float3(wheel_IXX, wheel_IYY, wheel_IXX)); // Give the wheel a family number so we can potentially add prescription wheel->SetFamily(1); + // Split it into patches: if angle more than 30 deg, split. This is not strictly needed; but more physically + // plausible. + wheel->SplitIntoConvexPatches(30.); // Track it auto wheel_tracker = DEMSim.Track(wheel); @@ -219,16 +223,16 @@ int main() { auto total_mass_finder = DEMSim.CreateInspector("clump_mass"); auto max_v_finder = DEMSim.CreateInspector("clump_max_absv"); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -G_mag)); - DEMSim.SetCDUpdateFreq(30); + // DEMSim.SetCDUpdateFreq(30); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't // happen anyway and if it does, something already went wrong. DEMSim.SetMaxVelocity(50.); // Error out vel is used to force the simulation to abort when something goes wrong and sim diverges. - DEMSim.SetErrorOutVelocity(60.); - DEMSim.SetExpandSafetyMultiplier(1.1); + DEMSim.SetErrorOutVelocity(200.); + // DEMSim.SetExpandSafetyMultiplier(1.1); // You usually don't have to worry about initial bin size. In very rare cases, init bin size is so bad that auto // bin size adaption is effectless, and you should notice in that case kT runs extremely slow. Then in that case // setting init bin size may save the simulation. @@ -265,7 +269,7 @@ int main() { // Switch wheel from free fall into DP test DEMSim.ChangeFamily(1, 2); step_size *= 2.; - DEMSim.UpdateStepSize(step_size); + DEMSim.SetTimeStepSize(step_size); for (double t = 0; t < sim_end; t += step_size, curr_step++) { if (curr_step % out_steps == 0) { diff --git a/src/demo/DEMdemo_WheelDPSimplified.cpp b/src/demo/DEMdemo_WheelDPSimplified.cpp index 714c7e6b..5b08009a 100644 --- a/src/demo/DEMdemo_WheelDPSimplified.cpp +++ b/src/demo/DEMdemo_WheelDPSimplified.cpp @@ -13,7 +13,6 @@ // ============================================================================= #include -#include #include #include @@ -28,12 +27,14 @@ using namespace deme; const double math_PI = 3.1415927; int main() { + std::cout << "==== DEME demo/test: DEMdemo_WheelDPSimplified ====" << std::endl; + std::cout << "========================================" << std::endl; std::filesystem::path out_dir = std::filesystem::current_path(); out_dir /= "DemoOutput_WheelDPSimplified"; std::filesystem::create_directory(out_dir); DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); @@ -144,7 +145,7 @@ int main() { auto max_v_finder = DEMSim.CreateInspector("clump_max_absv"); // Make ready for simulation - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -G_mag)); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't diff --git a/src/demo/DEMdemo_WheelSlopeSlip.cpp b/src/demo/DEMdemo_WheelSlopeSlip.cpp index dcf98947..638d8b4d 100644 --- a/src/demo/DEMdemo_WheelSlopeSlip.cpp +++ b/src/demo/DEMdemo_WheelSlopeSlip.cpp @@ -13,7 +13,6 @@ // ============================================================================= #include -#include #include #include @@ -28,6 +27,8 @@ using namespace deme; const double math_PI = 3.1415927; int main() { + std::cout << "==== DEME demo/test: DEMdemo_WheelSlopeSlip ====" << std::endl; + std::cout << "========================================" << std::endl; std::filesystem::path out_dir = std::filesystem::current_path(); out_dir /= "DemoOutput_WheelSlopeSlip"; std::filesystem::create_directory(out_dir); @@ -54,7 +55,7 @@ int main() { for (float Slope_deg : Slopes_deg) { DEMSolver DEMSim; - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetMeshOutputFormat(MESH_FORMAT::VTK); @@ -229,7 +230,7 @@ int main() { float3 this_G = make_float3(-G_mag * std::sin(G_ang), 0, -G_mag * std::cos(G_ang)); DEMSim.SetGravitationalAcceleration(this_G); - DEMSim.SetInitTimeStep(step_size); + DEMSim.SetTimeStepSize(step_size); DEMSim.SetCDUpdateFreq(15); // Max velocity info is generally just for the solver's reference and the user do not have to set it. The solver // wouldn't take into account a vel larger than this when doing async-ed contact detection: but this vel won't diff --git a/src/demo/ModularTests/CMakeLists.txt b/src/demo/ModularTests/CMakeLists.txt index 0bc4c92e..2ba17ae8 100644 --- a/src/demo/ModularTests/CMakeLists.txt +++ b/src/demo/ModularTests/CMakeLists.txt @@ -11,10 +11,36 @@ SET(LIBRARIES # ------------------------------------------------------------------------------ SET(MODULAR_TESTS + DEMTest_StateFinite + DEMTest_AdaptiveTimestep DEMTest_AnalyticalCone DEMTest_AnalyticalConePour + DEMTest_CombinedOwners + DEMTest_ContactIslandFlooding + DEMTest_ConeGrazingPlane + DEMTest_ConeGrazingSphere + DEMTest_CylinderCrossRotation + DEMTest_CudaHeaderDiscovery + DEMTest_DeviceDataRetrieval + DEMTest_DeviceSelection + DEMTest_MeshBox1Particle + DEMTest_MeshSlidingOnSlope + DEMTest_MeshTemplate + DEMTest_PatchLocations + DEMTest_RuntimeSimParams + DEMTest_MeshPatch + DEMTest_SimpleCollisions + DEMTest_SpherePlanePeneArea + DEMTest_SphereSphereArea + DEMTest_TrianglePVTracking + DEMTest_VisualizationSnapshot + DEMTest_VisualizationScene ) +if(DEME_BUILD_VISUALIZER) + list(APPEND MODULAR_TESTS DEMTest_Visualizer) +endif() + # ------------------------------------------------------------------------------ # Add all test executables # ------------------------------------------------------------------------------ @@ -46,12 +72,17 @@ FOREACH(PROGRAM ${MODULAR_TESTS}) PUBLIC ${LIBRARIES} PUBLIC ${EXTERNAL_LIBRARIES} ) + if(PROGRAM STREQUAL "DEMTest_Visualizer") + target_link_libraries(${PROGRAM} PRIVATE DEMEVisualizer) + endif() add_dependencies(${PROGRAM} ${LIBRARIES}) - set_target_properties( - ${PROGRAM} PROPERTIES - CXX_STANDARD ${CXXSTD_SUPPORTED} - ) + if (CXXSTD_SUPPORTED) + set_target_properties( + ${PROGRAM} PROPERTIES + CXX_STANDARD ${CXXSTD_SUPPORTED} + ) + endif() ENDFOREACH(PROGRAM) diff --git a/src/demo/ModularTests/DEMTest_AdaptiveTimestep.cpp b/src/demo/ModularTests/DEMTest_AdaptiveTimestep.cpp new file mode 100644 index 00000000..f689ab42 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_AdaptiveTimestep.cpp @@ -0,0 +1,92 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Setup-time adaptive timestep smoke test. +// +// The hertz_const mode computes one fixed timestep during Initialize() from +// material stiffness, minimum clump mass, and minimum radius. This test keeps the +// system minimal and checks that the initialized timestep matches the documented +// Hertz estimate rather than the manually supplied initial value. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace deme; + +namespace { + +int fail(const std::string& message) { + std::cerr << "DEMTest_AdaptiveTimestep failed: " << message << std::endl; + return 1; +} + +double expectedHertzConstTimestep(double mass, double radius, double youngs_modulus, double poisson_ratio) { + const double effective_E = 1.0 / (2.0 * (1.0 - poisson_ratio * poisson_ratio) / youngs_modulus); + const double effective_radius = 0.5 * radius; + const double effective_mass = 0.5 * mass; + const double hertz_stiffness = FOUR_OVER_THREE * std::sqrt(0.1) * effective_E * effective_radius; + return (PI / (2.0 * N_DT)) * std::sqrt(effective_mass / hertz_stiffness); +} + +bool nearlyEqual(double a, double b, double rel_tol = 1e-10) { + const double scale = std::max(1.0, std::max(std::abs(a), std::abs(b))); + return std::abs(a - b) <= rel_tol * scale; +} + +} // namespace + +int main() { + constexpr double manual_dt = 1.0e-5; + constexpr double mass = 0.2; + constexpr double radius = 0.05; + constexpr double youngs_modulus = 1.0e8; + constexpr double poisson_ratio = 0.25; + + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(1.0, 1.0, 1.0); + DEMSim.SetGravitationalAcceleration(make_float3(0.f, 0.f, 0.f)); + DEMSim.SetInitBinNumTarget(8); + DEMSim.SetTimeStepSize(manual_dt); + + auto mat = DEMSim.LoadMaterial({{"E", static_cast(youngs_modulus)}, + {"nu", static_cast(poisson_ratio)}, + {"CoR", 0.2f}, + {"mu", 0.4f}, + {"Crr", 0.0f}}); + auto sphere = DEMSim.LoadSphereType(static_cast(mass), static_cast(radius), mat); + DEMSim.AddClumps(sphere, make_float3(0.f, 0.f, 0.f)); + + DEMSim.UseHertzConstTimeStep(); + DEMSim.Initialize(); + + const double actual_dt = DEMSim.GetTimeStepSize(); + const double expected_dt = expectedHertzConstTimestep(mass, radius, youngs_modulus, poisson_ratio); + + if (!std::isfinite(actual_dt) || actual_dt <= 0.0) { + return fail("initialized timestep is not finite and positive"); + } + if (nearlyEqual(actual_dt, manual_dt)) { + return fail("hertz_const did not replace the manually supplied timestep"); + } + if (!nearlyEqual(actual_dt, expected_dt)) { + std::cerr << "Expected dt: " << expected_dt << ", actual dt: " << actual_dt << std::endl; + return fail("hertz_const timestep does not match the Hertz estimate"); + } + + std::cout << "DEMTest_AdaptiveTimestep passed. " + << "manual_dt=" << manual_dt << " hertz_dt=" << actual_dt << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_AnalyticalCone.cpp b/src/demo/ModularTests/DEMTest_AnalyticalCone.cpp index d00b2385..bdd90da8 100644 --- a/src/demo/ModularTests/DEMTest_AnalyticalCone.cpp +++ b/src/demo/ModularTests/DEMTest_AnalyticalCone.cpp @@ -118,7 +118,7 @@ int main(int argc, char** argv) { std::filesystem::create_directories(out_dir); DEMSolver DEMSim(1); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetContactOutputContent({"OWNER", "GEO_ID", "FORCE", "POINT", "NORMAL"}); @@ -168,7 +168,7 @@ int main(int argc, char** argv) { } DEMSim.InstructBoxDomainDimension({-1.5f, 14.0f}, {-1.5f, 1.5f}, {-1.0f, 2.5f}); DEMSim.InstructBoxDomainBoundingBC("none", mat_cone); - DEMSim.SetInitTimeStep(1.0e-6); + DEMSim.SetTimeStepSize(1.0e-6); DEMSim.SetCDUpdateFreq(1); DEMSim.SetGravitationalAcceleration(make_float3(0.0f)); DEMSim.SetMaxVelocity(1.0f); diff --git a/src/demo/ModularTests/DEMTest_AnalyticalConePour.cpp b/src/demo/ModularTests/DEMTest_AnalyticalConePour.cpp index 63dc80a5..84e2bd91 100644 --- a/src/demo/ModularTests/DEMTest_AnalyticalConePour.cpp +++ b/src/demo/ModularTests/DEMTest_AnalyticalConePour.cpp @@ -142,7 +142,7 @@ int main(int argc, char** argv) { std::filesystem::create_directories(dynamic_dir); DEMSolver DEMSim(1); - DEMSim.SetVerbosity(INFO); + DEMSim.SetVerbosity("INFO"); DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); DEMSim.SetOutputContent(OUTPUT_CONTENT::ABSV); DEMSim.SetContactOutputContent({"OWNER", "GEO_ID", "FORCE", "POINT", "NORMAL"}); @@ -216,7 +216,7 @@ int main(int argc, char** argv) { DEMSim.InstructBoxDomainDimension({-3.35f, 3.35f}, {-0.95f, 0.95f}, {-0.20f, 2.20f}); DEMSim.InstructBoxDomainBoundingBC("none", mat_wall); - DEMSim.SetInitTimeStep(2.0e-5); + DEMSim.SetTimeStepSize(2.0e-5); DEMSim.SetCDUpdateFreq(10); DEMSim.SetGravitationalAcceleration(make_float3(0.0f, 0.0f, -9.81f)); DEMSim.SetMaxVelocity(80.0f); diff --git a/src/demo/ModularTests/DEMTest_CombinedOwners.cpp b/src/demo/ModularTests/DEMTest_CombinedOwners.cpp new file mode 100644 index 00000000..d0c450b9 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_CombinedOwners.cpp @@ -0,0 +1,412 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Combined-owner modular test. +// +// This verifies combined clump instantiation, combined tracking, default +// intra-group contact suppression, explicit contact re-enable, and rigid +// member motion re-imposition after member-level acceleration inputs. It also +// checks combined mesh tracking, prescribed translation, and per-member charge. +// ============================================================================= + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace deme; + +namespace { + +constexpr float kRadius = 0.5f; +constexpr float kMass = 1.0f; +constexpr float kMemberSpacing = 0.75f; +constexpr float kStepSize = 1e-5f; +constexpr float kPosTol = 1e-5f; +constexpr float kVelTol = 1e-5f; +constexpr float kQuatTol = 1e-5f; +const float3 kInitPos = make_float3(1.0f, -0.5f, 0.25f); +const float3 kInitPos2 = make_float3(5.0f, 0.0f, 0.0f); +const float4 kIdentityQ = make_float4(0, 0, 0, 1); + +bool approxEqual(const float3& a, const float3& b, float tol = kPosTol) { + return std::abs(a.x - b.x) < tol && std::abs(a.y - b.y) < tol && std::abs(a.z - b.z) < tol; +} + +bool approxEqualQuat(const float4& a, const float4& b, float tol = kQuatTol) { + const float direct = std::abs(a.x - b.x) + std::abs(a.y - b.y) + std::abs(a.z - b.z) + std::abs(a.w - b.w); + const float negated = std::abs(a.x + b.x) + std::abs(a.y + b.y) + std::abs(a.z + b.z) + std::abs(a.w + b.w); + return direct < tol || negated < tol; +} + +std::pair normalizePair(bodyID_t a, bodyID_t b) { + if (b < a) { + std::swap(a, b); + } + return {a, b}; +} + +bool containsPair(const std::vector>& contacts, bodyID_t a, bodyID_t b) { + const auto target = normalizePair(a, b); + return std::any_of(contacts.begin(), contacts.end(), + [&target](const auto& pair) { return normalizePair(pair.first, pair.second) == target; }); +} + +struct ScenarioResult { + size_t combined_count_before_init = 0; + std::shared_ptr tracker; + size_t total_member_owners = 0; + std::vector tracker_positions; + std::vector tracker_owner_ids; + std::vector> contacts; +}; + +struct MotionResult { + std::vector initial_positions; + std::vector final_positions; + std::vector final_velocities; + std::vector final_ang_velocities; + std::vector final_orientations; +}; + +ScenarioResult runScenario(bool allow_intra_combined_contacts, bool use_batch = false) { + ScenarioResult result; + + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.InstructBoxDomainDimension(10, 10, 10); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + DEMSim.SetCDUpdateFreq(1); + + auto mat = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.5}, {"Crr", 0.0}}); + auto sphere = DEMSim.LoadSphereType(kMass, kRadius, mat); + + std::vector> component_templates = {sphere, sphere}; + std::vector component_rel_pos = {make_float3(0, 0, 0), make_float3(kMemberSpacing, 0, 0)}; + auto combined_type = DEMSim.LoadCombinedClumpType(component_templates, component_rel_pos, {}, 0); + + std::shared_ptr combined_inst; + if (use_batch) { + std::vector positions = {kInitPos, kInitPos2}; + combined_inst = DEMSim.AddCombinedFromTemplate(combined_type, positions); + } else { + combined_inst = DEMSim.AddCombinedFromTemplate(combined_type, kInitPos, kIdentityQ); + } + + result.combined_count_before_init = DEMSim.GetNumCombinedInstances(); + result.total_member_owners = combined_inst->member_objs.size(); + auto tracker = DEMSim.Track(combined_inst); + result.tracker = tracker; + + if (allow_intra_combined_contacts) { + DEMSim.SetAllowIntraCombinedOwnerContacts(true); + } + + DEMSim.SetTimeStepSize(kStepSize); + DEMSim.Initialize(true); + + for (size_t i = 0; i < result.total_member_owners; i++) { + result.tracker_owner_ids.push_back(tracker->GetOwnerID(i)); + result.tracker_positions.push_back(tracker->Pos(i)); + } + + result.contacts = DEMSim.GetClumpContacts(); + return result; +} + +MotionResult runMemberAccelerationScenario(bool angular) { + MotionResult result; + + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.InstructBoxDomainDimension(10, 10, 10); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + DEMSim.SetCDUpdateFreq(1); + + auto mat = DEMSim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.5}, {"Crr", 0.0}}); + auto sphere = DEMSim.LoadSphereType(kMass, kRadius, mat); + + std::vector> component_templates = {sphere, sphere}; + std::vector component_rel_pos = {make_float3(0, 0, 0), make_float3(kMemberSpacing, 0, 0)}; + auto combined_type = DEMSim.LoadCombinedClumpType(component_templates, component_rel_pos, {}, 0); + auto combined_inst = DEMSim.AddCombinedFromTemplate(combined_type, kInitPos, kIdentityQ); + auto tracker = DEMSim.Track(combined_inst); + + DEMSim.SetTimeStepSize(kStepSize); + DEMSim.Initialize(); + + result.initial_positions = tracker->Positions(); + if (angular) { + tracker->AddAngAcc(make_float3(0, 0, 2.0e6f), 1); + } else { + tracker->AddAcc(make_float3(1000.f, 0, 0), 1); + } + DEMSim.DoDynamicsThenSync(kStepSize); + + result.final_positions = tracker->Positions(); + result.final_velocities = tracker->Velocities(); + result.final_ang_velocities = tracker->AngularVelocitiesGlobal(); + result.final_orientations = tracker->OrientationQuaternions(); + return result; +} + +// Exercise the electrostatic rod's mesh-template and tracker path with ten adjacent sections. +// Distinct wildcard values check that runtime charge belongs to each mesh owner independently. +bool testCombinedMeshRod() { + DEMSolver sim; + sim.SetVerbosity("ERROR"); + sim.InstructBoxDomainDimension(10, 10, 10); + sim.SetGravitationalAcceleration(make_float3(0)); + sim.SetCDUpdateFreq(1); + sim.SetTimeStepSize(kStepSize); + auto model = sim.DefineContactForceModel("force = make_float3(0); Q[AOwner] += 0.f; Q[BOwner] += 0.f;"); + model->SetPerOwnerWildcards({"Q"}); + auto mat = sim.LoadMaterial({{"E", 1e8}, {"nu", 0.3}}); + // Keep a sphere away from the rod to exercise the normal mesh/clump initialization path without contacts. + auto sphere = sim.LoadSphereType(kMass, kRadius, mat); + sim.AddClumps(sphere, make_float3(-2, 0, 0)); + constexpr size_t count = 10; + constexpr float spacing = 0.05f; + constexpr float charge = -2e-7f; + auto section = sim.LoadMeshType(GetDEMEDataFile("mesh/cyl_r1_h2.obj"), mat); + section->Scale(make_float3(0.01f, 0.01f, spacing / 2)); + section->SetMass(0.1f); + section->SetMOI(make_float3(1e-4f)); + section->SetFamily(1); + sim.SetFamilyFixed(1); + std::vector offsets(count); + for (size_t i = 0; i < count; i++) { + offsets[i] = make_float3(0, 0, i * spacing); + } + auto type = sim.LoadCombinedMeshType(std::vector>(count, section), offsets); + auto rod = sim.AddCombinedFromTemplate(type, kInitPos); + auto tracker = sim.Track(rod); + sim.Initialize(); + + // Initialize clears the solver's setup cache; the retained instance handle holds resolved owner IDs. + const auto& members = rod->member_owner_ids; + if (!rod->owners_resolved || members.size() != count || rod->GetNumOwners() != count || + rod->master_owner_ids.size() != 1 || rod->master_owner_ids[0] != tracker->GetOwnerID()) { + std::cout << "Mesh combined metadata or tracker owner count mismatch" << std::endl; + return false; + } + tracker->SetOwnerWildcardValues("Q", std::vector(count, charge)); + float total_charge = 0; + for (size_t i = 0; i < count; i++) { + if (members[i] != tracker->GetOwnerID(i) || !approxEqual(tracker->Pos(i), kInitPos + offsets[i]) || + tracker->GetOwnerWildcardValue("Q", i) != charge) { + const auto pos = tracker->Pos(i); + std::cout << "Mesh member " << i << " initial state mismatch: position " << pos.x << ", " << pos.y << ", " + << pos.z << "; Q = " << tracker->GetOwnerWildcardValue("Q", i) << std::endl; + return false; + } + total_charge += tracker->GetOwnerWildcardValue("Q", i); + } + if (std::abs(total_charge - count * charge) > 1e-12f) { + std::cout << "Mesh rod total charge mismatch: " << total_charge << std::endl; + return false; + } + tracker->SetOwnerWildcardValue("Q", 2 * charge, count - 1); + // Move every section before stepping, then check both immediate poses and fixed-body re-imposition. + std::vector positions(count); + for (size_t i = 0; i < count; i++) { + positions[i] = make_float3(1, 0.5f, -0.25f) + offsets[i]; + } + tracker->SetPos(positions); + for (size_t i = 0; i < count; i++) { + if (!approxEqual(tracker->Pos(i), positions[i])) { + std::cout << "Mesh member " << i << " prescribed position mismatch" << std::endl; + return false; + } + } + // Deliberately displace one slave: the combined mapping must restore it even with no contacts. + tracker->SetPos(positions.back() + make_float3(0, 0, 0.01f), count - 1); + sim.DoDynamicsThenSync(5 * kStepSize); + for (size_t i = 0; i < count; i++) { + const float expected_charge = (i == count - 1) ? 2 * charge : charge; + if (!approxEqual(tracker->Pos(i), positions[i]) || tracker->GetOwnerWildcardValue("Q", i) != expected_charge) { + const auto pos = tracker->Pos(i); + std::cout << "Mesh member " << i << " post-step state mismatch: position " << pos.x << ", " << pos.y << ", " + << pos.z << "; Q = " << tracker->GetOwnerWildcardValue("Q", i) << std::endl; + return false; + } + } + return true; +} + +void printContacts(const std::vector>& contacts) { + if (contacts.empty()) { + std::cout << "none"; + return; + } + for (size_t i = 0; i < contacts.size(); i++) { + std::cout << "(" << contacts[i].first << ", " << contacts[i].second << ")"; + if (i + 1 < contacts.size()) { + std::cout << ", "; + } + } +} + +} // namespace + +int main() { + std::cout << "========================================" << std::endl; + std::cout << "DEM Combined Owner Test" << std::endl; + std::cout << "========================================" << std::endl; + + int test_failures = 0; + + const auto suppressed = runScenario(false); + const auto allowed = runScenario(true); + const auto batch = runScenario(false, true); + const auto linear_acc = runMemberAccelerationScenario(false); + const auto angular_acc = runMemberAccelerationScenario(true); + + std::cout << "\n--- Test 1: Combined instance is cached pre-initialize ---" << std::endl; + std::cout << "Combined count before init: " << suppressed.combined_count_before_init << std::endl; + if (suppressed.combined_count_before_init == 1) { + std::cout << "PASS: One combined instance is cached before Initialize()" << std::endl; + } else { + std::cout << "FAIL: Expected one cached combined instance before Initialize()" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 2: Combined tracker covers all members ---" << std::endl; + std::cout << "Total member owners: " << suppressed.total_member_owners << std::endl; + if (suppressed.tracker != nullptr && suppressed.total_member_owners == 2 && + suppressed.tracker_owner_ids.size() == 2 && + suppressed.tracker_owner_ids[0] != suppressed.tracker_owner_ids[1]) { + std::cout << "PASS: Single tracker covers two distinct member owners" << std::endl; + } else { + std::cout << "FAIL: Expected single tracker covering two distinct member owners" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 3: Member positions match combined layout ---" << std::endl; + const float3 expected_pos_0 = kInitPos; + const float3 expected_pos_1 = kInitPos + make_float3(kMemberSpacing, 0, 0); + if (suppressed.tracker_positions.size() >= 2 && approxEqual(suppressed.tracker_positions[0], expected_pos_0) && + approxEqual(suppressed.tracker_positions[1], expected_pos_1)) { + std::cout << "PASS: Member positions match the combined template layout" << std::endl; + } else { + std::cout << "FAIL: Member tracker positions do not match the combined template layout" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 4: Intra-combined contacts are suppressed by default ---" << std::endl; + std::cout << "Clump contacts with default setting: "; + printContacts(suppressed.contacts); + std::cout << std::endl; + if (suppressed.tracker_owner_ids.size() >= 2 && + !containsPair(suppressed.contacts, suppressed.tracker_owner_ids[0], suppressed.tracker_owner_ids[1])) { + std::cout << "PASS: Internal combined-member contact pair is absent by default" << std::endl; + } else { + std::cout << "FAIL: Internal combined-member contact pair should be suppressed by default" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 5: Intra-combined contacts can be enabled explicitly ---" << std::endl; + std::cout << "Clump contacts with allow=true: "; + printContacts(allowed.contacts); + std::cout << std::endl; + if (allowed.tracker_owner_ids.size() >= 2 && + containsPair(allowed.contacts, allowed.tracker_owner_ids[0], allowed.tracker_owner_ids[1])) { + std::cout << "PASS: Internal combined-member contact pair appears when explicitly enabled" << std::endl; + } else { + std::cout << "FAIL: Expected internal combined-member contact pair when allow=true" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 6: Batch instantiation creates correct member count ---" << std::endl; + if (batch.total_member_owners == 4) { + std::cout << "PASS: Batch of 2 combined instances produced 4 member owners" << std::endl; + } else { + std::cout << "FAIL: Expected 4 member owners from batch of 2 combined instances" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 7: Batch instantiation positions are correct ---" << std::endl; + const float3 batch_expected_0 = kInitPos; + const float3 batch_expected_1 = kInitPos + make_float3(kMemberSpacing, 0, 0); + const float3 batch_expected_2 = kInitPos2; + const float3 batch_expected_3 = kInitPos2 + make_float3(kMemberSpacing, 0, 0); + if (batch.tracker_positions.size() >= 4 && approxEqual(batch.tracker_positions[0], batch_expected_0) && + approxEqual(batch.tracker_positions[1], batch_expected_1) && + approxEqual(batch.tracker_positions[2], batch_expected_2) && + approxEqual(batch.tracker_positions[3], batch_expected_3)) { + std::cout << "PASS: All batch member positions match expected combined layouts" << std::endl; + } else { + std::cout << "FAIL: Batch member positions do not match expected combined layouts" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 8: Axial AddAcc preserves pure translation ---" << std::endl; + bool linear_motion_ok = false; + if (linear_acc.initial_positions.size() >= 2 && linear_acc.final_positions.size() >= 2 && + linear_acc.final_velocities.size() >= 2) { + const float3 initial_rel = linear_acc.initial_positions[1] - linear_acc.initial_positions[0]; + const float3 final_rel = linear_acc.final_positions[1] - linear_acc.final_positions[0]; + const float3 displacement_0 = linear_acc.final_positions[0] - linear_acc.initial_positions[0]; + const float3 displacement_1 = linear_acc.final_positions[1] - linear_acc.initial_positions[1]; + linear_motion_ok = approxEqual(final_rel, initial_rel) && approxEqual(displacement_0, displacement_1) && + approxEqual(linear_acc.final_velocities[0], linear_acc.final_velocities[1], kVelTol) && + length(linear_acc.final_velocities[0]) > kVelTol; + } + if (linear_motion_ok) { + std::cout << "PASS: Axial AddAcc on one member moves the combined owner in pure translation" << std::endl; + } else { + std::cout << "FAIL: Axial AddAcc on one member did not produce pure rigid translation" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 9: AddAngAcc preserves rigid rotational motion ---" << std::endl; + bool angular_motion_ok = false; + if (angular_acc.initial_positions.size() >= 2 && angular_acc.final_positions.size() >= 2 && + angular_acc.final_velocities.size() >= 2 && angular_acc.final_ang_velocities.size() >= 2 && + angular_acc.final_orientations.size() >= 2) { + const float3 initial_rel = angular_acc.initial_positions[1] - angular_acc.initial_positions[0]; + const float3 final_rel = angular_acc.final_positions[1] - angular_acc.final_positions[0]; + const float3 expected_member_vel = + angular_acc.final_velocities[0] + cross(angular_acc.final_ang_velocities[0], final_rel); + angular_motion_ok = + std::abs(length(final_rel) - length(initial_rel)) < kPosTol && + approxEqualQuat(angular_acc.final_orientations[0], angular_acc.final_orientations[1]) && + approxEqual(angular_acc.final_ang_velocities[0], angular_acc.final_ang_velocities[1], kVelTol) && + approxEqual(angular_acc.final_velocities[1], expected_member_vel, kVelTol) && + length(angular_acc.final_ang_velocities[0]) > kVelTol; + } + if (angular_motion_ok) { + std::cout << "PASS: AddAngAcc on one member rotates the combined owner as one rigid body" << std::endl; + } else { + std::cout << "FAIL: AddAngAcc on one member introduced non-rigid rotational motion" << std::endl; + test_failures++; + } + + std::cout << "\n--- Test 10: Combined mesh rod positions and owner charges ---" << std::endl; + if (testCombinedMeshRod()) { + std::cout << "PASS: Ten mesh sections retain their prescribed layout and independent owner charges" + << std::endl; + } else { + std::cout << "FAIL: Combined mesh rod tracking, translation, or owner charge mismatch" << std::endl; + test_failures++; + } + + std::cout << "\n========================================" << std::endl; + if (test_failures == 0) { + std::cout << "All tests PASSED!" << std::endl; + std::cout << "========================================" << std::endl; + return 0; + } + + std::cout << "FAILED " << test_failures << " test(s)" << std::endl; + std::cout << "========================================" << std::endl; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_ConeGrazingPlane.cpp b/src/demo/ModularTests/DEMTest_ConeGrazingPlane.cpp new file mode 100644 index 00000000..e93554fd --- /dev/null +++ b/src/demo/ModularTests/DEMTest_ConeGrazingPlane.cpp @@ -0,0 +1,261 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Sharp-point grazing plane test: a cone mesh is driven at prescribed +// horizontal velocity so that its tip grazes along a flat plane mesh (no gravity). +// Uses cone.obj and plane_20by20.obj. +// We inspect the time evolution of the contact force magnitude and direction +// to verify stability of the mesh-mesh contact during grazing. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std::filesystem; +using namespace deme; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + // Material properties (same for cone and plane) + const float mu = 0.3f; // friction coefficient + const float CoR = 0.3f; // coefficient of restitution + const float E = 1e8f; // Young's modulus (Pa) + const float nu = 0.3f; // Poisson's ratio + + // Cone geometry (cone.obj: tip at origin, base at z=1, base radius=1) + const float cone_scale = 0.2f; // scale factor: tip stays at origin + const float cone_height = cone_scale; // 0.2 m after scaling + const float cone_radius = cone_scale; // 0.2 m base radius after scaling + const float cone_density = 2600.0f; // kg/m^3 + const float cone_volume = (1.0f / 3.0f) * PI * cone_radius * cone_radius * cone_height; + const float cone_mass = cone_density * cone_volume; + // MOI of solid cone about centroid (at 3/4 height from tip = 3/4 * cone_height) + // Ix = Iy = 3m/20*r^2 + 3m/80*h^2, Iz = 3m/10*r^2 + const float cone_Ixy = + 3.0f * cone_mass / 20.0f * cone_radius * cone_radius + 3.0f * cone_mass / 80.0f * cone_height * cone_height; + const float cone_Iz = 3.0f * cone_mass / 10.0f * cone_radius * cone_radius; + const float3 cone_moi = make_float3(cone_Ixy, cone_Ixy, cone_Iz); + + // Grazing motion + const float graze_speed = 0.5f; // prescribed x-velocity (m/s) + const float tip_penetration = 0.005f; // initial tip depth below plane (m) + + // The cone centroid is at 3/4 of height from tip (in original mesh coordinates). + // After Scale(cone_scale), centroid is at z = 0.75 * cone_scale above the tip. + const float centroid_above_tip = 0.75f * cone_scale; + + // Simulation time settings + const float step_size = 1e-5f; + const float frame_time = 0.2f; + const float total_time = 2.0f; + + std::cout << "=====================================================" << std::endl; + std::cout << "DEM Sharp-Point Cone Grazing Plane Test" << std::endl; + std::cout << "=====================================================" << std::endl; + std::cout << "Cone scale: " << cone_scale << " m" << std::endl; + std::cout << "Cone mass: " << cone_mass << " kg" << std::endl; + std::cout << "Graze speed: " << graze_speed << " m/s" << std::endl; + std::cout << "Tip penetration: " << tip_penetration << " m" << std::endl; + std::cout << "Total graze dist: " << graze_speed * total_time << " m" << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(30, 30, 5); + // No gravity: we are controlling the cone position/velocity explicitly + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + // Enable mesh-mesh contacts (required for cone-plane mesh contact) + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetExpandSafetyAdder(graze_speed); + DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT", "NORMAL"}); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.0f}}); + + // ========================================================================= + // Flat plane mesh: plane_20by20.obj (fixed) + // The plane has vertices at z=0 with normal (0,0,1). + // ========================================================================= + auto plane = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), mat); + plane->Scale(0.1f); + plane->SetMass(1.0f); + plane->SetMOI(make_float3(1.0f, 1.0f, 1.0f)); + plane->SetInitPos(make_float3(0.f, 0.f, 0.f)); + plane->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + // ========================================================================= + // Cone mesh: cone.obj (prescribed horizontal velocity) + // cone.obj: tip at origin (0,0,0), cone axis along +z, base at z=1, radius=1. + // After InformCentroidPrincipal, centroid is the reference; tip is at + // (0, 0, -0.75) in local frame. + // After Scale(cone_scale), tip is at (0, 0, -0.75*cone_scale) in local frame. + // To place tip at z = -tip_penetration (below plane at z=0): + // SetInitPos z = -tip_penetration + centroid_above_tip + // Cone grazes from x=-0.2 toward x = init_x + graze_speed * total_time. + // ========================================================================= + const float cone_init_x = -0.2f; + const float cone_init_z = -tip_penetration + centroid_above_tip; + + auto cone = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cone.obj").string(), mat); + // Set centroid location (3/4 of cone height from tip) and principal axes + // before scaling, in original mesh coordinates. + cone->InformCentroidPrincipal(make_float3(0.f, 0.f, 0.75f), make_float4(0.f, 0.f, 0.f, 1.f)); + cone->Scale(cone_scale); + cone->SetMass(cone_mass); + cone->SetMOI(cone_moi); + cone->SetInitPos(make_float3(cone_init_x, 0.f, cone_init_z)); + cone->SetFamily(1); + auto cone_tracker = DEMSim.Track(cone); + + // Prescribe: constant horizontal velocity, no vertical motion, no rotation. + DEMSim.SetFamilyPrescribedLinVel(1, to_string_with_precision(graze_speed), "0", "0"); + DEMSim.SetFamilyPrescribedAngVel(1, "0", "0", "0"); + + // ========================================================================= + // Initialize and run + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.Initialize(); + + // After initialization, set cone velocity (prescribed family uses expression + // strings, but tracker SetVel provides an initial push for the first step). + cone_tracker->SetVel(make_float3(graze_speed, 0.f, 0.f)); + + std::cout << "\nCone centroid init pos: (" << cone_init_x << ", 0, " << cone_init_z << ")" << std::endl; + std::cout << "Cone tip init z: " << cone_init_z - centroid_above_tip << " m (plane at z=0)" << std::endl; + std::cout << "Running simulation for " << total_time << " s ..." << std::endl; + std::cout << "----------------------------------------------------" << std::endl; + + path out_dir = current_path() / "modular_test_output" / "DEMTest_ConeGrazingPlane"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + if (dir_ec || !is_directory(out_dir)) { + std::cerr << "Failed to create output directory: " << out_dir << " (" << dir_ec.message() << ")" << std::endl; + return 1; + } + + std::vector force_mags; + std::vector force_z_components; + const int n_frames = static_cast(total_time / frame_time); + + for (int i = 1; i <= n_frames; i++) { + // Output + char meshfilename[200]; + sprintf(meshfilename, "DEMTest_mesh_%04d.vtk", i); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + DEMSim.DoDynamics(frame_time); + + // Contact force = contact acceleration * mass + float3 cnt_acc = cone_tracker->ContactAcc(); + float3 cnt_force = cnt_acc * cone_mass; + double force_mag = std::sqrt((double)cnt_force.x * cnt_force.x + (double)cnt_force.y * cnt_force.y + + (double)cnt_force.z * cnt_force.z); + force_mags.push_back(force_mag); + force_z_components.push_back(static_cast(cnt_force.z)); + + float3 pos = cone_tracker->Pos(); + // Tip position = centroid position - centroid_above_tip in z + float tip_x = pos.x; + float tip_z = pos.z - centroid_above_tip; + + auto cnt_info = DEMSim.GetContactDetailedInfo(0.0f); + float3 avg_normal = make_float3(0.f); + int num_cnts = 0; + const auto& cnt_types = cnt_info->GetContactType(); + const auto& cnt_normals = cnt_info->GetNormal(); + for (size_t k = 0; k < cnt_types.size(); k++) { + if (cnt_types[k] == "MM") { + avg_normal += cnt_normals[k]; + num_cnts++; + } + } + if (num_cnts > 0) { + avg_normal *= 1.0f / static_cast(num_cnts); + } + + std::cout << "t=" << i * frame_time << "s" + << " tip_x=" << tip_x << " m" + << " tip_z=" << tip_z << " m" + << " #MM_contacts=" << num_cnts << " |F_cnt|=" << force_mag << " N"; + + if (force_mag > 1e-3) { + float inv_f = static_cast(1.0 / force_mag); + std::cout << " F_dir=(" << cnt_force.x * inv_f << "," << cnt_force.y * inv_f << "," << cnt_force.z * inv_f + << ")"; + } + const float normal_mag = length(avg_normal); + if (normal_mag > 1e-6f) { + const float inv_nm = 1.0f / normal_mag; + std::cout << " contact_normal=(" << avg_normal.x * inv_nm << "," << avg_normal.y * inv_nm << "," + << avg_normal.z * inv_nm << ")"; + } + std::cout << std::endl; + } + + // ========================================================================= + // Statistics over the contact force time series + // ========================================================================= + if (force_mags.empty()) { + std::cout << "\nNo force data collected." << std::endl; + return 0; + } + double sum_f = 0.0, min_f = force_mags[0], max_f = force_mags[0]; + double sum_fz = 0.0, min_fz = force_z_components[0], max_fz = force_z_components[0]; + for (size_t k = 0; k < force_mags.size(); k++) { + double f = force_mags[k]; + double fz = force_z_components[k]; + sum_f += f; + sum_fz += fz; + if (f < min_f) + min_f = f; + if (f > max_f) + max_f = f; + if (fz < min_fz) + min_fz = fz; + if (fz > max_fz) + max_fz = fz; + } + double mean_f = sum_f / static_cast(force_mags.size()); + double mean_fz = sum_fz / static_cast(force_z_components.size()); + double var = 0.0; + for (double f : force_mags) { + double d = f - mean_f; + var += d * d; + } + double stddev_f = std::sqrt(var / static_cast(force_mags.size())); + + std::cout << "\n=== Contact Force Statistics (over " << force_mags.size() << " frames) ===" << std::endl; + std::cout << " Total force magnitude:" << std::endl; + std::cout << " Mean: " << mean_f << " N" << std::endl; + std::cout << " Min: " << min_f << " N" << std::endl; + std::cout << " Max: " << max_f << " N" << std::endl; + std::cout << " StdDev: " << stddev_f << " N" << std::endl; + std::cout << " Normal (z) component (should be positive = pushing cone up):" << std::endl; + std::cout << " Mean: " << mean_fz << " N" << std::endl; + std::cout << " Min: " << min_fz << " N" << std::endl; + std::cout << " Max: " << max_fz << " N" << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_ConeGrazingPlane exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_ConeGrazingSphere.cpp b/src/demo/ModularTests/DEMTest_ConeGrazingSphere.cpp new file mode 100644 index 00000000..da84aebe --- /dev/null +++ b/src/demo/ModularTests/DEMTest_ConeGrazingSphere.cpp @@ -0,0 +1,319 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Cone-tip grazing sphere test: a cone mesh is driven in a circular orbit so +// that its tip grazes the surface of a fixed sphere mesh (no gravity). +// Uses cone.obj and sphere.obj from the data folder. +// The sphere is fixed at the origin; the cone orbits around it at a prescribed +// angular velocity, keeping the tip in contact with the sphere surface. +// We inspect the time evolution of the contact force magnitude and direction +// to verify stability of the mesh-mesh contact during circular grazing. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std::filesystem; +using namespace deme; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + // Material properties (same for cone and sphere) + const float mu = 0.3f; // friction coefficient + const float CoR = 0.3f; // coefficient of restitution + const float E = 1e8f; // Young's modulus (Pa) + const float nu = 0.3f; // Poisson's ratio + + // Sphere geometry (sphere.obj: unit sphere centered at origin, radius=1) + const float sphere_scale = 0.25f; // sphere radius after scaling (m) + + // Cone geometry (cone.obj: tip at origin, base at z=1, base radius=1) + const float cone_scale = 0.2f; // scale factor + const float cone_height = cone_scale; // 0.2 m after scaling + const float cone_radius = cone_scale; // 0.2 m base radius after scaling + const float cone_density = 2600.0f; // kg/m^3 + const float cone_volume = (1.0f / 3.0f) * PI * cone_radius * cone_radius * cone_height; + const float cone_mass = cone_density * cone_volume; + // MOI of solid cone about centroid (at 3/4 height from tip = 3/4 * cone_height) + // Ix = Iy = 3m/20*r^2 + 3m/80*h^2, Iz = 3m/10*r^2 + const float cone_Ixy = + 3.0f * cone_mass / 20.0f * cone_radius * cone_radius + 3.0f * cone_mass / 80.0f * cone_height * cone_height; + const float cone_Iz = 3.0f * cone_mass / 10.0f * cone_radius * cone_radius; + const float3 cone_moi = make_float3(cone_Ixy, cone_Ixy, cone_Iz); + + // Orbit parameters + const float orbit_omega = 2.0f; // angular speed of orbit (rad/s) + const float tip_penetration = 0.005f; // initial tip depth into sphere surface (m) + + // The cone centroid is at 3/4 of height from tip (in original mesh coordinates). + // After Scale(cone_scale), centroid is at z = 0.75 * cone_scale above the tip. + const float centroid_above_tip = 0.75f * cone_scale; + + // The cone centroid orbits at a radius such that the tip is at + // (sphere_scale - tip_penetration) from the sphere center. + const float orbit_radius = sphere_scale - tip_penetration + centroid_above_tip; + + // Tangential speed of the orbiting centroid + const float orbit_vel = orbit_radius * orbit_omega; + + // Simulation time settings + const float step_size = 5e-6f; + const float frame_time = 0.2f; + const int n_sub_samples = 500; // sub-samples per frame for force averaging + const float sub_dt = frame_time / static_cast(n_sub_samples); // sub-step size + const float total_time = 2.0f; + + std::cout << "=====================================================" << std::endl; + std::cout << "DEM Cone-Tip Grazing Sphere Test" << std::endl; + std::cout << "=====================================================" << std::endl; + std::cout << "Sphere radius: " << sphere_scale << " m (fixed)" << std::endl; + std::cout << "Cone scale: " << cone_scale << " m" << std::endl; + std::cout << "Cone mass: " << cone_mass << " kg" << std::endl; + std::cout << "Orbit radius: " << orbit_radius << " m (centroid)" << std::endl; + std::cout << "Tip-sphere gap: " << -(tip_penetration) << " m (negative = penetration)" << std::endl; + std::cout << "Orbit omega: " << orbit_omega << " rad/s" << std::endl; + std::cout << "Orbit speed: " << orbit_vel << " m/s" << std::endl; + std::cout << "Total graze angle: " << orbit_omega * total_time << " rad" << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(5, 5, 5); + // No gravity: we are controlling the cone motion explicitly + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + // Enable mesh-mesh contacts (required for cone-sphere mesh contact) + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetExpandSafetyAdder(orbit_vel); + DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT", "NORMAL"}); + // DEMSim.SetSimplePatchCombination(true); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.0f}}); + + // ========================================================================= + // Sphere mesh: sphere.obj (fixed at origin) + // sphere.obj is a unit sphere centered at (0,0,0). + // ========================================================================= + auto sphere = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/sphere.obj").string(), mat); + sphere->Scale(sphere_scale); + sphere->SetMass(1000.0f); + sphere->SetMOI(make_float3(1000.0f, 1000.0f, 1000.0f)); + sphere->SetInitPos(make_float3(0.f, 0.f, 0.f)); + sphere->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + // ========================================================================= + // Cone mesh: cone.obj (prescribed circular orbit) + // cone.obj: tip at origin (0,0,0), cone axis along +z, base at z=1, radius=1. + // + // We orient the cone so its axis (from tip to base) points in the +x direction + // at t=0, which means the tip is on the -x side (toward the sphere center). + // This requires rotating the cone's local +z axis to align with world +x: + // rotation about y-axis by +90 deg -> Q = (0, sin(pi/4), 0, cos(pi/4)) + // + // Initial centroid position: (orbit_radius, 0, 0). + // Tip position = centroid - centroid_above_tip * axis_dir + // = (orbit_radius - centroid_above_tip, 0, 0) + // = (sphere_scale - tip_penetration, 0, 0) [inside sphere surface] + // + // Prescribed motion: + // - Linear velocity: circular orbit in the xy-plane + // vx = -orbit_vel * sin(orbit_omega * t) + // vy = orbit_vel * cos(orbit_omega * t) + // - Angular velocity: constant rotation about world z-axis at orbit_omega, + // keeping the cone axis (from tip to base) always pointing radially outward. + // ========================================================================= + const float q_y = static_cast(std::sqrt(2.0) / 2.0); // sin(pi/4) = cos(pi/4) + float4 cone_init_q = make_float4(0.f, q_y, 0.f, q_y); // float4(qx,qy,qz,qw) + + auto cone = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cone.obj").string(), mat); + cone->InformCentroidPrincipal(make_float3(0.f, 0.f, 0.75f), make_float4(0.f, 0.f, 0.f, 1.f)); + cone->Scale(cone_scale); + cone->SetMass(cone_mass); + cone->SetMOI(cone_moi); + cone->SetInitPos(make_float3(orbit_radius, 0.f, 0.f)); + cone->SetInitQuat(cone_init_q); + cone->SetFamily(1); + auto cone_tracker = DEMSim.Track(cone); + + // Prescribe circular orbit: tangential velocity as a function of time. + // The variable 't' represents simulation time in expression strings. + std::string vx_expr = + to_string_with_precision(-orbit_vel) + " * sin(" + to_string_with_precision(orbit_omega) + " * t)"; + std::string vy_expr = + to_string_with_precision(orbit_vel) + " * cos(" + to_string_with_precision(orbit_omega) + " * t)"; + DEMSim.SetFamilyPrescribedLinVel(1, vx_expr, vy_expr, "0"); + // Prescribe constant angular velocity about world z-axis to keep cone + // pointed radially outward as it orbits. + DEMSim.SetFamilyPrescribedAngVel(1, to_string_with_precision(-orbit_omega), "0", "0"); + + // ========================================================================= + // Initialize and run + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.Initialize(); + + // Set initial velocity to match the prescribed formula at t=0. + // At t=0: vx = -orbit_vel*sin(0) = 0, vy = orbit_vel*cos(0) = orbit_vel. + cone_tracker->SetVel(make_float3(0.f, orbit_vel, 0.f)); + + const float tip_init_x = orbit_radius - centroid_above_tip; + std::cout << "\nCone centroid init pos: (" << orbit_radius << ", 0, 0)" << std::endl; + std::cout << "Cone tip init pos: (" << tip_init_x << ", 0, 0) m (sphere surface at r=" << sphere_scale << ")" + << std::endl; + std::cout << "Running simulation for " << total_time << " s ..." << std::endl; + std::cout << "----------------------------------------------------" << std::endl; + + path out_dir = current_path() / "modular_test_output" / "DEMTest_ConeGrazingSphere"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + if (dir_ec || !is_directory(out_dir)) { + std::cerr << "Failed to create output directory: " << out_dir << " (" << dir_ec.message() << ")" << std::endl; + return 1; + } + + std::vector force_mags; + std::vector force_r_components; // radial (outward) force component + const int n_frames = static_cast(total_time / frame_time); + + for (int i = 1; i <= n_frames; i++) { + // Output + char meshfilename[200]; + sprintf(meshfilename, "DEMTest_mesh_%04d.vtk", i); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + // Advance simulation in sub-steps and accumulate contact force readings + // to compute the average force over this frame interval. + double sum_fx = 0.0, sum_fy = 0.0, sum_fz = 0.0; + for (int s = 0; s < n_sub_samples; s++) { + DEMSim.DoDynamics(sub_dt); + float3 cnt_acc = cone_tracker->ContactAcc(); + sum_fx += cnt_acc.x; + sum_fy += cnt_acc.y; + sum_fz += cnt_acc.z; + } + + // Average contact force = (average contact acceleration) * mass + float3 avg_cnt_force; + avg_cnt_force.x = static_cast(sum_fx / n_sub_samples) * cone_mass; + avg_cnt_force.y = static_cast(sum_fy / n_sub_samples) * cone_mass; + avg_cnt_force.z = static_cast(sum_fz / n_sub_samples) * cone_mass; + double force_mag = + std::sqrt((double)avg_cnt_force.x * avg_cnt_force.x + (double)avg_cnt_force.y * avg_cnt_force.y + + (double)avg_cnt_force.z * avg_cnt_force.z); + force_mags.push_back(force_mag); + + // Radial direction at the center of this frame interval: cone is at angle + // orbit_omega * (i - 0.5) * frame_time from the +x axis. + float t_center = (i - 0.5f) * frame_time; + float radial_x = std::cos(orbit_omega * t_center); + float radial_y = std::sin(orbit_omega * t_center); + // Radial force component (positive = pushes cone outward away from sphere) + double force_r = avg_cnt_force.x * radial_x + avg_cnt_force.y * radial_y; + force_r_components.push_back(force_r); + + float3 pos = cone_tracker->Pos(); + // Tip position = centroid - centroid_above_tip * axis_dir + float t_end = static_cast(i) * frame_time; + float tip_r = std::sqrt(pos.x * pos.x + pos.y * pos.y) - centroid_above_tip; + + auto cnt_info = DEMSim.GetContactDetailedInfo(0.0f); + float3 avg_normal = make_float3(0.f); + int num_cnts = 0; + const auto& cnt_types = cnt_info->GetContactType(); + const auto& cnt_normals = cnt_info->GetNormal(); + for (size_t k = 0; k < cnt_types.size(); k++) { + if (cnt_types[k] == "MM") { + avg_normal += cnt_normals[k]; + num_cnts++; + } + } + if (num_cnts > 0) { + avg_normal *= 1.0f / static_cast(num_cnts); + } + + std::cout << "t=" << t_end << "s (avg over " << n_sub_samples << " sub-steps)" + << " centroid_r=" << std::sqrt(pos.x * pos.x + pos.y * pos.y) << " m" + << " tip_r~" << tip_r << " m" + << " #MM_contacts=" << num_cnts << " |F_cnt_avg|=" << force_mag << " N"; + + if (force_mag > 1e-3) { + float inv_f = static_cast(1.0 / force_mag); + std::cout << " F_dir=(" << avg_cnt_force.x * inv_f << "," << avg_cnt_force.y * inv_f << "," + << avg_cnt_force.z * inv_f << ")"; + } + const float normal_mag = length(avg_normal); + if (normal_mag > 1e-6f) { + const float inv_nm = 1.0f / normal_mag; + std::cout << " contact_normal=(" << avg_normal.x * inv_nm << "," << avg_normal.y * inv_nm << "," + << avg_normal.z * inv_nm << ")"; + } + std::cout << std::endl; + } + + // ========================================================================= + // Statistics over the contact force time series + // ========================================================================= + if (force_mags.empty()) { + std::cout << "\nNo force data collected." << std::endl; + return 0; + } + double sum_f = 0.0, min_f = force_mags[0], max_f = force_mags[0]; + double sum_fr = 0.0, min_fr = force_r_components[0], max_fr = force_r_components[0]; + for (size_t k = 0; k < force_mags.size(); k++) { + double f = force_mags[k]; + double fr = force_r_components[k]; + sum_f += f; + sum_fr += fr; + if (f < min_f) + min_f = f; + if (f > max_f) + max_f = f; + if (fr < min_fr) + min_fr = fr; + if (fr > max_fr) + max_fr = fr; + } + double mean_f = sum_f / static_cast(force_mags.size()); + double mean_fr = sum_fr / static_cast(force_r_components.size()); + double var = 0.0; + for (double f : force_mags) { + double d = f - mean_f; + var += d * d; + } + double stddev_f = std::sqrt(var / static_cast(force_mags.size())); + + std::cout << "\n=== Contact Force Statistics (over " << force_mags.size() << " frames, each averaged over " + << n_sub_samples << " sub-steps) ===" << std::endl; + std::cout << " Total force magnitude:" << std::endl; + std::cout << " Mean: " << mean_f << " N" << std::endl; + std::cout << " Min: " << min_f << " N" << std::endl; + std::cout << " Max: " << max_f << " N" << std::endl; + std::cout << " StdDev: " << stddev_f << " N" << std::endl; + std::cout << " Radial (outward) component (should be positive = pushing cone away from sphere):" << std::endl; + std::cout << " Mean: " << mean_fr << " N" << std::endl; + std::cout << " Min: " << min_fr << " N" << std::endl; + std::cout << " Max: " << max_fr << " N" << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_ConeGrazingSphere exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_ContactIslandFlooding.cpp b/src/demo/ModularTests/DEMTest_ContactIslandFlooding.cpp new file mode 100644 index 00000000..bc6ede22 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_ContactIslandFlooding.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include + +#include + +using namespace deme; + +int main() { + constexpr int kNumQuads = 200; + constexpr int kNumTriangles = 2 * kNumQuads; + constexpr float kDx = 0.01f; + constexpr float kWidth = 0.1f; + constexpr float kPenetration = 0.001f; + constexpr float kStepSize = 1e-6f; + + DEMSolver sim; + sim.SetVerbosity("ERROR"); + sim.InstructBoxDomainDimension(5.f, 5.f, 5.f); + sim.SetGravitationalAcceleration(make_float3(0.f)); + sim.SetMeshUniversalContact(true); + sim.SetExpandSafetyType("auto"); + + auto mat = sim.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.3}, {"mu", 0.3}}); + sim.AddBCPlane(make_float3(0.f), make_float3(0.f, 0.f, 1.f), mat); + + DEMMesh strip; + strip.m_vertices.reserve(2 * (kNumQuads + 1)); + strip.m_face_v_indices.reserve(kNumTriangles); + for (int i = 0; i <= kNumQuads; ++i) { + const float x = (i - kNumQuads / 2) * kDx; + strip.m_vertices.push_back(make_float3(x, -0.5f * kWidth, -kPenetration)); + strip.m_vertices.push_back(make_float3(x, 0.5f * kWidth, -kPenetration)); + } + for (int i = 0; i < kNumQuads; ++i) { + const int bottom = 2 * i; + const int top = bottom + 1; + const int next_bottom = bottom + 2; + const int next_top = bottom + 3; + // This ordering makes the triangle adjacency graph a path with diameter kNumTriangles - 1. + strip.m_face_v_indices.push_back(make_int3(bottom, next_bottom, top)); + strip.m_face_v_indices.push_back(make_int3(next_bottom, next_top, top)); + } + strip.nTri = kNumTriangles; + strip.SetMaterial(mat); + strip.SetMass(1.f); + strip.SetMOI(make_float3(1.f)); + auto strip_handle = sim.AddMesh(strip); + strip_handle->SetFamily(1); + + sim.SetTimeStepSize(kStepSize); + sim.Initialize(); + sim.DoDynamics(kStepSize); + + const size_t patch_contacts = sim.GetNumContacts(); + if (patch_contacts != 1) { + std::cerr << "FAIL: expected one island for a connected " << kNumTriangles << "-triangle contact strip, got " + << patch_contacts << std::endl; + return 1; + } + + std::cout << "PASS: connected " << kNumTriangles << "-triangle contact strip produced one island." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_CudaHeaderDiscovery.cpp b/src/demo/ModularTests/DEMTest_CudaHeaderDiscovery.cpp new file mode 100644 index 00000000..00bfd231 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_CudaHeaderDiscovery.cpp @@ -0,0 +1,42 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include + +#include "core/utils/JitHelper.h" + +// A native process must ignore the old Python environment override. Building a +// program selects and validates headers without compiling the full solver kernels. +int main() { + const auto directory = + std::filesystem::temp_directory_path() / + ("deme-native-headers-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory); + const auto source = directory / "probe.cu"; + std::ofstream(source) << "extern \"C\" __global__ void probe() {}\n"; +#ifdef _WIN32 + _putenv_s("DEME_CUDA_INCLUDE_PATH", "Z:/missing-python-cuda-headers"); + _putenv_s("DEME_JIT_CACHE_DIR", directory.string().c_str()); +#else + setenv("DEME_CUDA_INCLUDE_PATH", "/missing-python-cuda-headers", 1); + setenv("DEME_JIT_CACHE_DIR", directory.string().c_str(), 1); +#endif + try { + const auto program = JitHelper::buildProgram("native_header_probe", source); + if (!std::filesystem::exists(directory / program.key() / "fingerprint.txt")) { + throw std::runtime_error("Native header selection did not create a program fingerprint"); + } + std::filesystem::remove_all(directory); + std::cout << "PASS: native CUDA discovery ignores Python configuration\n"; + return 0; + } catch (const std::exception& error) { + std::filesystem::remove_all(directory); + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } +} diff --git a/src/demo/ModularTests/DEMTest_CylinderCrossRotation.cpp b/src/demo/ModularTests/DEMTest_CylinderCrossRotation.cpp new file mode 100644 index 00000000..543e33da --- /dev/null +++ b/src/demo/ModularTests/DEMTest_CylinderCrossRotation.cpp @@ -0,0 +1,242 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Cylinder-cylinder cross-to-co-axial rotation test: Two cylinders +// (cyl_r1_h2.obj) start in a "cross" configuration (axes perpendicular, +// intersecting at the origin). Cylinder A is fixed; Cylinder B is slowly +// rotated about the world y-axis at a constant angular rate until both +// cylinders are co-axial (axes parallel along z, with axis-direction overlap). +// At each frame we record the average contact-force magnitude and the averaged +// contact normal direction to show that both metrics evolve smoothly through +// the full 90-degree rotation, demonstrating the stability of the mesh-mesh +// contact derivation. +// Uses cyl_r1_h2.obj. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace std::filesystem; +using namespace deme; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + // Material properties + const float mu = 0.3f; // friction coefficient + const float CoR = 0.3f; // coefficient of restitution + const float E = 1e8f; // Young's modulus (Pa) + const float nu = 0.3f; // Poisson's ratio + + // cyl_r1_h2.obj: cylinder with radius=1, height=2 (axis along z, z in [-1,1]) + const float cyl_scale = 0.1f; // scale factor + const float cyl_radius = cyl_scale; // 0.1 m after scaling + const float cyl_height = 2.0f * cyl_scale; // 0.2 m after scaling + const float cyl_density = 2600.0f; // kg/m^3 + const float cyl_volume = (float)PI * cyl_radius * cyl_radius * cyl_height; + const float cyl_mass = cyl_density * cyl_volume; + // MOI of solid cylinder about centroid: + // Ixx = Iyy = m*(3*r^2 + h^2)/12, Izz = m*r^2/2 + const float Ixy = cyl_mass * (3.0f * cyl_radius * cyl_radius + cyl_height * cyl_height) / 12.0f; + const float Iz = cyl_mass * cyl_radius * cyl_radius / 2.0f; + const float3 cyl_moi = make_float3(Ixy, Ixy, Iz); + + // Rotation: Cylinder B rotates from 90 deg (cross) to 0 deg (co-axial) + // about the world y-axis. omega_y < 0 takes the axis from +x toward +z. + const float total_time = 1.0f; // s + const float omega_y = -(float)(PI / 2.0) / total_time; // rad/s + + // Simulation time settings + const float frame_time = 0.05f; // 20 frames total + const float step_size = frame_time; + const int n_frames = static_cast(total_time / frame_time); + + std::cout << "=====================================================" << std::endl; + std::cout << "DEM Cylinder-Cylinder Cross-to-Co-axial Rotation Test" << std::endl; + std::cout << "=====================================================" << std::endl; + std::cout << "Cylinder radius: " << cyl_radius << " m" << std::endl; + std::cout << "Cylinder height: " << cyl_height << " m" << std::endl; + std::cout << "Cylinder mass: " << cyl_mass << " kg" << std::endl; + std::cout << "omega_y: " << omega_y << " rad/s" << std::endl; + std::cout << "Total rotation: " << omega_y * total_time * 180.0f / (float)PI << " deg over " << total_time << " s" + << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(20, 20, 20); + // No gravity: motion is fully prescribed + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + // Enable mesh-mesh contacts (required for cylinder-cylinder contact) + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + // Enable contact-normal output for the detailed info query + DEMSim.SetContactOutputContent({"FORCE", "NORMAL", "OWNER"}); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.0f}}); + + // ========================================================================= + // Cylinder A (fixed): axis along z, centered at origin. + // cyl_r1_h2.obj already has its axis along z, centered at origin. + // ========================================================================= + auto cyl_a = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cyl_r1_h2.obj").string(), mat); + cyl_a->Scale(cyl_scale); + cyl_a->SetMass(cyl_mass); + cyl_a->SetMOI(cyl_moi); + cyl_a->SetInitPos(make_float3(0.f, 0.f, 0.f)); + cyl_a->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + // ========================================================================= + // Cylinder B (rotating): starts with axis along +x (cross configuration). + // In the OBJ the axis is along +z, so we rotate +90 deg about the world + // y-axis to align the axis with +x. Then we prescribe a constant angular + // velocity omega_y about y to rotate the axis back to +z (co-axial). + // The centre stays at the origin (linear velocity locked to zero). + // ========================================================================= + float4 cyl_b_init_q = make_float4(0.f, 0.f, 0.f, 1.f); + cyl_b_init_q = RotateQuat(cyl_b_init_q, make_float3(0.f, 1.f, 0.f), (float)(PI / 2.0)); + + auto cyl_b = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cyl_r1_h2.obj").string(), mat); + cyl_b->Scale(cyl_scale); + cyl_b->SetMass(cyl_mass); + cyl_b->SetMOI(cyl_moi); + cyl_b->SetInitPos(make_float3(0.f, 0.f, 0.05f)); + cyl_b->SetInitQuat(cyl_b_init_q); + cyl_b->SetFamily(1); + auto cyl_b_tracker = DEMSim.Track(cyl_b); + + // Cylinder B: fixed centre, constant rotation about world y-axis + DEMSim.SetFamilyPrescribedLinVel(1, "0", "0", "0"); + DEMSim.SetFamilyPrescribedAngVel(1, "0", to_string_with_precision(omega_y), "0"); + + // ========================================================================= + // Initialize and run + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetInitBinNumTarget(10); + DEMSim.SetErrorOutAvgContacts(10000); + DEMSim.Initialize(); + + // The two cylinders start intersecting; tell the solver the expected + // initial tri-tri penetration so it can handle pre-existing overlap. + // Must be called after Initialize(). + // DEMSim.SetTriTriPenetration(cyl_radius * 0.5); + + std::cout << "\nRunning simulation for " << total_time << " s ..." << std::endl; + std::cout << "Cylinder B axis starts along +x (90 deg from z) and ends along +z (co-axial)." << std::endl; + std::cout << "----------------------------------------------------" << std::endl; + + path out_dir = current_path() / "modular_test_output" / "DEMTest_CylinderCrossRotation"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + if (dir_ec || !is_directory(out_dir)) { + std::cerr << "Failed to create output directory: " << out_dir << " (" << dir_ec.message() << ")" << std::endl; + return 1; + } + + std::vector force_mags; + std::vector avg_normals; + + for (int i = 1; i <= n_frames; i++) { + DEMSim.DoDynamicsThenSync(frame_time); + + // Write mesh snapshot at the start of this interval + char meshfilename[200]; + sprintf(meshfilename, "DEMTest_mesh_%04d.vtk", i); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + // Query the detailed contact info to get average contact normal + // (0.0 threshold: include all contacts, even those with zero force) + auto cnt_info = DEMSim.GetContactDetailedInfo(0.0f); + float3 avg_normal = make_float3(0.f), avg_force = make_float3(0.f); + int mm_cnt = 0; + const auto& cnt_types = cnt_info->GetContactType(); + const auto& cnt_normals = cnt_info->GetNormal(); + const auto& cnt_forces = cnt_info->GetForce(); + for (size_t k = 0; k < cnt_types.size(); k++) { + if (cnt_types[k] == "MM") { // mesh-mesh (triangle--triangle) contacts + avg_normal += cnt_normals[k]; + avg_force += cnt_forces[k]; + mm_cnt++; + } + } + if (mm_cnt > 0) { + float inv_n = 1.0f / static_cast(mm_cnt); + avg_normal *= inv_n; + avg_force *= inv_n; + } + avg_normals.push_back(avg_normal); + force_mags.push_back(length(avg_force)); + + // Current angle of Cylinder B axis from the z-axis + float angle_deg = 90.0f + omega_y * static_cast(i) * frame_time * 180.0f / (float)PI; + + std::cout << "t=" << i * frame_time << "s" + << " axis_angle_from_z=" << angle_deg << " deg" + << " |F_cnt_avg|=" << length(avg_force) << " N" + << " #MM_contacts=" << mm_cnt; + + if (mm_cnt > 0) { + float nm = + std::sqrt(avg_normal.x * avg_normal.x + avg_normal.y * avg_normal.y + avg_normal.z * avg_normal.z); + if (nm > 1e-6f) { + float inv_nm = 1.0f / nm; + std::cout << " avg_normal=(" << avg_normal.x * inv_nm << "," << avg_normal.y * inv_nm << "," + << avg_normal.z * inv_nm << ")"; + } + } + std::cout << std::endl; + } + + // ========================================================================= + // Summary statistics + // ========================================================================= + if (force_mags.empty()) { + std::cout << "\nNo force data collected." << std::endl; + return 0; + } + + double sum_f = 0.0, min_f = force_mags[0], max_f = force_mags[0]; + for (double f : force_mags) { + sum_f += f; + if (f < min_f) + min_f = f; + if (f > max_f) + max_f = f; + } + double mean_f = sum_f / static_cast(force_mags.size()); + double var = 0.0; + for (double f : force_mags) { + double d = f - mean_f; + var += d * d; + } + double stddev_f = std::sqrt(var / static_cast(force_mags.size())); + + std::cout << "\n=== Contact Force Statistics (over " << force_mags.size() << " frames) ===" << std::endl; + std::cout << " Mean: " << mean_f << " N" << std::endl; + std::cout << " Min: " << min_f << " N" << std::endl; + std::cout << " Max: " << max_f << " N" << std::endl; + std::cout << " StdDev: " << stddev_f << " N" << std::endl; + std::cout << " (Smooth force evolution indicates stable contact derivation.)" << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_CylinderCrossRotation exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_DeviceDataRetrieval.cpp b/src/demo/ModularTests/DEMTest_DeviceDataRetrieval.cpp new file mode 100644 index 00000000..22d1e49a --- /dev/null +++ b/src/demo/ModularTests/DEMTest_DeviceDataRetrieval.cpp @@ -0,0 +1,744 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include + +#include +#include +#include +#include +#include + +#include "DEM/API.h" +#include "core/ApiVersion.h" +#include "core/utils/DataMigrationHelper.hpp" + +using namespace deme; + +namespace { + +constexpr float kTolerance = 2e-5f; + +bool close(float a, float b) { + return std::abs(a - b) <= kTolerance; +} + +bool close(const float3& a, const float3& b) { + return close(a.x, b.x) && close(a.y, b.y) && close(a.z, b.z); +} + +bool close(const float4& a, const float4& b) { + return close(a.x, b.x) && close(a.y, b.y) && close(a.z, b.z) && close(a.w, b.w); +} + +// The direct wrench reduction uses owner-local contact arms, while its reference below reconstructs those arms by +// subtracting packed global float positions. Compare resultants with scale-aware tolerances so coordinate packing and +// atomic accumulation order do not turn an algebraically equivalent result into a brittle test. +bool closeWrenchComponent(float a, float b) { + constexpr float absolute_tolerance = 2e-4f; + constexpr float relative_tolerance = 2e-5f; + return std::abs(a - b) <= absolute_tolerance + relative_tolerance * std::max(std::abs(a), std::abs(b)); +} + +bool compareWrenchVectors(const std::vector& actual, + const std::vector& expected, + const std::string& label) { + if (actual.size() != expected.size()) { + std::cerr << "FAIL: " << label << " size mismatch." << std::endl; + return false; + } + for (size_t i = 0; i < actual.size(); i++) { + const float3& a = actual[i]; + const float3& b = expected[i]; + if (!closeWrenchComponent(a.x, b.x) || !closeWrenchComponent(a.y, b.y) || !closeWrenchComponent(a.z, b.z)) { + std::cerr << "FAIL: " << label << " differs at index " << i << ": actual=(" << a.x << ", " << a.y << ", " + << a.z << "), expected=(" << b.x << ", " << b.y << ", " << b.z << ")." << std::endl; + return false; + } + } + return true; +} + +// Owner positions are stored as a voxel ID plus uint16 sub-voxel coordinates. A device set/get round trip therefore +// has a domain-dependent quantization error even though the API input and output are float3 values. +bool comparePositionVectors(const std::vector& actual, + const std::vector& expected, + const std::string& label) { + constexpr float position_tolerance = 2e-4f; + if (actual.size() != expected.size()) { + std::cerr << "FAIL: " << label << " size mismatch." << std::endl; + return false; + } + for (size_t i = 0; i < actual.size(); i++) { + const float3& a = actual[i]; + const float3& b = expected[i]; + if (std::abs(a.x - b.x) > position_tolerance || std::abs(a.y - b.y) > position_tolerance || + std::abs(a.z - b.z) > position_tolerance) { + std::cerr << "FAIL: " << label << " differs at index " << i << ": actual=(" << a.x << ", " << a.y << ", " + << a.z << "), expected=(" << b.x << ", " << b.y << ", " << b.z << ")." << std::endl; + return false; + } + } + return true; +} + +template +class TestDeviceBuffer { + public: + TestDeviceBuffer(size_t count, int device) : m_count(count), m_device(device) { + ScopedCudaDevice scope(device); + DEME_GPU_CALL(cudaMalloc(reinterpret_cast(&m_pointer), count * sizeof(T))); + } + + ~TestDeviceBuffer() { + ScopedCudaDevice scope(m_device); + (void)cudaFree(m_pointer); + } + + T* data() { return m_pointer; } + size_t size() const { return m_count; } + int device() const { return m_device; } + + void FromHost(const std::vector& input) { + if (input.size() > m_count) { + DEME_ERROR("Test device input has %zu elements but its buffer holds only %zu.", input.size(), m_count); + } + ScopedCudaDevice scope(m_device); + DEME_GPU_CALL(cudaMemcpy(m_pointer, input.data(), input.size() * sizeof(T), cudaMemcpyHostToDevice)); + } + + std::vector ToHost(size_t count = 0) const { + const size_t output_count = count == 0 ? m_count : count; + std::vector output(output_count); + ScopedCudaDevice scope(m_device); + DEME_GPU_CALL(cudaMemcpy(output.data(), m_pointer, output_count * sizeof(T), cudaMemcpyDeviceToHost)); + return output; + } + + private: + T* m_pointer = nullptr; + size_t m_count; + int m_device; +}; + +template +bool compareVectors(const std::vector& actual, const std::vector& expected, const std::string& label) { + if (actual.size() != expected.size()) { + std::cerr << "FAIL: " << label << " size mismatch." << std::endl; + return false; + } + for (size_t i = 0; i < actual.size(); i++) { + if (!close(actual[i], expected[i])) { + std::cerr << "FAIL: " << label << " differs at index " << i << "." << std::endl; + return false; + } + } + return true; +} + +template <> +bool compareVectors(const std::vector& actual, + const std::vector& expected, + const std::string& label) { + if (actual != expected) { + std::cerr << "FAIL: " << label << " differs." << std::endl; + return false; + } + return true; +} + +bool testFixedOwnerData(int visible_devices) { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(8, 8, 8); + solver.SetGravitationalAcceleration(make_float3(0, 0, 0)); + solver.SetCDUpdateFreq(1); + + // Use a deterministic force-only torque contribution so the wrench comparison covers extra torque without + // depending on the Hertz model's collision-age gate for rolling resistance. + auto force_model = solver.DefineContactForceModel(R"( +if (overlapDepth > 0.0) { + force = B2A * (1.0e3f * static_cast(overlapDepth)); + torque_only_force = make_float3(0.f, 17.f, 0.f); +} +)"); + force_model->SetPerOwnerWildcards({"retrieval_tag"}); + auto material = solver.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.4}, {"Crr", 0.02}}); + auto sphere = solver.LoadSphereType(2.5f, 0.5f, material); + sphere->SetMOI(make_float3(0.21f, 0.37f, 0.52f)); + + const std::vector positions = {make_float3(1.0f, 1.0f, 1.0f), make_float3(1.8f, 1.0f, 1.0f), + make_float3(4.0f, 2.0f, 1.5f)}; + const std::vector velocities = {make_float3(1, 2, 3), make_float3(-1, 0.5f, 2), make_float3(0, -2, 1)}; + const std::vector angular_velocities = {make_float3(1, 0, 0), make_float3(0, 2, 0), make_float3(0, 0, 3)}; + const float half_sqrt_two = std::sqrt(0.5f); + const std::vector orientations = {make_float4(0, 0, half_sqrt_two, half_sqrt_two), + make_float4(half_sqrt_two, 0, 0, half_sqrt_two), make_float4(0, 0, 0, 1)}; + const std::vector families = {2, 3, 4}; + const std::vector wildcards = {10.5f, -2.0f, 7.25f}; + + DEMClumpBatch input_batch(3); + input_batch.SetTypes(sphere); + input_batch.SetPos(positions); + input_batch.SetVel(velocities); + input_batch.SetAngVel(angular_velocities); + input_batch.SetOriQ(orientations); + input_batch.SetFamilies(families); + input_batch.AddOwnerWildcard("retrieval_tag", wildcards); + auto batch = solver.AddClumps(input_batch); + auto tracker = solver.Track(batch); + + solver.SetTimeStepSize(1e-5); + solver.Initialize(); + + const size_t count = positions.size(); + TestDeviceBuffer float3_output(count, 0); + TestDeviceBuffer float4_output(count, 0); + TestDeviceBuffer float_output(count, 0); + TestDeviceBuffer uint_output(count, 0); + + // Set all coupling state directly from CUDA memory, then round-trip it through the independent output kernels. + const std::vector prescribed_positions = {make_float3(1.1f, 1.2f, 1.3f), make_float3(1.85f, 1.1f, 1.0f), + make_float3(4.1f, 2.2f, 1.7f)}; + const std::vector prescribed_velocities = {make_float3(2, 3, 4), make_float3(-2, 1, 0.5f), + make_float3(0.25f, -1, 2)}; + const std::vector prescribed_global_angular_velocities = { + make_float3(0.5f, 1.5f, -2), make_float3(-1, 0.25f, 2.5f), make_float3(3, -2, 1)}; + const std::vector prescribed_orientations = {make_float4(0, 0, 0, 1), + make_float4(0, half_sqrt_two, 0, half_sqrt_two), + make_float4(0.182574f, 0.365148f, 0.547723f, 0.730297f)}; + const std::vector unnormalized_orientations = { + prescribed_orientations[0] * 2.f, prescribed_orientations[1] * 3.f, prescribed_orientations[2] * 4.f}; + TestDeviceBuffer float3_input(count, 0); + TestDeviceBuffer float4_input(count, 0); + float3_input.FromHost(prescribed_positions); + tracker->SetPositionsFromDevice(float3_input.data(), 0, false); + float4_input.FromHost(unnormalized_orientations); + tracker->SetOrientationQuaternionsFromDevice(float4_input.data(), 0, false); + float3_input.FromHost(prescribed_velocities); + tracker->SetVelocitiesFromDevice(float3_input.data(), 0, false); + float3_input.FromHost(prescribed_global_angular_velocities); + tracker->SetAngularVelocitiesGlobalFromDevice(float3_input.data(), 0, false); + + solver.GetOwnerPositionToDevice(float3_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float3_output.ToHost(), prescribed_positions, "device-input positions")) + return false; + solver.GetOwnerOriQToDevice(float4_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float4_output.ToHost(), prescribed_orientations, "device-input orientations")) + return false; + solver.GetOwnerVelocityToDevice(float3_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float3_output.ToHost(), prescribed_velocities, "device-input velocities")) + return false; + solver.GetOwnerAngVelGlobalToDevice(float3_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float3_output.ToHost(), prescribed_global_angular_velocities, + "device-input global angular velocities")) + return false; + + // The local-frame flavor has the same physical interpretation as the original host SetOwnerAngVel API. + const std::vector prescribed_local_angular_velocities = { + make_float3(1.f, 2.f, 3.f), make_float3(-4.f, 5.f, -6.f), make_float3(0.25f, 0.5f, 0.75f)}; + float3_input.FromHost(prescribed_local_angular_velocities); + tracker->SetAngularVelocitiesFromDevice(float3_input.data(), 0, false); + solver.GetOwnerAngVelLocalToDevice(float3_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float3_output.ToHost(), prescribed_local_angular_velocities, + "device-input local angular velocities")) + return false; + + // Host and device orientation setters both normalize valid quaternions before storing them. + solver.SetOwnerOriQ(tracker->GetOwnerID(), unnormalized_orientations); + if (!compareVectors(tracker->OrientationQuaternions(), prescribed_orientations, "host-normalized orientations")) + return false; + + // The common one-owner call uses the default count while retaining explicit pointer-device information. + float3_input.FromHost({prescribed_velocities.front()}); + solver.SetOwnerVelocityFromDevice(tracker->GetOwnerID(), float3_input.data(), 0); + solver.GetOwnerVelocityToDevice(float3_output.data(), count, 0, tracker->GetOwnerID()); + if (!compareVectors(float3_output.ToHost(1), {prescribed_velocities.front()}, "default one-owner exchange")) + return false; + + tracker->PositionsToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->Positions(), "positions")) + return false; + tracker->VelocitiesToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->Velocities(), "velocities")) + return false; + tracker->AngularVelocitiesLocalToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->AngularVelocitiesLocal(), "local angular velocities")) + return false; + tracker->AngularVelocitiesGlobalToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->AngularVelocitiesGlobal(), "global angular velocities")) + return false; + tracker->OrientationQuaternionsToDevice(float4_output.data(), count, 0, false); + if (!compareVectors(float4_output.ToHost(), tracker->OrientationQuaternions(), "orientations")) + return false; + tracker->ContactAccelerationsToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->ContactAccelerations(), "contact accelerations")) + return false; + tracker->ContactAngularAccelerationsLocalToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->ContactAngularAccelerationsLocal(), + "local contact angular accelerations")) + return false; + tracker->ContactAngularAccelerationsGlobalToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->ContactAngularAccelerationsGlobal(), + "global contact angular accelerations")) + return false; + tracker->FamiliesToDevice(uint_output.data(), count, 0, false); + if (!compareVectors(uint_output.ToHost(), tracker->GetFamilies(), "families")) + return false; + tracker->MassesToDevice(float_output.data(), count, 0, false); + if (!compareVectors(float_output.ToHost(), tracker->Masses(), "masses")) + return false; + tracker->MOIsToDevice(float3_output.data(), count, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->MOIs(), "moments of inertia")) + return false; + tracker->OwnerWildcardValuesToDevice("retrieval_tag", float_output.data(), count, 0, false); + if (!compareVectors(float_output.ToHost(), tracker->GetOwnerWildcardValues("retrieval_tag"), "owner wildcards")) + return false; + + TestDeviceBuffer range_output(2, 0); + solver.GetOwnerVelocityToDevice(range_output.data(), 2, 0, tracker->GetOwnerID(1), 2); + const auto host_velocities = tracker->Velocities(); + if (!compareVectors(range_output.ToHost(), std::vector(host_velocities.begin() + 1, host_velocities.end()), + "nonzero owner range")) + return false; + + // Repeated output exercises reusable scratch/staging ownership and ensures no query leaves a claimed temp buffer. + for (int iteration = 0; iteration < 3; iteration++) { + tracker->PositionsToDevice(float3_output.data(), count, 0); + } + + // Disabling validation trusts caller-provided metadata. The allocation really has `count` elements, so declaring + // zero capacity here safely verifies that the optional capacity check is bypassed. + tracker->PositionsToDevice(float3_output.data(), 0, 0, false); + if (!compareVectors(float3_output.ToHost(), tracker->Positions(), "unchecked positions")) + return false; + + // These calls intentionally throw. Suppress their expected logger output so a passing test does not look broken. + solver.SetVerbosity("QUIET"); + bool capacity_rejected = false; + try { + tracker->PositionsToDevice(float3_output.data(), count - 1, 0); + } catch (const SolverException&) { + capacity_rejected = true; + } + if (!capacity_rejected) { + std::cerr << "FAIL: insufficient fixed-output capacity was not rejected." << std::endl; + return false; + } + + bool host_pointer_rejected = false; + std::vector host_only_output(count); + try { + tracker->PositionsToDevice(host_only_output.data(), count, 0); + } catch (const SolverException&) { + host_pointer_rejected = true; + } + if (!host_pointer_rejected) { + std::cerr << "FAIL: ordinary host memory was accepted as CUDA device output." << std::endl; + return false; + } + + bool null_input_rejected = false; + try { + solver.SetOwnerPositionFromDevice(tracker->GetOwnerID(), nullptr, 0); + } catch (const SolverException&) { + null_input_rejected = true; + } + if (!null_input_rejected) { + std::cerr << "FAIL: null CUDA owner-state input was not rejected." << std::endl; + return false; + } + + bool zero_orientation_rejected = false; + try { + float4_input.FromHost({make_float4(0.f)}); + solver.SetOwnerOriQFromDevice(tracker->GetOwnerID(), float4_input.data(), 0); + } catch (const SolverException&) { + zero_orientation_rejected = true; + } + if (!zero_orientation_rejected) { + std::cerr << "FAIL: zero-length CUDA owner orientation was not rejected." << std::endl; + return false; + } + + bool input_range_rejected = false; + try { + solver.SetOwnerVelocityFromDevice(tracker->GetOwnerID(2), float3_input.data(), 0, 2); + } catch (const SolverException&) { + input_range_rejected = true; + } + if (!input_range_rejected) { + std::cerr << "FAIL: out-of-range CUDA owner-state input was not rejected." << std::endl; + return false; + } + + if (visible_devices >= 2) { + TestDeviceBuffer peer_output(count, 1); + tracker->PositionsToDevice(peer_output.data(), count, 1); + if (!compareVectors(peer_output.ToHost(), tracker->Positions(), "cross-device positions")) + return false; + + bool wrong_device_rejected = false; + try { + tracker->PositionsToDevice(peer_output.data(), count, 0); + } catch (const SolverException&) { + wrong_device_rejected = true; + } + if (!wrong_device_rejected) { + std::cerr << "FAIL: mismatched pointer device declaration was not rejected." << std::endl; + return false; + } + + const std::vector remote_velocities = {make_float3(3, -2, 1), make_float3(-4, 5, -6), + make_float3(7, 8, -9)}; + TestDeviceBuffer peer_input(count, 1); + peer_input.FromHost(remote_velocities); + solver.SetOwnerVelocityFromDevice(tracker->GetOwnerID(), peer_input.data(), 1, count); + solver.GetOwnerVelocityToDevice(float3_output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(float3_output.ToHost(), remote_velocities, "cross-device velocity input")) + return false; + + bool wrong_source_device_rejected = false; + try { + solver.SetOwnerVelocityFromDevice(tracker->GetOwnerID(), peer_input.data(), 0, count); + } catch (const SolverException&) { + wrong_source_device_rejected = true; + } + if (!wrong_source_device_rejected) { + std::cerr << "FAIL: mismatched CUDA owner-state source-device declaration was not rejected." << std::endl; + return false; + } + } + solver.SetVerbosity("ERROR"); + + // Refresh contacts and forces only after all prescribed state is in place. Besides producing a coherent wrench + // snapshot, this lets the fixed-field exchange checks remain independent of contact-detection setup. + solver.RequestContactUpdate(); + solver.DoDynamicsThenSync(1e-5); + const size_t contact_capacity = solver.GetNumContacts(); + if (contact_capacity == 0) { + std::cerr << "FAIL: contact-force retrieval scenario produced no contacts." << std::endl; + return false; + } + TestDeviceBuffer contact_points(contact_capacity, 0); + TestDeviceBuffer contact_forces(contact_capacity, 0); + TestDeviceBuffer contact_torques(contact_capacity, 0); + + std::vector host_points; + std::vector host_forces; + std::vector host_torques; + const size_t host_count = tracker->GetContactForcesAndGlobalTorqueForAll(host_points, host_forces, host_torques); + const size_t device_count = tracker->GetContactForcesAndGlobalTorqueForAllToDevice( + contact_points.data(), contact_forces.data(), contact_torques.data(), contact_capacity, 0); + if (device_count != host_count || + !compareVectors(contact_points.ToHost(device_count), host_points, "contact points") || + !compareVectors(contact_forces.ToHost(device_count), host_forces, "contact forces") || + !compareVectors(contact_torques.ToHost(device_count), host_torques, "global contact torques")) { + return false; + } + + const size_t force_only_count = + tracker->GetContactForcesForAllToDevice(contact_points.data(), contact_forces.data(), contact_capacity, 0); + if (force_only_count != host_count || + !compareVectors(contact_forces.ToHost(force_only_count), host_forces, "force-only contact retrieval")) { + return false; + } + + const size_t local_count = tracker->GetContactForcesAndLocalTorqueForAll(host_points, host_forces, host_torques); + const size_t local_device_count = tracker->GetContactForcesAndLocalTorqueForAllToDevice( + contact_points.data(), contact_forces.data(), contact_torques.data(), contact_capacity, 0); + if (local_device_count != local_count || + !compareVectors(contact_torques.ToHost(local_device_count), host_torques, "local contact torques")) { + return false; + } + + // Independently reconstruct each owner's resultant from the existing per-contact API. + TestDeviceBuffer wrench_forces(count, 0); + TestDeviceBuffer wrench_torques(count, 0); + tracker->ContactWrenchesToDevice(wrench_forces.data(), wrench_torques.data(), count, 0); + std::vector reference_forces(count, make_float3(0)); + std::vector reference_torques(count, make_float3(0)); + bool observed_extra_torque = false; + const auto owner_positions = tracker->Positions(); + for (size_t owner = 0; owner < count; owner++) { + std::vector owner_points; + std::vector owner_forces; + std::vector owner_extra_torques; + tracker->GetContactForcesAndGlobalTorque(owner_points, owner_forces, owner_extra_torques, owner); + for (size_t contact = 0; contact < owner_forces.size(); contact++) { + observed_extra_torque = observed_extra_torque || length(owner_extra_torques[contact]) > kTolerance; + reference_forces[owner] += owner_forces[contact]; + reference_torques[owner] += cross(owner_points[contact] - owner_positions[owner], owner_forces[contact]) + + owner_extra_torques[contact]; + } + } + if (!observed_extra_torque) { + std::cerr << "FAIL: wrench reference scenario did not exercise force-model extra torque." << std::endl; + return false; + } + if (!compareWrenchVectors(wrench_forces.ToHost(), reference_forces, "reduced owner contact forces") || + !compareWrenchVectors(wrench_torques.ToHost(), reference_torques, "reduced owner contact torques")) { + return false; + } + std::vector host_wrench_forces; + std::vector host_wrench_torques; + tracker->ContactWrenches(host_wrench_forces, host_wrench_torques); + if (!compareWrenchVectors(host_wrench_forces, wrench_forces.ToHost(), "host/device owner contact forces") || + !compareWrenchVectors(host_wrench_torques, wrench_torques.ToHost(), "host/device owner contact torques")) { + return false; + } + std::vector solver_wrench_forces; + std::vector solver_wrench_torques; + solver.GetOwnerContactWrench(solver_wrench_forces, solver_wrench_torques, tracker->GetOwnerID(), count); + if (!compareWrenchVectors(solver_wrench_forces, host_wrench_forces, "solver/tracker owner contact forces") || + !compareWrenchVectors(solver_wrench_torques, host_wrench_torques, "solver/tracker owner contact torques")) { + return false; + } + + if (visible_devices >= 2) { + TestDeviceBuffer peer_wrench_forces(count, 1); + TestDeviceBuffer peer_wrench_torques(count, 1); + tracker->ContactWrenchesToDevice(peer_wrench_forces.data(), peer_wrench_torques.data(), count, 1); + if (!compareWrenchVectors(peer_wrench_forces.ToHost(), reference_forces, + "cross-device reduced owner contact forces") || + !compareWrenchVectors(peer_wrench_torques.ToHost(), reference_torques, + "cross-device reduced owner contact torques")) { + return false; + } + } + + bool wrench_capacity_rejected = false; + solver.SetVerbosity("QUIET"); + try { + tracker->ContactWrenchesToDevice(wrench_forces.data(), wrench_torques.data(), count - 1, 0); + } catch (const SolverException&) { + wrench_capacity_rejected = true; + } + solver.SetVerbosity("ERROR"); + if (!wrench_capacity_rejected) { + std::cerr << "FAIL: insufficient owner-wrench capacity was not rejected." << std::endl; + return false; + } + + return true; +} + +bool testNextStepAccelerationInput() { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(8, 8, 8); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetCDUpdateFreq(1); + solver.SetInitBinNumTarget(10); + + auto material = solver.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.4}, {"Crr", 0.0}}); + auto sphere = solver.LoadSphereType(2.f, 0.25f, material); + sphere->SetMOI(make_float3(0.3f, 0.4f, 0.5f)); + auto batch = solver.AddClumps(sphere, std::vector{make_float3(-2, 0, 0), make_float3(2, 0, 0)}); + auto tracker = solver.Track(batch); + + constexpr float step_size = 1e-4f; + constexpr size_t count = 2; + solver.SetTimeStepSize(step_size); + solver.Initialize(); + + TestDeviceBuffer input(count, 0); + TestDeviceBuffer output(count, 0); + const std::vector first_linear = {make_float3(1, 2, 3), make_float3(-1, -2, -3)}; + const std::vector queued_linear = {make_float3(4, -5, 6), make_float3(-7, 8, -9)}; + const std::vector first_angular = {make_float3(0.5f, 1.f, 1.5f), make_float3(-0.5f, -1.f, -1.5f)}; + const std::vector queued_angular = {make_float3(2, 3, 4), make_float3(-4, -3, -2)}; + + // Exercise both solver and tracker entry points. A later submission replaces the already queued contribution, + // matching the established host methods despite their historical "Add" name. + input.FromHost(first_linear); + solver.AddOwnerNextStepAccFromDevice(tracker->GetOwnerID(), input.data(), 0, count); + input.FromHost(queued_linear); + tracker->AddAccFromDevice(input.data(), 0, false); + solver.GetOwnerAccToDevice(output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(output.ToHost(), queued_linear, "queued device linear acceleration")) + return false; + + input.FromHost(first_angular); + tracker->AddAngAccFromDevice(input.data(), 0); + input.FromHost(queued_angular); + solver.AddOwnerNextStepAngAccFromDevice(tracker->GetOwnerID(), input.data(), 0, count, false); + solver.GetOwnerAngAccLocalToDevice(output.data(), count, 0, tracker->GetOwnerID(), count); + if (!compareVectors(output.ToHost(), queued_angular, "queued device local angular acceleration")) + return false; + + // Zero-count calls must not inspect the pointer or alter the already queued values. + solver.AddOwnerNextStepAccFromDevice(tracker->GetOwnerID(), nullptr, 0, 0); + + solver.SetVerbosity("QUIET"); + bool range_rejected = false; + try { + solver.AddOwnerNextStepAccFromDevice(tracker->GetOwnerID(1), input.data(), 0, count); + } catch (const SolverException&) { + range_rejected = true; + } + solver.SetVerbosity("ERROR"); + if (!range_rejected) { + std::cerr << "FAIL: out-of-range CUDA next-step acceleration input was not rejected." << std::endl; + return false; + } + + solver.DoDynamicsThenSync(step_size); + std::vector expected_velocity(count); + std::vector expected_angular_velocity(count); + for (size_t i = 0; i < count; i++) { + expected_velocity[i] = queued_linear[i] * step_size; + expected_angular_velocity[i] = queued_angular[i] * step_size; + } + if (!compareVectors(tracker->Velocities(), expected_velocity, "next-step linear velocity response") || + !compareVectors(tracker->AngularVelocitiesLocal(), expected_angular_velocity, + "next-step angular velocity response")) { + return false; + } + + // The preservation flags are consumed by the first force preparation, so a second step must not apply the same + // acceleration again when there is no contact, gravity, or family acceleration prescription. + solver.DoDynamicsThenSync(step_size); + return compareVectors(tracker->Velocities(), expected_velocity, "one-shot linear acceleration") && + compareVectors(tracker->AngularVelocitiesLocal(), expected_angular_velocity, + "one-shot angular acceleration"); +} + +bool testNonJitifiedMassData() { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.DisableJitifyMassProperties(); + solver.InstructBoxDomainDimension(4, 4, 4); + auto material = solver.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.4}, {"Crr", 0.0}}); + auto sphere = solver.LoadSphereType(3.25f, 0.4f, material); + auto batch = solver.AddClumps(sphere, std::vector{make_float3(1, 1, 1), make_float3(3, 1, 1)}); + auto tracker = solver.Track(batch); + solver.SetTimeStepSize(1e-5); + solver.Initialize(); + + TestDeviceBuffer masses(2, 0); + TestDeviceBuffer moments(2, 0); + tracker->MassesToDevice(masses.data(), 2, 0); + tracker->MOIsToDevice(moments.data(), 2, 0); + return compareVectors(masses.ToHost(), tracker->Masses(), "non-jitified masses") && + compareVectors(moments.ToHost(), tracker->MOIs(), "non-jitified moments of inertia"); +} + +bool testExternallyMovedFixedMeshes() { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + // This scenario uses positive coordinates through x = 6. Use explicit bounds instead of the centered [-4, 4] + // domain produced by the dimension-only overload, since positions outside the domain cannot be voxel-encoded. + solver.InstructBoxDomainDimension({0.f, 8.f}, {0.f, 8.f}, {0.f, 8.f}); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetMeshUniversalContact(true); + solver.SetCDUpdateFreq(1); + solver.SetInitBinNumTarget(10); + solver.SetErrorOutAvgContacts(10000); + solver.SetErrorOutVelocity(1e6f); + + auto material = solver.LoadMaterial({{"E", 1e7}, {"nu", 0.3}, {"CoR", 0.2}, {"mu", 0.4}, {"Crr", 0.0}}); + std::vector> meshes; + const auto cube_path = (GET_DATA_PATH() / "mesh/cube.obj").string(); + for (unsigned int i = 0; i < 3; i++) { + auto mesh = solver.AddWavefrontMeshObject(cube_path, material); + mesh->SetMass(1.f); + mesh->SetMOI(make_float3(1.f)); + mesh->SetInitPos(make_float3(2.f + 2.f * i, 2.f, 2.f)); + mesh->SetFamily(20 + i); + solver.SetFamilyFixed(20 + i); + meshes.push_back(mesh); + } + auto first_tracker = solver.Track(meshes.front()); + + constexpr size_t owner_count = 3; + solver.SetTimeStepSize(1e-5); + solver.Initialize(); + const bodyID_t first_owner = first_tracker->GetOwnerID(); + TestDeviceBuffer device_input(owner_count, 0); + TestDeviceBuffer device_positions(owner_count, 0); + TestDeviceBuffer device_velocities(owner_count, 0); + TestDeviceBuffer device_forces(owner_count, 0); + TestDeviceBuffer device_torques(owner_count, 0); + std::vector prescribed_positions(owner_count); + const std::vector moving_owner_x = {3.02f, 3.01f, 3.00f, 2.99f}; + for (float x : moving_owner_x) { + prescribed_positions = {make_float3(2.f, 2.f, 2.f), make_float3(x, 2.f, 2.f), make_float3(6.f, 2.f, 2.f)}; + device_input.FromHost(prescribed_positions); + solver.SetOwnerPositionFromDevice(first_owner, device_input.data(), 0, owner_count); + const std::vector prescribed_velocities = {make_float3(0), make_float3(-1000.f, 0, 0), make_float3(0)}; + device_velocities.FromHost(prescribed_velocities); + solver.SetOwnerVelocityFromDevice(first_owner, device_velocities.data(), 0, owner_count); + // Owner setters match their host counterparts and do not implicitly alter dT/kT scheduling policy. + solver.RequestContactUpdate(); + solver.DoDynamicsThenSync(1e-5); + + // A fixed family must retain the just-prescribed pose rather than dynamically integrating its supplied speed. + solver.GetOwnerPositionToDevice(device_positions.data(), owner_count, 0, first_owner, owner_count); + if (!comparePositionVectors(device_positions.ToHost(), prescribed_positions, + "fixed-mesh prescribed positions")) { + return false; + } + solver.GetOwnerContactWrenchToDevice(device_forces.data(), device_torques.data(), owner_count, 0, first_owner, + owner_count); + const auto step_forces = device_forces.ToHost(); + if (x > 3.005f && (length(step_forces[0]) > kTolerance || length(step_forces[1]) > kTolerance)) { + std::cerr << "FAIL: separated externally moved meshes produced a contact reaction." << std::endl; + return false; + } + } + + solver.GetOwnerContactWrenchToDevice(device_forces.data(), device_torques.data(), owner_count, 0, first_owner, + owner_count); + const auto forces = device_forces.ToHost(); + const auto torques = device_torques.ToHost(); + for (size_t i = 0; i < owner_count; i++) { + if (!std::isfinite(forces[i].x) || !std::isfinite(forces[i].y) || !std::isfinite(forces[i].z) || + !std::isfinite(torques[i].x) || !std::isfinite(torques[i].y) || !std::isfinite(torques[i].z)) { + std::cerr << "FAIL: fixed-mesh contact wrench contains a non-finite value." << std::endl; + return false; + } + } + if (length(forces[0]) <= kTolerance || length(forces[1]) <= kTolerance || dot(forces[0], forces[1]) >= 0.f) { + std::cerr << "FAIL: externally overlapped fixed meshes did not produce opposing contact reactions." + << std::endl; + return false; + } + if (length(forces[2]) > kTolerance || length(torques[2]) > kTolerance) { + std::cerr << "FAIL: fixed mesh without contact did not receive a zero wrench." << std::endl; + return false; + } + return true; +} + +} // namespace + +int main() { + int visible_devices = 0; + const cudaError_t status = cudaGetDeviceCount(&visible_devices); + if (status != cudaSuccess) { + std::cerr << "SKIP: CUDA runtime unavailable: " << cudaGetErrorString(status) << std::endl; + return 0; + } + if (visible_devices == 0) { + std::cout << "SKIP: DEMTest_DeviceDataRetrieval requires a visible CUDA device." << std::endl; + return 0; + } + + if (!testFixedOwnerData(visible_devices)) { + return 1; + } + if (!testNextStepAccelerationInput()) { + return 1; + } + if (!testNonJitifiedMassData()) { + return 1; + } + if (!testExternallyMovedFixedMeshes()) { + return 1; + } + std::cout << "PASS: GPU owner-state exchange and contact-wrench retrieval match reference behavior." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_DeviceSelection.cpp b/src/demo/ModularTests/DEMTest_DeviceSelection.cpp new file mode 100644 index 00000000..5461f3c2 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_DeviceSelection.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include + +#include +#include + +#include "DEM/API.h" +#include "core/utils/Logger.hpp" + +using namespace deme; + +// Verify that count-based and explicit device selection produce the documented, deterministic worker placement. +int main() { + int visible_devices = 0; + DEME_GPU_CALL(cudaGetDeviceCount(&visible_devices)); + if (visible_devices < 1) { + std::cout << "SKIP: DEMTest_DeviceSelection requires a visible CUDA device." << std::endl; + return 0; + } + + { + DEMSolver solver(1); + const std::vector devices = solver.GetGPUDeviceIDs(); + if (devices != std::vector({0, 0})) { + std::cerr << "FAIL: DEMSolver(1) did not place both workers on device 0." << std::endl; + return 1; + } + } + + const int selected_device = visible_devices - 1; + { + DEMSolver solver(std::vector{selected_device}); + const std::vector devices = solver.GetGPUDeviceIDs(); + if (devices != std::vector({selected_device, selected_device})) { + std::cerr << "FAIL: single explicit device selection did not place both workers on the selected device." + << std::endl; + return 1; + } + } + + if (visible_devices >= 2) { + DEMSolver solver(std::vector{1, 0}); + const std::vector devices = solver.GetGPUDeviceIDs(); + if (devices != std::vector({1, 0})) { + std::cerr << "FAIL: two-device selection did not preserve dT/kT assignment order." << std::endl; + return 1; + } + } + + std::cout << "PASS: DEMSolver GPU device selection is deterministic." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_MeshBox1Particle.cpp b/src/demo/ModularTests/DEMTest_MeshBox1Particle.cpp new file mode 100644 index 00000000..16ec1d64 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshBox1Particle.cpp @@ -0,0 +1,152 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Drop one 1 mm sphere inside a reverse-wound 50 mm mesh box. This is the C++ +// regression counterpart of the pyDEME 01_mesh_box_1particle_1s.py case. +// ============================================================================= + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +namespace { + +int fail(const char* reason) { + std::cerr << "DEMTest_MeshBox1Particle FAILED: " << reason << std::endl; + return 1; +} + +bool isFinite(const float3& value) { + return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z); +} + +} // namespace + +int main() { + constexpr float mm_to_m = 1.0e-3f; + constexpr float box_size = 0.050f; + constexpr float sphere_radius = 0.001f; + constexpr float sphere_density = 2600.0f; + constexpr float step_size = 1.0e-6f; + constexpr float sample_time = 0.02f; + constexpr float total_time = 1.0f; + constexpr float gravity = 9.81f; + const float3 initial_position = make_float3(0.010f, 0.020f, 0.010f); // make_float3(0.0125f, 0.020f, 0.0125f); + + DEMSolver solver; + solver.SetOutputFormat(OUTPUT_FORMAT::CSV); + solver.SetOutputContent({"ABSV", "XYZ"}); + solver.SetMeshOutputFormat(MESH_FORMAT::VTK); + solver.SetNoForceRecord(); + + // World size + const std::pair mesh_domain = {-0.005, 0.055}; + solver.InstructBoxDomainDimension(mesh_domain, mesh_domain, mesh_domain); + + auto particle_material = + solver.LoadMaterial({{"E", 1.0e8f}, {"nu", 0.30f}, {"CoR", 0.80f}, {"mu", 0.20f}, {"Crr", 0.0f}}); + auto wall_material = + solver.LoadMaterial({{"E", 1.0e8f}, {"nu", 0.30f}, {"CoR", 0.60f}, {"mu", 0.50f}, {"Crr", 0.0f}}); + solver.SetMaterialPropertyPair("mu", particle_material, wall_material, 0.35f); + solver.SetMaterialPropertyPair("CoR", particle_material, wall_material, 0.50f); + + // The reverse winding makes the box's active triangle faces point toward + // the enclosed particle, matching the reference pyDEME mesh. + auto mesh_wall = + solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/box_50mm_reverse_winding.obj").string(), wall_material); + mesh_wall->Scale(mm_to_m); + mesh_wall->SetMass(1.0f); + mesh_wall->SetMOI(make_float3(1.0f, 1.0f, 1.0f)); + mesh_wall->SetFamily(10); + solver.SetFamilyFixed(10); + + const float sphere_mass = + (4.0f / 3.0f) * static_cast(PI) * sphere_radius * sphere_radius * sphere_radius * sphere_density; + auto sphere_type = solver.LoadSphereType(sphere_mass, sphere_radius, particle_material); + auto sphere_batch = solver.AddClumps(sphere_type, initial_position); + auto sphere_tracker = solver.Track(sphere_batch); + + solver.SetTimeStepSize(step_size); + solver.SetGravitationalAcceleration(make_float3(0.0f, -gravity, 0.0f)); + solver.Initialize(); + + // Match the pyDEME driver by reporting solver state and writing sphere and + // mesh snapshots at every sample boundary, including the initial state. + const path output_dir = current_path() / "modular_test_output" / "DEMTest_MeshBox1Particle"; + std::error_code dir_error; + create_directories(output_dir, dir_error); + if (dir_error || !is_directory(output_dir)) { + std::cerr << "Failed to create output directory: " << output_dir << " (" << dir_error.message() << ")" + << std::endl; + return 1; + } + auto max_velocity = solver.CreateInspector("clump_max_absv"); + + bool observed_downward_motion = false; + bool observed_rebound = false; + float max_speed = 0.0f; + const int sample_count = static_cast(total_time / sample_time); + for (int sample = 0; sample < sample_count; sample++) { + const float current_time = static_cast(sample) * sample_time; + std::cout << "Sample " << sample << ": t=" << current_time << " s, max_v=" << max_velocity->GetValue() + << " m/s, avg_contacts=" << solver.GetAvgSphContacts() << ", update_freq=" << solver.GetUpdateFreq() + << std::endl; + + char sphere_filename[64]; + char mesh_filename[64]; + std::snprintf(sphere_filename, sizeof(sphere_filename), "sphere_%04d.csv", sample); + std::snprintf(mesh_filename, sizeof(mesh_filename), "mesh_%04d.vtk", sample); + solver.WriteSphereFile(output_dir / sphere_filename); + solver.WriteMeshFile(output_dir / mesh_filename); + + solver.DoDynamicsThenSync(sample_time); + + const float3 position = sphere_tracker->Pos(); + const float3 velocity = sphere_tracker->Vel(); + if (!isFinite(position) || !isFinite(velocity)) { + return fail("sphere state became non-finite"); + } + const float speed = std::sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z); + max_speed = std::max(max_speed, speed); + observed_downward_motion = observed_downward_motion || velocity.y < -0.1f; + observed_rebound = observed_rebound || (observed_downward_motion && velocity.y > 0.05f); + + // Permit a small contact-scale tolerance while requiring the sphere's + // center to remain inside the closed mesh throughout the run. + constexpr float containment_tolerance = 0.25f * sphere_radius; + if (position.x < sphere_radius - containment_tolerance || + position.x > box_size - sphere_radius + containment_tolerance || + position.y < sphere_radius - containment_tolerance || + position.y > box_size - sphere_radius + containment_tolerance || + position.z < sphere_radius - containment_tolerance || + position.z > box_size - sphere_radius + containment_tolerance) { + return fail("sphere escaped the mesh box"); + } + } + + if (!observed_downward_motion) { + return fail("gravity-driven downward motion was not observed"); + } + if (!observed_rebound) { + return fail("a rebound from the bottom mesh wall was not observed"); + } + + const float expected_first_impact_speed = std::sqrt(2.0f * gravity * (initial_position.y - sphere_radius)); + std::cout << "DEMTest_MeshBox1Particle passed. simulated_time=" << total_time + << " s, max_sampled_speed=" << max_speed + << " m/s, expected_first_impact_speed=" << expected_first_impact_speed << " m/s" << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_MeshPatch.cpp b/src/demo/ModularTests/DEMTest_MeshPatch.cpp new file mode 100644 index 00000000..b9e37999 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshPatch.cpp @@ -0,0 +1,252 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// A demo that tests mesh patch splitting functionality. +// This demo loads a mesh and splits it into convex patches based on angle +// thresholds, demonstrating the mesh patch splitting utility. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + auto fail = [](const std::string& message) { + std::cerr << "FAIL: " << message << std::endl; + return 1; + }; + + std::cout << "========================================" << std::endl; + std::cout << "DEM Mesh Patch Splitting Demo" << std::endl; + std::cout << "========================================" << std::endl; + + // Test with a simple cube mesh + std::cout << "\n--- Test 1: Cube Mesh with Default Patch Info ---" << std::endl; + auto cube_mesh = std::make_shared(); + bool loaded = cube_mesh->LoadWavefrontMesh((GET_DATA_PATH() / "mesh/cube.obj").string()); + + if (loaded) { + std::cout << "Loaded cube mesh successfully" << std::endl; + std::cout << "Number of triangles: " << cube_mesh->GetNumTriangles() << std::endl; + std::cout << "Number of vertices: " << cube_mesh->GetNumNodes() << std::endl; + + // Test default patch info (should be all in patch 0) + std::cout << "\nDefault patch info (assuming convex mesh):" << std::endl; + std::cout << "Number of patches: " << cube_mesh->GetNumPatches() << std::endl; + std::cout << "Patches explicitly set: " << (cube_mesh->ArePatchesExplicitlySet() ? "yes" : "no") << std::endl; + const auto& default_patch_ids = cube_mesh->GetPatchIDs(); + std::cout << "All triangles in patch 0: " + << (std::all_of(default_patch_ids.begin(), default_patch_ids.end(), [](int id) { return id == 0; }) + ? "yes" + : "no") + << std::endl; + + // Test different angle thresholds + std::cout << "\n--- Test 2: Automatic Patch Splitting ---" << std::endl; + float thresholds[] = {10.0f, 45.0f, 90.0f, 300.0f}; + + for (float threshold : thresholds) { + size_t num_patches = cube_mesh->SplitIntoConvexPatches(threshold); + std::cout << "\nAngle threshold: " << std::fixed << std::setprecision(1) << threshold << " degrees" + << std::endl; + std::cout << "Number of patches: " << num_patches << std::endl; + std::cout << "Patches explicitly set: " << (cube_mesh->ArePatchesExplicitlySet() ? "yes" : "no") + << std::endl; + + // Show patch distribution + const auto& patch_ids = cube_mesh->GetPatchIDs(); + + // Count triangles per patch + std::map patch_counts; + for (int patch_id : patch_ids) { + patch_counts[patch_id]++; + } + + std::cout << "Patch distribution:" << std::endl; + for (const auto& entry : patch_counts) { + std::cout << " Patch " << entry.first << ": " << entry.second << " triangles" << std::endl; + } + if (threshold > 180.0f && num_patches != 1) { + return fail( + "high-threshold vanilla SplitIntoConvexPatches should merge the connected cube mesh into one " + "patch"); + } + } + + // Test the extended patch-split API added + std::cout << "\n--- Test 3: Extended Patch Split Options and Quality Report ---" << std::endl; + auto legacy_merge_mesh = std::make_shared(); + auto extended_mesh = std::make_shared(); + if (!legacy_merge_mesh->LoadWavefrontMesh((GET_DATA_PATH() / "mesh/cube.obj").string()) || + !extended_mesh->LoadWavefrontMesh((GET_DATA_PATH() / "mesh/cube.obj").string())) { + return fail("failed to reload cube mesh for extended patch split test"); + } + + const unsigned int legacy_patches = legacy_merge_mesh->SplitIntoConvexPatches(180.0f); + DEMMesh::PatchSplitOptions split_opt; + split_opt.patch_normal_max_deg = 30.0f; + split_opt.patch_min = 2; + split_opt.patch_max = 4; + split_opt.block_concave_edges = true; + DEMMesh::PatchQualityOptions quality_opt; + DEMMesh::PatchQualityReport report; + const unsigned int extended_patches = + extended_mesh->SplitIntoConvexPatches(180.0f, split_opt, &report, quality_opt); + + std::cout << "Legacy 180-degree split patches: " << legacy_patches << std::endl; + std::cout << "Extended split patches: " << extended_patches << std::endl; + std::cout << "Report status: " << static_cast(report.constraint_status) + << ", overall quality: " << static_cast(report.overall) << std::endl; + + if (legacy_patches != 1) { + return fail("legacy 180-degree split should merge the connected cube mesh into one patch"); + } + if (extended_patches <= legacy_patches) { + return fail("patch_normal_max_deg option did not split the cube more strictly than the legacy path"); + } + if (extended_patches != extended_mesh->GetNumPatches() || report.achieved_patches != extended_patches) { + return fail("extended patch count disagrees with the mesh or quality report"); + } + if (!extended_mesh->ArePatchesExplicitlySet()) { + return fail("extended split did not mark patch IDs as explicitly set"); + } + if (report.requested_min != split_opt.patch_min || report.requested_max != split_opt.patch_max) { + return fail("quality report did not preserve requested patch-count bounds"); + } + if (report.per_patch.size() != extended_patches) { + return fail("quality report per-patch array size does not match achieved patch count"); + } + + unsigned int reported_tris = 0; + std::vector counted_tris(extended_patches, 0); + for (const auto& patch_report : report.per_patch) { + reported_tris += patch_report.n_tris; + } + for (patchID_t patch_id : extended_mesh->GetPatchIDs()) { + if (patch_id < 0 || patch_id >= static_cast(extended_patches)) { + return fail("extended split produced an out-of-range patch ID"); + } + counted_tris[patch_id]++; + } + if (reported_tris != extended_mesh->GetNumTriangles()) { + return fail("quality report does not account for every triangle"); + } + for (unsigned int p = 0; p < extended_patches; ++p) { + if (report.per_patch[p].n_tris != counted_tris[p]) { + return fail("quality report per-patch triangle count disagrees with patch IDs"); + } + } + + DEMMesh::PatchConstraintStatus expected_status = DEMMesh::PatchConstraintStatus::SATISFIED; + if (extended_patches > split_opt.patch_max) { + expected_status = DEMMesh::PatchConstraintStatus::TOO_MANY_UNMERGEABLE; + } else if (extended_patches < split_opt.patch_min) { + expected_status = DEMMesh::PatchConstraintStatus::TOO_FEW_UNSPLITTABLE; + } + if (report.constraint_status != expected_status) { + return fail("quality report constraint status does not match achieved patch count"); + } + + // Test manual patch ID setting + std::cout << "\n--- Test 4: Manual Patch ID Setting ---" << std::endl; + size_t num_tris = cube_mesh->GetNumTriangles(); + std::vector manual_patches(num_tris); + // Split triangles into 3 patches based on index + for (size_t i = 0; i < num_tris; ++i) { + manual_patches[i] = i % 3; // Assign patches 0, 1, 2 cyclically + } + + cube_mesh->SetPatchIDs(manual_patches); + std::cout << "Manually set patch IDs (cycling 0, 1, 2)" << std::endl; + std::cout << "Number of patches: " << cube_mesh->GetNumPatches() << std::endl; + std::cout << "Patches explicitly set: " << (cube_mesh->ArePatchesExplicitlySet() ? "yes" : "no") << std::endl; + + // Count triangles per patch + const auto& manual_patch_ids = cube_mesh->GetPatchIDs(); + std::map manual_patch_counts; + for (int patch_id : manual_patch_ids) { + manual_patch_counts[patch_id]++; + } + std::cout << "Manual patch distribution:" << std::endl; + for (const auto& entry : manual_patch_counts) { + std::cout << " Patch " << entry.first << ": " << entry.second << " triangles" << std::endl; + } + + // Both convenience names must assign one consecutive patch ID to every triangle. + cube_mesh->SetEachTriangleAsPatch(); + if (cube_mesh->GetNumPatches() != num_tris || cube_mesh->GetPatchIDs().size() != num_tris) { + return fail("SetEachTriangleAsPatch did not create one patch per triangle"); + } + for (size_t triangle = 0; triangle < num_tris; triangle++) { + if (cube_mesh->GetPatchIDs()[triangle] != static_cast(triangle)) { + return fail("SetEachTriangleAsPatch did not assign consecutive patch IDs"); + } + } + + } else { + std::cout << "Failed to load cube mesh" << std::endl; + } + + // Test with sphere mesh if available + std::cout << "\n--- Test 5: Sphere Mesh ---" << std::endl; + auto sphere_mesh = std::make_shared(); + loaded = sphere_mesh->LoadWavefrontMesh((GET_DATA_PATH() / "mesh/sphere.obj").string()); + + if (loaded) { + std::cout << "Loaded sphere mesh successfully" << std::endl; + std::cout << "Number of triangles: " << sphere_mesh->GetNumTriangles() << std::endl; + std::cout << "Number of vertices: " << sphere_mesh->GetNumNodes() << std::endl; + + // Test with 30 degree threshold + size_t num_patches = sphere_mesh->SplitIntoConvexPatches(30.0f); + std::cout << "Split into " << num_patches << " patches (threshold: 30 degrees)" << std::endl; + + if (sphere_mesh->ArePatchesExplicitlySet()) { + const auto& patch_ids = sphere_mesh->GetPatchIDs(); + + // Count triangles per patch + std::map patch_counts; + for (int patch_id : patch_ids) { + patch_counts[patch_id]++; + } + + std::cout << "Number of patches with different sizes:" << std::endl; + std::map size_distribution; + for (const auto& entry : patch_counts) { + size_distribution[entry.second]++; + } + for (const auto& entry : size_distribution) { + std::cout << " " << entry.second << " patches with " << entry.first << " triangles each" << std::endl; + } + } + } else { + std::cout << "Sphere mesh not available, skipping" << std::endl; + } + + // Test edge case: empty mesh + std::cout << "\n--- Test 6: Empty Mesh ---" << std::endl; + auto empty_mesh = std::make_shared(); + std::cout << "Empty mesh default patches: " << empty_mesh->GetNumPatches() << " (expected: 1)" << std::endl; + std::cout << "Patches explicitly set: " << (empty_mesh->ArePatchesExplicitlySet() ? "yes" : "no") + << " (expected: no)" << std::endl; + + std::cout << "\n========================================" << std::endl; + std::cout << "Demo completed successfully!" << std::endl; + std::cout << "========================================" << std::endl; + + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_MeshSlidingOnSlope.cpp b/src/demo/ModularTests/DEMTest_MeshSlidingOnSlope.cpp new file mode 100644 index 00000000..5e5f37de --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshSlidingOnSlope.cpp @@ -0,0 +1,242 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Mesh planar sliding test: a mesh cube slides on a meshed slope under gravity. +// Similar to SphereRollUpIncline in DEMdemo_TestPack, but both the sliding body +// and the slope are mesh objects (cube.obj and thin_plate.obj). +// We inspect the time evolution of the contact force magnitude and direction +// to verify stability of the mesh-mesh contact during sliding. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + // Slope angle from horizontal (degrees). With mu=0.3 < tan(30deg)~0.577, + // the cube will slide rather than stay stationary. + const float slope_angle_deg = 30.0f; + const float slope_angle_rad = slope_angle_deg * PI / 180.0f; + + // Material properties + const float mu = 0.3f; // friction coefficient + const float CoR = 0.3f; // coefficient of restitution + const float E = 1e8f; // Young's modulus (Pa) + const float nu = 0.3f; // Poisson's ratio + + // Cube geometry and mass (cube.obj is a unit cube, side = 1m before scale) + const float cube_scale = 0.3f; // scale to 0.3 m per side + const float cube_density = 2600.0f; // kg/m^3 (rock-like) + const float cube_mass = cube_density * cube_scale * cube_scale * cube_scale; + // MOI of a solid cube: I = m*a^2/6 per axis + const float cube_moi_val = cube_mass * cube_scale * cube_scale / 6.0f; + const float3 cube_moi = make_float3(cube_moi_val, cube_moi_val, cube_moi_val); + + // Simulation time settings + const float step_size = 5e-6f; + const float frame_time = 0.05f; + const int n_sub_samples = 250; // sub-samples per frame for force averaging + const float sub_dt = frame_time / static_cast(n_sub_samples); // sub-step size + const float total_time = 1.f; + + std::cout << "=====================================================" << std::endl; + std::cout << "DEM Mesh Planar Sliding Test" << std::endl; + std::cout << "=====================================================" << std::endl; + std::cout << "Slope angle: " << slope_angle_deg << " deg" << std::endl; + std::cout << "Friction coefficient: " << mu << std::endl; + std::cout << "Cube side: " << cube_scale << " m" << std::endl; + std::cout << "Cube mass: " << cube_mass << " kg" << std::endl; + + const float g = 9.81f; + const float Fn_expected = cube_mass * g * std::cos(slope_angle_rad); + const float Ft_expected = mu * Fn_expected; + const float F_total_expected = std::sqrt(Fn_expected * Fn_expected + Ft_expected * Ft_expected); + std::cout << "Expected normal force (steady slide): " << Fn_expected << " N" << std::endl; + std::cout << "Expected friction force: " << Ft_expected << " N" << std::endl; + std::cout << "Expected total contact force: " << F_total_expected << " N" << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(5, 5, 5); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -g)); + // Enable mesh-mesh contacts (required for cube-slope mesh contact) + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + // DEMSim.SetSimplePatchCombination(true); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.0f}}); + + // ========================================================================= + // Slope mesh: thin_plate.obj (fixed) + // thin_plate has its large flat face normal in the +x direction. + // We rotate it so the face normal aligns with the slope normal: + // slope_normal = (-sin(alpha), 0, cos(alpha)) + // This requires a rotation about the y-axis by -(90 + alpha) = -120 deg. + // Verification: R_y(-120 deg) * (1,0,0) = (-0.5, 0, 0.866) = (-sin30, 0, cos30). OK. + // ========================================================================= + float4 slope_q = make_float4(0.f, 0.f, 0.f, 1.f); + slope_q = RotateQuat(slope_q, make_float3(0.f, 1.f, 0.f), static_cast(-(PI / 2.0f + slope_angle_rad))); + + auto slope = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/thin_plate.obj").string(), mat); + // thin_plate CoM is at (0,0,0) per its construction; set reasonable mass/MOI. + slope->SetMass(1000.0f); + slope->SetMOI(make_float3(100.f, 100.f, 200.f)); + slope->SetInitQuat(slope_q); + slope->SetInitPos(make_float3(0.f, 0.f, 0.f)); + slope->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + // ========================================================================= + // Sliding box: cube.obj (free to move under gravity) + // cube.obj is a unit cube centered at origin. After Scale(cube_scale), it + // is a cube_scale-side cube. + // Initial position: on the slope surface, near the top, slightly lifted off. + // Slope normal: n = (-sin(alpha), 0, cos(alpha)) + // Uphill dir: u = (cos(alpha), 0, sin(alpha)) + // Cube center = slope_center + uphill*0.5 + n*(half_cube + gap) + // ========================================================================= + const float3 slope_normal = make_float3(-std::sin(slope_angle_rad), 0.f, std::cos(slope_angle_rad)); + const float3 uphill_dir = make_float3(std::cos(slope_angle_rad), 0.f, std::sin(slope_angle_rad)); + const float half_cube = cube_scale / 2.0f; + const float initial_gap = 0.025f; + + float3 cube_init_pos = uphill_dir * 0.5f + slope_normal * (half_cube + initial_gap); + + // Orient cube so the face resting on slope is parallel to slope surface. + // We rotate the cube by -alpha about y, so the cube's local z-axis + // aligns with the slope normal: R_y(-alpha) * (0,0,1) = (-sin(alpha),0,cos(alpha)). OK. + float4 cube_q = make_float4(0.f, 0.f, 0.f, 1.f); + cube_q = RotateQuat(cube_q, make_float3(0.f, 1.f, 0.f), -slope_angle_rad); + + auto cube = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat); + cube->Scale(cube_scale); + cube->SetMass(cube_mass); + cube->SetMOI(cube_moi); + cube->SetInitPos(cube_init_pos); + cube->SetInitQuat(cube_q); + cube->SetFamily(0); + auto cube_tracker = DEMSim.Track(cube); + + // ========================================================================= + // Initialize and run + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.Initialize(); + + std::cout << "\nCube init position: (" << cube_init_pos.x << ", " << cube_init_pos.y << ", " << cube_init_pos.z + << ")" << std::endl; + std::cout << "Running simulation for " << total_time << " s ..." << std::endl; + std::cout << "----------------------------------------------------" << std::endl; + + path out_dir = current_path() / "modular_test_output" / "DEMTest_MeshSlidingOnSlope"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + if (dir_ec || !is_directory(out_dir)) { + std::cerr << "Failed to create output directory: " << out_dir << " (" << dir_ec.message() << ")" << std::endl; + return 1; + } + + std::vector force_mags; + const int n_frames = static_cast(total_time / frame_time); + + for (int i = 1; i <= n_frames; i++) { + // Output mesh snapshot at the start of this interval + char meshfilename[200]; + sprintf(meshfilename, "DEMTest_mesh_%04d.vtk", i); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + // Advance simulation in sub-steps and accumulate contact force readings + // to compute the average force over the interval [t_prev, t_center+half_window]. + double sum_fx = 0.0, sum_fy = 0.0, sum_fz = 0.0; + for (int s = 0; s < n_sub_samples; s++) { + DEMSim.DoDynamics(sub_dt); + float3 cnt_acc = cube_tracker->ContactAcc(); + sum_fx += cnt_acc.x; + sum_fy += cnt_acc.y; + sum_fz += cnt_acc.z; + } + + // Average contact force = (average contact acceleration) * mass + float3 avg_cnt_force; + avg_cnt_force.x = static_cast(sum_fx / n_sub_samples) * cube_mass; + avg_cnt_force.y = static_cast(sum_fy / n_sub_samples) * cube_mass; + avg_cnt_force.z = static_cast(sum_fz / n_sub_samples) * cube_mass; + double force_mag = + std::sqrt((double)avg_cnt_force.x * avg_cnt_force.x + (double)avg_cnt_force.y * avg_cnt_force.y + + (double)avg_cnt_force.z * avg_cnt_force.z); + force_mags.push_back(force_mag); + + float3 pos = cube_tracker->Pos(); + float3 vel = cube_tracker->Vel(); + double vel_mag = std::sqrt((double)vel.x * vel.x + (double)vel.y * vel.y + (double)vel.z * vel.z); + + std::cout << "t=" << i * frame_time << "s (avg over " << n_sub_samples << " sub-steps)" + << " pos=(" << pos.x << "," << pos.y << "," << pos.z << ")" + << " |v|=" << vel_mag << " m/s" + << " |F_cnt_avg|=" << force_mag << " N"; + + if (force_mag > 1.0) { + float inv_f = static_cast(1.0 / force_mag); + std::cout << " F_dir=(" << avg_cnt_force.x * inv_f << "," << avg_cnt_force.y * inv_f << "," + << avg_cnt_force.z * inv_f << ")"; + } + std::cout << std::endl; + } + + // ========================================================================= + // Statistics over the contact force time series + // ========================================================================= + if (force_mags.empty()) { + std::cout << "\nNo force data collected." << std::endl; + return 0; + } + double sum_f = 0.0, min_f = force_mags[0], max_f = force_mags[0]; + for (double f : force_mags) { + sum_f += f; + if (f < min_f) + min_f = f; + if (f > max_f) + max_f = f; + } + double mean_f = sum_f / static_cast(force_mags.size()); + double var = 0.0; + for (double f : force_mags) { + double d = f - mean_f; + var += d * d; + } + double stddev_f = std::sqrt(var / static_cast(force_mags.size())); + + std::cout << "\n=== Contact Force Statistics (over " << force_mags.size() << " frames, each averaged over " + << n_sub_samples << " sub-steps) ===" << std::endl; + std::cout << " Mean: " << mean_f << " N" << std::endl; + std::cout << " Min: " << min_f << " N" << std::endl; + std::cout << " Max: " << max_f << " N" << std::endl; + std::cout << " StdDev: " << stddev_f << " N" << std::endl; + std::cout << " Expected total at steady slide: " << F_total_expected << " N" << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_MeshSlidingOnSlope exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_MeshTemplate.cpp b/src/demo/ModularTests/DEMTest_MeshTemplate.cpp new file mode 100644 index 00000000..a4f16cff --- /dev/null +++ b/src/demo/ModularTests/DEMTest_MeshTemplate.cpp @@ -0,0 +1,145 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// A demo that demonstrates the new mesh template functionality. +// This demo shows: +// 1. Loading a mesh as a template (not immediately in simulation) +// 2. Instantiating multiple mesh instances from the template at different locations +// 3. Duplicating an existing mesh object +// 4. Running a simple simulation with template-based meshes +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include + +using namespace deme; +using namespace std::filesystem; + +int main() { + std::cout << "========================================" << std::endl; + std::cout << "DEM Mesh Template Demo" << std::endl; + std::cout << "========================================" << std::endl; + + DEMSolver DEMSim; + DEMSim.SetVerbosity("INFO"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.SetContactOutputContent({"OWNER", "FORCE", "POINT"}); + DEMSim.InstructBoxDomainDimension(10, 10, 10); + // Enable mesh-mesh contacts + DEMSim.SetMeshUniversalContact(true); + + // Load a material + auto mat_type = DEMSim.LoadMaterial({{"E", 1e9}, {"nu", 0.3}, {"CoR", 0.6}, {"mu", 0.5}}); + + // Add a bottom plane + DEMSim.AddBCPlane(make_float3(0, 0, -4), make_float3(0, 0, 1), mat_type); + + std::cout << "\n--- Test 1: Load Mesh Template ---" << std::endl; + // Load a mesh as a template (not yet in simulation) + auto mesh_template = DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type, + true, // load_normals + false // load_uv + ); + + if (mesh_template) { + std::cout << "Successfully loaded mesh template" << std::endl; + std::cout << "Number of triangles: " << mesh_template->GetNumTriangles() << std::endl; + std::cout << "Number of vertices: " << mesh_template->GetNumNodes() << std::endl; + } else { + std::cout << "Failed to load mesh template" << std::endl; + return 1; + } + + std::cout << "\n--- Test 2: Instantiate Meshes from Template ---" << std::endl; + // Create multiple instances of the mesh at different locations + auto mesh1 = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(-1.5, 0, 0)); + mesh1->SetFamily(0); + mesh1->SetMass(1000.); + mesh1->SetMOI(make_float3(200., 200., 200.)); + std::cout << "Created mesh instance 1 at (-1.5, 0, 0)" << std::endl; + auto tracker1 = DEMSim.Track(mesh1); + + auto mesh2 = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(1.5, 0, 2)); + mesh2->SetFamily(0); + mesh2->SetMass(1000.); + mesh2->SetMOI(make_float3(200., 200., 200.)); + std::cout << "Created mesh instance 2 at (1.5, 0, 2)" << std::endl; + auto tracker2 = DEMSim.Track(mesh2); + + auto mesh3 = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(0, 2, 1)); + mesh3->SetFamily(0); + mesh3->SetMass(1000.); + mesh3->SetMOI(make_float3(200., 200., 200.)); + std::cout << "Created mesh instance 3 at (0, 2, 1)" << std::endl; + + std::cout << "\n--- Test 3: Duplicate a Mesh Object ---" << std::endl; + // Duplicate an existing mesh and modify its properties + auto mesh_dup = DEMSim.Duplicate(mesh1); + mesh_dup->SetInitPos(make_float3(0, -2, 1)); + mesh_dup->SetFamily(0); + mesh_dup->SetMass(1000.); + mesh_dup->SetMOI(make_float3(200., 200., 200.)); + std::cout << "Duplicated mesh1 and moved to (0, -2, 1)" << std::endl; + + std::cout << "\n--- Test 4: Vector-based Position Interface ---" << std::endl; + // Test the vector-based interface + float3 pos = make_float3(3, 0, 1); + auto mesh4 = DEMSim.AddMeshFromTemplate(mesh_template, pos); + mesh4->SetFamily(0); + mesh4->SetMass(1000.); + mesh4->SetMOI(make_float3(200., 200., 200.)); + std::cout << "Created mesh instance 4 at (3, 0, 1) using vector interface" << std::endl; + + std::cout << "\n--- Initializing Simulation ---" << std::endl; + float step_time = 1e-5; + DEMSim.SetTimeStepSize(step_time); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, -9.81)); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.Initialize(); + + path out_dir = current_path(); + out_dir /= "DemoOutput_MeshTemplate"; + create_directory(out_dir); + + std::cout << "\n--- Running Short Simulation ---" << std::endl; + float frame_time = 1e-2; + int frame = 0; + int frame_step = (int)(frame_time / step_time); + double final_time = 0.1; // Short simulation + + for (int i = 0; i < (int)(final_time / step_time); i++) { + if (i % frame_step == 0) { + frame++; + std::cout << "Frame: " << frame << " Time: " << i * step_time << "s" << std::endl; + + char meshfilename[100]; + sprintf(meshfilename, "mesh_template_demo_%04d.vtk", frame); + DEMSim.WriteMeshFile(out_dir / meshfilename); + + float3 pos1 = tracker1->Pos(); + float3 pos2 = tracker2->Pos(); + std::cout << " Mesh 1 position: (" << pos1.x << ", " << pos1.y << ", " << pos1.z << ")" << std::endl; + std::cout << " Mesh 2 position: (" << pos2.x << ", " << pos2.y << ", " << pos2.z << ")" << std::endl; + } + + DEMSim.DoDynamics(step_time); + } + + std::cout << "\n========================================" << std::endl; + std::cout << "Demo completed successfully!" << std::endl; + std::cout << "Total mesh instances created: 5" << std::endl; + std::cout << "Output written to: " << out_dir << std::endl; + DEMSim.ShowTimingStats(); + std::cout << "========================================" << std::endl; + + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_PatchLocations.cpp b/src/demo/ModularTests/DEMTest_PatchLocations.cpp new file mode 100644 index 00000000..f181b2ba --- /dev/null +++ b/src/demo/ModularTests/DEMTest_PatchLocations.cpp @@ -0,0 +1,148 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// A test that verifies mesh patch location functionality. +// This test checks: +// 1. Default patch locations for single-patch meshes (should be 0,0,0) +// 2. Computed patch locations for multi-patch meshes +// 3. Explicitly set patch locations +// ============================================================================= + +#include +#include + +#include +#include +#include + +using namespace deme; + +// Helper to check if two float3 values are approximately equal +bool approxEqual(const float3& a, const float3& b, float tolerance = 1e-5f) { + return std::abs(a.x - b.x) < tolerance && std::abs(a.y - b.y) < tolerance && std::abs(a.z - b.z) < tolerance; +} + +int main() { + std::cout << "========================================" << std::endl; + std::cout << "DEM Mesh Patch Location Test" << std::endl; + std::cout << "========================================" << std::endl; + + int test_failures = 0; + + // Test 1: Single patch mesh - default location should be (0,0,0) + std::cout << "\n--- Test 1: Single Patch Mesh Default Location ---" << std::endl; + auto mesh1 = std::make_shared(); + + // Create a simple triangle mesh + mesh1->m_vertices = {make_float3(0, 0, 0), make_float3(1, 0, 0), make_float3(0, 1, 0)}; + mesh1->m_face_v_indices = {make_int3(0, 1, 2)}; + mesh1->nTri = 1; + mesh1->nPatches = 1; + mesh1->m_patch_ids = {0}; + + auto locations1 = mesh1->ComputePatchLocations(); + std::cout << "Number of patches: " << mesh1->GetNumPatches() << std::endl; + std::cout << "Computed location for patch 0: (" << locations1[0].x << ", " << locations1[0].y << ", " + << locations1[0].z << ")" << std::endl; + + if (approxEqual(locations1[0], make_float3(0, 0, 0))) { + std::cout << "✓ PASS: Single patch has location (0,0,0)" << std::endl; + } else { + std::cout << "✗ FAIL: Single patch should have location (0,0,0)" << std::endl; + test_failures++; + } + + // Test 2: Multiple patches - should compute centroids + std::cout << "\n--- Test 2: Multi-Patch Mesh Computed Locations ---" << std::endl; + auto mesh2 = std::make_shared(); + + // Create a mesh with two triangles in different patches + mesh2->m_vertices = {make_float3(0, 0, 0), // Triangle 1 vertices (patch 0) + make_float3(1, 0, 0), make_float3(0, 1, 0), + make_float3(2, 0, 0), // Triangle 2 vertices (patch 1) + make_float3(3, 0, 0), make_float3(2, 1, 0)}; + mesh2->m_face_v_indices = { + make_int3(0, 1, 2), // Triangle 1 + make_int3(3, 4, 5) // Triangle 2 + }; + mesh2->nTri = 2; + mesh2->nPatches = 2; + mesh2->m_patch_ids = {0, 1}; + + auto locations2 = mesh2->ComputePatchLocations(); + std::cout << "Number of patches: " << mesh2->GetNumPatches() << std::endl; + + // Expected centroids: + // Patch 0 triangle centroid: ((0+1+0)/3, (0+0+1)/3, 0) = (0.333, 0.333, 0) + // Patch 1 triangle centroid: ((2+3+2)/3, (0+0+1)/3, 0) = (2.333, 0.333, 0) + float3 expected_loc0 = make_float3(1.0f / 3.0f, 1.0f / 3.0f, 0.0f); + float3 expected_loc1 = make_float3(7.0f / 3.0f, 1.0f / 3.0f, 0.0f); + + std::cout << "Computed location for patch 0: (" << locations2[0].x << ", " << locations2[0].y << ", " + << locations2[0].z << ")" << std::endl; + std::cout << "Expected location for patch 0: (" << expected_loc0.x << ", " << expected_loc0.y << ", " + << expected_loc0.z << ")" << std::endl; + + if (approxEqual(locations2[0], expected_loc0)) { + std::cout << "✓ PASS: Patch 0 location is correct" << std::endl; + } else { + std::cout << "✗ FAIL: Patch 0 location is incorrect" << std::endl; + test_failures++; + } + + std::cout << "Computed location for patch 1: (" << locations2[1].x << ", " << locations2[1].y << ", " + << locations2[1].z << ")" << std::endl; + std::cout << "Expected location for patch 1: (" << expected_loc1.x << ", " << expected_loc1.y << ", " + << expected_loc1.z << ")" << std::endl; + + if (approxEqual(locations2[1], expected_loc1)) { + std::cout << "✓ PASS: Patch 1 location is correct" << std::endl; + } else { + std::cout << "✗ FAIL: Patch 1 location is incorrect" << std::endl; + test_failures++; + } + + // Test 3: Explicitly set patch locations + std::cout << "\n--- Test 3: Explicitly Set Patch Locations ---" << std::endl; + auto mesh3 = std::make_shared(); + // Initialize mesh with proper patch setup + mesh3->nTri = 0; // No actual triangles needed for this test + mesh3->nPatches = 2; + mesh3->m_patch_ids.resize(0); // Ensure consistency + + std::vector manual_locations = {make_float3(1.0f, 2.0f, 3.0f), make_float3(4.0f, 5.0f, 6.0f)}; + + mesh3->SetPatchLocations(manual_locations); + + std::cout << "Patch locations explicitly set: " << (mesh3->ArePatchLocationsExplicitlySet() ? "yes" : "no") + << std::endl; + + const auto& retrieved_locations = mesh3->GetPatchLocations(); + std::cout << "Retrieved location for patch 0: (" << retrieved_locations[0].x << ", " << retrieved_locations[0].y + << ", " << retrieved_locations[0].z << ")" << std::endl; + std::cout << "Retrieved location for patch 1: (" << retrieved_locations[1].x << ", " << retrieved_locations[1].y + << ", " << retrieved_locations[1].z << ")" << std::endl; + + if (approxEqual(retrieved_locations[0], manual_locations[0]) && + approxEqual(retrieved_locations[1], manual_locations[1])) { + std::cout << "✓ PASS: Manually set patch locations are correct" << std::endl; + } else { + std::cout << "✗ FAIL: Manually set patch locations are incorrect" << std::endl; + test_failures++; + } + + // Summary + std::cout << "\n========================================" << std::endl; + if (test_failures == 0) { + std::cout << "All tests PASSED!" << std::endl; + std::cout << "========================================" << std::endl; + return 0; + } else { + std::cout << "FAILED " << test_failures << " test(s)" << std::endl; + std::cout << "========================================" << std::endl; + return 1; + } +} diff --git a/src/demo/ModularTests/DEMTest_RuntimeSimParams.cpp b/src/demo/ModularTests/DEMTest_RuntimeSimParams.cpp new file mode 100644 index 00000000..d274a09a --- /dev/null +++ b/src/demo/ModularTests/DEMTest_RuntimeSimParams.cpp @@ -0,0 +1,120 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +// Verify that lightweight simulation-parameter setters update initialized workers without a full UpdateSimParams(). + +#include +#include + +#include +#include +#include +#include + +using namespace deme; + +namespace { + +int fail(const std::string& message) { + std::cerr << "DEMTest_RuntimeSimParams failed: " << message << std::endl; + return 1; +} + +} // namespace + +int main() { + constexpr double initial_step = 1.0e-4; + constexpr double runtime_step = 2.0e-4; + constexpr double runtime_duration = 2.0e-3; + constexpr float runtime_gravity = -5.0f; + + DEMSolver solver; + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(2.0, 2.0, 2.0); + solver.SetInitBinNumTarget(8); + solver.SetTimeStepSize(initial_step); + solver.SetGravitationalAcceleration(make_float3(0.f, 0.f, 0.f)); + + auto material = solver.LoadMaterial({{"E", 1.0e7f}, {"nu", 0.3f}, {"CoR", 0.2f}, {"mu", 0.4f}, {"Crr", 0.f}}); + + // The solver-level DEME 2 policy affects only meshes loaded while it is enabled. + DEMMesh deme2_mesh; + if (!deme2_mesh.LoadWavefrontMesh((GET_DATA_PATH() / "mesh/cube.obj").string())) { + return fail("could not load the DEME 2 policy test mesh"); + } + deme2_mesh.SetMaterial(material); + const size_t deme2_triangle_count = deme2_mesh.GetNumTriangles(); + solver.SetDEME2MeshBehavior(); + auto deme2_template = solver.LoadMeshType(deme2_mesh); + if (deme2_template->GetNumPatches() != deme2_triangle_count) { + return fail("enabled SetDEME2MeshBehavior did not create one patch per triangle"); + } + + DEMMesh normal_mesh; + if (!normal_mesh.LoadWavefrontMesh((GET_DATA_PATH() / "mesh/cube.obj").string())) { + return fail("could not load the normal-policy test mesh"); + } + normal_mesh.SetMaterial(material); + solver.SetDEME2MeshBehavior(false); + auto normal_template = solver.LoadMeshType(normal_mesh); + if (normal_template->GetNumPatches() != 1) { + return fail("disabled SetDEME2MeshBehavior modified a newly loaded mesh"); + } + + auto sphere_type = solver.LoadSphereType(1.0f, 0.05f, material); + auto batch = solver.AddClumps(sphere_type, make_float3(0.f, 0.f, 0.f)); + auto tracker = solver.Track(batch); + solver.Initialize(); + + solver.DoDynamicsThenSync(runtime_duration); + if (std::abs(tracker->GetVel()[2]) > 1.0e-6f) { + return fail("zero setup gravity did not preserve zero vertical velocity"); + } + + solver.SetTimeStepSize(runtime_step); + solver.SetGravitationalAcceleration(make_float3(0.f, 0.f, runtime_gravity)); + solver.SetMaxTriTriPenetration(0.25); + solver.SetTriTriContactRejectionRatio(-1.f); + solver.SetMaxSphereInBin(std::numeric_limits::max()); + solver.SetMaxTriangleInBin(std::numeric_limits::max()); + solver.SetErrorOutVelocity(1.0e6f); + solver.SetErrorOutAngularVelocity(1.0e6f); + solver.SetErrorOutAvgContacts(1.0e6f); + solver.SetUseAngularVelocityMargin(false); + + if (std::abs(solver.GetTimeStepSize() - runtime_step) > 1.0e-12) { + return fail("SetTimeStepSize did not update the public timestep state"); + } + + const double gravity_start_time = solver.GetSimTime(); + solver.DoDynamicsThenSync(runtime_duration); + const double gravity_end_time = solver.GetSimTime(); + const double actual_duration = gravity_end_time - gravity_start_time; + const std::vector velocity = tracker->GetVel(); + const std::vector position = tracker->GetPos(); + const float actual_velocity = velocity[2]; + const float expected_velocity = runtime_gravity * static_cast(actual_duration); + if (std::abs(actual_velocity - expected_velocity) > 5.0e-4f) { + std::cerr << "Runtime gravity diagnostics:\n" + << " requested duration: " << runtime_duration << "\n" + << " actual time range: [" << gravity_start_time << ", " << gravity_end_time << "]\n" + << " actual duration: " << actual_duration << "\n" + << " configured timestep: " << solver.GetTimeStepSize() << "\n" + << " configured gravity z: " << runtime_gravity << "\n" + << " expected velocity z: " << expected_velocity << "\n" + << " actual velocity: [" << velocity[0] << ", " << velocity[1] << ", " << velocity[2] << "]\n" + << " actual position: [" << position[0] << ", " << position[1] << ", " << position[2] << "]\n" + << " velocity z error: " << std::abs(actual_velocity - expected_velocity) << std::endl; + return fail("runtime gravity did not take effect without UpdateSimParams"); + } + + // Exercise both compatibility names and confirm they share the unified implementation. + solver.UpdateStepSize(initial_step); + solver.SetInitTimeStep(runtime_step); + if (std::abs(solver.GetTimeStepSize() - runtime_step) > 1.0e-12) { + return fail("legacy timestep methods do not delegate to SetTimeStepSize"); + } + + std::cout << "DEMTest_RuntimeSimParams passed." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_SimpleCollisions.cpp b/src/demo/ModularTests/DEMTest_SimpleCollisions.cpp new file mode 100644 index 00000000..0f5345fa --- /dev/null +++ b/src/demo/ModularTests/DEMTest_SimpleCollisions.cpp @@ -0,0 +1,482 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Simple collision test: a cube hits an analytical plane with no gravity. +// Cases: +// 1) Edge-first impact (45 deg rotation) +// 2) Corner-first impact (45 deg around X and Y) +// For each case, run with: +// a) Single patch cube +// b) 12-patch cube (one patch per triangle) +// Each scenario is repeated 10 times. We log rebound speed, rebound direction, +// and peak normal force on the plane, plus mean/min/max/std stats. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace deme; + +namespace { + +constexpr bool kUseTriangleParticles = true; // toggle to run the STL-based triangle setup +constexpr float kMmToMeters = 0.001f; +constexpr double kTriangleParticleDensity = 2600.0; + +constexpr int kNumRuns = 10; +constexpr double kGap = 0.005; // 0.5 mm +constexpr double kSpeed = 1.0; // 1 m/s magnitude +constexpr double kTimeStep = 1e-5; +constexpr int kMaxSteps = 50000; // oberserve 0.5s fitting with --> kTimeStep +constexpr double kContactEps = 1e-6; +constexpr double kYoungsModulus = 1e9; +constexpr double kPoissonsRatio = 0.3; + +// NEW: impact angle controls +constexpr double kImpactThetaDeg = 0.0; // 0 = vertical down, 90 = pure lateral +constexpr double kImpactPhiDeg = 0.0; // azimuth in XY plane: 0 -> +X, 90 -> +Y + +// NEW: multi-impact tracking +constexpr int kMaxImpactsToRecord = 8; + +double vmax = kSpeed; + +struct ImpactEvent { + bool has_rebound = false; // rebound captured at end of this contact episode + double peak_normal_force = 0.0; // peak Fn during this episode + double rebound_speed = 0.0; // |v| right after separation (if has_rebound) + float3 rebound_dir = make_float3(0, 0, 0); + int start_step = -1; + int end_step = -1; +}; + +struct RunResult { + bool ok = false; + std::vector impacts; // NEW: can contain multiple episodes +}; + +struct Stats { + double mean = 0.0; + double min = 0.0; + double max = 0.0; + double stddev = 0.0; +}; + +double vec_length(const float3& v) { + return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); +} + +double vec_dot(const float3& a, const float3& b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} + +float3 vec_scale(const float3& v, double s) { + return make_float3(v.x * s, v.y * s, v.z * s); +} + +Stats calc_stats(const std::vector& values) { + Stats s; + if (values.empty()) + return s; + s.min = values.front(); + s.max = values.front(); + double sum = 0.0; + for (double v : values) { + s.min = std::min(s.min, v); + s.max = std::max(s.max, v); + sum += v; + } + s.mean = sum / values.size(); + double var = 0.0; + for (double v : values) { + double d = v - s.mean; + var += d * d; + } + s.stddev = std::sqrt(var / values.size()); + return s; +} + +double compute_bounding_radius(const std::shared_ptr& mesh) { + if (!mesh || mesh->m_vertices.empty()) { + return 0.0; + } + + double cx = 0.0; + double cy = 0.0; + double cz = 0.0; + for (const auto& v : mesh->m_vertices) { + cx += v.x; + cy += v.y; + cz += v.z; + } + const double inv_n = 1.0 / static_cast(mesh->m_vertices.size()); + cx *= inv_n; + cy *= inv_n; + cz *= inv_n; + + double r_max = 0.0; + for (const auto& v : mesh->m_vertices) { + const double dx = v.x - cx; + const double dy = v.y - cy; + const double dz = v.z - cz; + r_max = std::max(r_max, std::sqrt(dx * dx + dy * dy + dz * dz)); + } + return r_max; +} + +double estimate_hertz_dt(double mass, double radius, double E, double nu) { + if (mass <= 0.0 || radius <= 0.0 || E <= 0.0) { + return std::numeric_limits::infinity(); + } + const double one_minus_nu_sq = 1.0 - nu * nu; + if (one_minus_nu_sq <= 0.0) { + return std::numeric_limits::infinity(); + } + + const double E_eff = E / (2.0 * one_minus_nu_sq); + const double R_eff = 0.5 * radius; + const double m_eff = 0.5 * mass; + const double KH = FOUR_OVER_THREE * std::sqrt(0.1) * E_eff * R_eff; + if (KH <= 0.0) { + return std::numeric_limits::infinity(); + } + + return (PI / (2.0 * N_DT)) * std::sqrt(m_eff / KH); +} + +double compute_min_z_rotated(const std::shared_ptr& mesh, const float4& rotQ) { + double min_z = std::numeric_limits::max(); + for (const auto& v_in : mesh->m_vertices) { + float3 v = v_in; + applyFrameTransformLocalToGlobal(v, make_float3(0, 0, 0), rotQ); + min_z = std::min(min_z, static_cast(v.z)); + } + return min_z; +} + +// NEW: build initial velocity vector from speed + angles (theta from +normal, phi azimuth in plane) +float3 build_velocity(double speed, double theta_deg, double phi_deg) { + const double theta = theta_deg * PI / 180.0; + const double phi = phi_deg * PI / 180.0; + + // normal component (downwards for approaching) + const double v_n = -speed * std::cos(theta); + // tangential magnitude + const double v_t = speed * std::sin(theta); + + const double vx = v_t * std::cos(phi); + const double vy = v_t * std::sin(phi); + const double vz = v_n; + + return make_float3((float)vx, (float)vy, (float)vz); +} + +std::shared_ptr load_cube_template(DEMSolver& DEMSim, const std::shared_ptr& mat_type) { + auto mesh_template = DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/cube.obj").string(), mat_type, true, false); + if (!mesh_template) + return nullptr; + mesh_template->SetMaterial(mat_type); + return mesh_template; +} + +std::shared_ptr load_triangle_template(DEMSolver& DEMSim, + const std::shared_ptr& mat_type, + float& out_mass, + float3& out_moi) { + std::shared_ptr mesh_template = + DEMSim.LoadMeshType((GET_DATA_PATH() / "mesh/simpleTriangleShape4mm.stl").string(), mat_type, true, false); + if (!mesh_template) + return nullptr; + + mesh_template->Scale(kMmToMeters); + + double volume = 0.0; + float3 center = make_float3(0, 0, 0); + float3 inertia = make_float3(0, 0, 0); + mesh_template->ComputeMassProperties(volume, center, inertia); + + out_mass = static_cast(volume * kTriangleParticleDensity); + out_moi = inertia * static_cast(kTriangleParticleDensity); + + mesh_template->SetMaterial(mat_type); + return mesh_template; +} + +RunResult run_single_collision(const float4& init_rot, + bool use_triangle_particles, + const std::string& label, + int run_id, + const float3& init_vel) { + RunResult result; + + DEMSolver DEMSim; + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(5, 5, 5); + DEMSim.SetGravitationalAcceleration(make_float3(0, 0, 0)); + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetExpandSafetyAdder(vmax); + + auto mat_type = + DEMSim.LoadMaterial({{"E", kYoungsModulus}, {"nu", kPoissonsRatio}, {"CoR", 0.6}, {"mu", 0.5}, {"Crr", 0.00}}); + + float3 plane_normal = make_float3(0, 0, 1); + auto plane = DEMSim.AddBCPlane(make_float3(0, 0, 0), plane_normal, mat_type); + auto plane_tracker = DEMSim.Track(plane); + + auto mesh_template = std::shared_ptr{}; + float particle_mass = 1.0f; + float3 particle_moi = make_float3(1.0f / 6.0f, 1.0f / 6.0f, 1.0f / 6.0f); + + if (use_triangle_particles) { + mesh_template = load_triangle_template(DEMSim, mat_type, particle_mass, particle_moi); + } else { + mesh_template = load_cube_template(DEMSim, mat_type); + } + if (!mesh_template) { + std::cout << "[" << label << "] Run " << run_id << ": failed to load mesh template" << std::endl; + return result; + } + + double min_z = compute_min_z_rotated(mesh_template, init_rot); + double init_z = kGap - min_z; + + auto body = DEMSim.AddMeshFromTemplate(mesh_template, make_float3(0, 0, 0)); + body->SetFamily(0); + body->SetMass(particle_mass); + body->SetMOI(particle_moi); + body->SetInitQuat(init_rot); + body->SetInitPos(make_float3(0, 0, static_cast(init_z))); + auto body_tracker = DEMSim.Track(body); + + double sim_dt = kTimeStep; + if (use_triangle_particles) { + const double radius_est = compute_bounding_radius(mesh_template); + const double dt_est = estimate_hertz_dt(particle_mass, radius_est, kYoungsModulus, kPoissonsRatio); + if (std::isfinite(dt_est) && dt_est > 0.0 && dt_est < sim_dt) { + sim_dt = dt_est; + std::cout << "[" << label << "] auto-dt active: clamped dt to " << sim_dt << " (requested " << kTimeStep + << ", estimated max " << dt_est << ")" << std::endl; + } + } + + DEMSim.SetTimeStepSize(sim_dt); + DEMSim.Initialize(); + + // NEW: angled initial velocity + body_tracker->SetVel(init_vel); + + bool in_contact = false; + ImpactEvent current{}; + int impacts_recorded = 0; + + for (int step = 0; step < kMaxSteps; ++step) { + DEMSim.DoStepDynamics(); + + // NOTE: this is your current way to estimate contact force on the plane + float3 plane_force = plane_tracker->ContactAcc(); + plane_force = vec_scale(plane_force, plane_tracker->Mass()); + double normal_force = std::abs(vec_dot(plane_force, plane_normal)); + + // start of a new contact episode + if (!in_contact && normal_force > kContactEps) { + in_contact = true; + current = ImpactEvent{}; + current.start_step = step; + current.peak_normal_force = normal_force; + } + + // update peak during contact + if (in_contact) { + current.peak_normal_force = std::max(current.peak_normal_force, normal_force); + } + + // end of contact episode + if (in_contact && normal_force <= kContactEps) { + in_contact = false; + current.end_step = step; + + // capture rebound info if moving away (positive normal velocity) + float3 vel = body_tracker->Vel(); + double vel_n = vec_dot(vel, plane_normal); + + if (vel_n > 0.0) { + double speed = vec_length(vel); + float3 dir = make_float3(0, 0, 0); + if (speed > 0) { + dir = vec_scale(vel, 1.0 / speed); + } + current.has_rebound = true; + current.rebound_speed = speed; + current.rebound_dir = dir; + } + + result.impacts.push_back(current); + impacts_recorded++; + + if (impacts_recorded >= kMaxImpactsToRecord) { + break; + } + } + } + + result.ok = !result.impacts.empty(); + if (!result.ok) { + std::cout << "[" << label << "] Run " << run_id << ": no impacts recorded within max steps" << std::endl; + } + + return result; +} + +// Updated stats: by default we evaluate the FIRST rebound episode that has_rebound==true +void print_stats_block(const std::string& label, const std::vector& results) { + std::vector speeds; + std::vector forces; + std::vector dir_x; + std::vector dir_y; + std::vector dir_z; + std::vector n_impacts; + + for (const auto& r : results) { + if (!r.ok) + continue; + + n_impacts.push_back((double)r.impacts.size()); + + // pick first episode with rebound + const ImpactEvent* chosen = nullptr; + for (const auto& ev : r.impacts) { + if (ev.has_rebound) { + chosen = &ev; + break; + } + } + if (!chosen) { + // still record peak of first impact if rebound wasn't detected + forces.push_back(r.impacts.front().peak_normal_force); + continue; + } + + speeds.push_back(chosen->rebound_speed); + forces.push_back(chosen->peak_normal_force); + dir_x.push_back(chosen->rebound_dir.x); + dir_y.push_back(chosen->rebound_dir.y); + dir_z.push_back(chosen->rebound_dir.z); + } + + Stats s_speed = calc_stats(speeds); + Stats s_force = calc_stats(forces); + Stats s_dx = calc_stats(dir_x); + Stats s_dy = calc_stats(dir_y); + Stats s_dz = calc_stats(dir_z); + Stats s_ni = calc_stats(n_impacts); + + std::cout << "\n=== " << label << " stats (population stddev) ===" << std::endl; + std::cout << "Impacts per run: mean=" << s_ni.mean << " min=" << s_ni.min << " max=" << s_ni.max + << " std=" << s_ni.stddev << std::endl; + std::cout << "Rebound speed [m/s]: mean=" << s_speed.mean << " min=" << s_speed.min << " max=" << s_speed.max + << " std=" << s_speed.stddev << std::endl; + std::cout << "Peak normal force [N]: mean=" << s_force.mean << " min=" << s_force.min << " max=" << s_force.max + << " std=" << s_force.stddev << std::endl; + std::cout << "Rebound dir X: mean=" << s_dx.mean << " min=" << s_dx.min << " max=" << s_dx.max + << " std=" << s_dx.stddev << std::endl; + std::cout << "Rebound dir Y: mean=" << s_dy.mean << " min=" << s_dy.min << " max=" << s_dy.max + << " std=" << s_dy.stddev << std::endl; + std::cout << "Rebound dir Z: mean=" << s_dz.mean << " min=" << s_dz.min << " max=" << s_dz.max + << " std=" << s_dz.stddev << std::endl; +} + +// Rotations +float4 flat_quat() { + return make_float4(0, 0, 0, 1); // NEW: identity +} + +float4 edge_quat() { + float4 q = make_float4(0, 0, 0, 1); + q = RotateQuat(q, make_float3(1, 0, 0), static_cast(PI / 4.0)); + return q; +} + +float4 corner_quat() { + float4 q = make_float4(0, 0, 0, 1); + q = RotateQuat(q, make_float3(1, 0, 0), static_cast(PI / 4.0)); + q = RotateQuat(q, make_float3(0, 1, 0), static_cast(PI / 4.0)); + return q; +} + +void run_scenario(const std::string& label, const float4& rot, bool use_triangle_particles, const float3& init_vel) { + std::cout << "\n========================================" << std::endl; + std::cout << label << std::endl; + std::cout << "========================================" << std::endl; + std::cout << "Using mesh: " << (use_triangle_particles ? "simpleTriangleShape4mm.stl" : "cube.obj") << std::endl; + std::cout << "Init vel: (" << init_vel.x << ", " << init_vel.y << ", " << init_vel.z << ")" + << " |v|=" << vec_length(init_vel) << std::endl; + + std::vector results; + results.reserve(kNumRuns); + + for (int i = 0; i < kNumRuns; ++i) { + RunResult r = run_single_collision(rot, use_triangle_particles, label, i, init_vel); + results.push_back(r); + + if (r.ok) { + std::cout << "Run " << i << ": impacts=" << r.impacts.size(); + // print first rebound episode if exists + const ImpactEvent* chosen = nullptr; + for (const auto& ev : r.impacts) { + if (ev.has_rebound) { + chosen = &ev; + break; + } + } + if (chosen) { + std::cout << " rebound_speed=" << chosen->rebound_speed << " dir=(" << chosen->rebound_dir.x << ", " + << chosen->rebound_dir.y << ", " << chosen->rebound_dir.z << ")" + << " peakFn=" << chosen->peak_normal_force; + } else { + std::cout << " (no rebound captured) peakFn_first=" << r.impacts.front().peak_normal_force; + } + std::cout << std::endl; + } + } + + print_stats_block(label, results); +} + +} // namespace + +int main() { + std::cout << "========================================" << std::endl; + std::cout << "DEM Simple Collisions Test" << std::endl; + std::cout << "========================================" << std::endl; + std::cout << "Particle mesh mode: " << (kUseTriangleParticles ? "simpleTriangleShape4mm.stl" : "cube.obj") + << std::endl; + + // NEW: build velocity once (same for all scenarios) + float3 init_vel = build_velocity(kSpeed, kImpactThetaDeg, kImpactPhiDeg); + + float4 q_flat = flat_quat(); + float4 q_edge = edge_quat(); + float4 q_corner = corner_quat(); + + run_scenario("Flat impact", q_flat, kUseTriangleParticles, init_vel); + run_scenario("Edge impact", q_edge, kUseTriangleParticles, init_vel); + run_scenario("Corner impact", q_corner, kUseTriangleParticles, init_vel); + + std::cout << "\n========================================" << std::endl; + std::cout << "Test completed" << std::endl; + std::cout << "========================================" << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_SpherePlanePeneArea.cpp b/src/demo/ModularTests/DEMTest_SpherePlanePeneArea.cpp new file mode 100644 index 00000000..de7f49fb --- /dev/null +++ b/src/demo/ModularTests/DEMTest_SpherePlanePeneArea.cpp @@ -0,0 +1,180 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Sphere-plane penetration and area test: a sphere mesh (sphere_highres.obj) +// is placed at several fixed positions so that it penetrates an analytical +// plane by a controlled depth. A custom force model (no actual force computed) +// writes the patch-based penetration depth and contact area into owner +// wildcards after each simulation step. The solver values are then compared +// against the analytical expressions: +// +// penetration_analytical = d +// area_analytical = pi * (2*R*d - d^2) +// +// where R is the sphere radius and d is the intended penetration depth. +// For a high-resolution sphere mesh the two should agree very closely. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::filesystem; +using namespace deme; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + const float R = 0.1f; // sphere radius after scaling (m) + + // Penetration depths to test (m) + const std::vector depths = {0.001f, 0.003f, 0.007f, 0.015f, 0.03f}; + + // Material (values are irrelevant since we generate no force, but must be set) + const float E = 1e8f; + const float nu = 0.3f; + const float CoR = 0.3f; + const float mu = 0.3f; + + // Simulation time step – we only need the solver to run one step per depth + const float step_size = 1e-5f; + + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_SpherePlanePeneArea" << std::endl; + std::cout << " Sphere radius R = " << R << " m" << std::endl; + std::cout << " Analytical plane at z=0 with normal (0,0,1)" << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + // Box large enough to contain the sphere at all test positions + DEMSim.InstructBoxDomainDimension(20, 20, 20); + DEMSim.SetGravitationalAcceleration(make_float3(0.f, 0.f, 0.f)); + // Mesh-related contacts must be enabled + DEMSim.SetMeshUniversalContact(true); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.f}}); + + // ========================================================================= + // Custom force model: write patch penetration and area to owner wildcards. + // No actual contact force is generated. + // ========================================================================= + auto force_model = DEMSim.DefineContactForceModel(R"( + // Record patch-based penetration depth and contact area in owner wildcards. + // AOwner is the sphere mesh owner; BOwner is the analytical plane owner. + if (overlapDepth > 0) { + penetration[AOwner] = (float)overlapDepth; + area[AOwner] = (float)overlapArea; + } else { + penetration[AOwner] = 0.f; + area[AOwner] = 0.f; + } + // Explicitly zero the force so the solver does not emit a warning. + force = make_float3(0.f, 0.f, 0.f); + )"); + force_model->SetPerOwnerWildcards({"penetration", "area"}); + + // ========================================================================= + // Analytical plane: z = 0, normal = (0,0,1), family 100 (fixed) + // ========================================================================= + auto plane = DEMSim.AddBCPlane(make_float3(0.f, 0.f, 0.f), make_float3(0.f, 0.f, 1.f), mat); + plane->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + // ========================================================================= + // Sphere mesh: sphere_highres.obj (unit sphere), scaled to radius R. + // Family 1 (fixed); initial position is placed at the first test depth. + // ========================================================================= + const float sphere_mass = 2600.f * (4.f / 3.f) * PI * R * R * R; + const float sphere_moi = 2.f / 5.f * sphere_mass * R * R; + + // Initial position: sphere centre above plane, no penetration yet + const float d0 = depths[0]; + auto sphere = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/sphere_highres.obj").string(), mat); + sphere->Scale(R); + sphere->SetMass(sphere_mass); + sphere->SetMOI(make_float3(sphere_moi, sphere_moi, sphere_moi)); + sphere->SetInitPos(make_float3(0.f, 0.f, R - d0)); + sphere->SetInitQuat(make_float4(0.7071, 0.7071, 0, 0)); + sphere->SetFamily(1); + DEMSim.SetFamilyFixed(1); + + auto sphere_tracker = DEMSim.Track(sphere); + + // ========================================================================= + // Initialise (JIT-compiles the GPU kernels once) + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetCDUpdateFreq(1); + DEMSim.Initialize(); + + // Output directory + path out_dir = current_path() / "modular_test_output" / "DEMTest_SpherePlanePeneArea"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + + // ========================================================================= + // Loop over all penetration depths + // ========================================================================= + const int accuracy = 7, t_width = 18; + std::cout << std::endl; + std::cout << std::setw(t_width) << "depth_d(m)" << std::setw(t_width) << "pen_solver(m)" << std::setw(t_width) + << "pen_analyt(m)" << std::setw(t_width) << "pen_err(%)" << std::setw(t_width) << "area_solver(m2)" + << std::setw(t_width) << "area_analyt(m2)" << std::setw(14) << "area_err(%)" << std::endl; + std::cout << std::string(125, '-') << std::endl; + + for (float d : depths) { + // Move sphere to the desired penetration depth + sphere_tracker->SetPos(make_float3(0.f, 0.f, R - d)); + + // Run one dynamics step so the force model executes + DEMSim.DoDynamicsThenSync(step_size); + + // Read back owner wildcards for the sphere (family 1) + std::vector pen_vec = DEMSim.GetFamilyOwnerWildcardValue(1, "penetration"); + std::vector area_vec = DEMSim.GetFamilyOwnerWildcardValue(1, "area"); + + const float pen_solver = pen_vec.empty() ? 0.f : pen_vec[0]; + const float area_solver = area_vec.empty() ? 0.f : area_vec[0]; + + // Analytical values + const float pen_analyt = d; + const float area_analyt = PI * (2.f * R * d - d * d); + + const float pen_err = (pen_analyt > 1e-12f) ? 100.f * std::abs(pen_solver - pen_analyt) / pen_analyt : 0.f; + const float area_err = (area_analyt > 1e-12f) ? 100.f * std::abs(area_solver - area_analyt) / area_analyt : 0.f; + + std::cout << std::setw(t_width) << std::fixed << std::setprecision(accuracy) << d << std::setw(t_width) + << std::setprecision(accuracy) << pen_solver << std::setw(t_width) << std::setprecision(accuracy) + << pen_analyt << std::setw(t_width) << std::setprecision(accuracy) << pen_err << std::setw(t_width) + << std::setprecision(accuracy) << area_solver << std::setw(t_width) << std::setprecision(accuracy) + << area_analyt << std::setw(t_width) << std::setprecision(accuracy) << area_err << std::endl; + } + + std::cout << std::string(125, '-') << std::endl; + std::cout << std::endl; + std::cout << "Note: solver penetration uses the maximum projected vertex depth" << std::endl; + std::cout << " across all triangles in the contact patch; analytical area" << std::endl; + std::cout << " = pi*(2*R*d - d^2). Differences reflect mesh discretisation." << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_SpherePlanePeneArea exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_SphereSphereArea.cpp b/src/demo/ModularTests/DEMTest_SphereSphereArea.cpp new file mode 100644 index 00000000..ec89bcda --- /dev/null +++ b/src/demo/ModularTests/DEMTest_SphereSphereArea.cpp @@ -0,0 +1,197 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Sphere-sphere penetration and area test: two sphere meshes (sphere_highres.obj) +// are fixed in place along the Z-axis at several separation distances so that +// their surfaces overlap by a controlled depth. A custom force model (no +// actual force computed) writes the patch-based penetration depth and contact +// area into owner wildcards after each simulation step. The solver values are +// compared against the analytical expressions for two equal spheres of radius R: +// +// penetration_analytical = d +// area_analytical = pi * (R*d - d^2/4) +// +// where d is the intended penetration depth (= 2R - centre_distance). +// For a high-resolution mesh the values should be similar to the analytical +// result, though the mesh discretisation introduces some approximation error. +// ============================================================================= + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std::filesystem; +using namespace deme; + +int main() { + // ========================================================================= + // Parameters + // ========================================================================= + const float R = 0.1f; // radius of each sphere after scaling (m) + + // Penetration depths to test (m) + const std::vector depths = {0.001f, 0.003f, 0.007f, 0.015f, 0.03f}; + + // Material (values are irrelevant since we generate no force, but must be set) + const float E = 1e8f; + const float nu = 0.3f; + const float CoR = 0.3f; + const float mu = 0.3f; + + // Simulation time step – we only need one step per depth measurement + const float step_size = 1e-5f; + + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_SphereSphereArea" << std::endl; + std::cout << " Sphere radius R = " << R << " m (each)" << std::endl; + std::cout << " Sphere A fixed at (0,0,R); Sphere B moves along Z" << std::endl; + std::cout << "=====================================================" << std::endl; + + // ========================================================================= + // Solver setup + // ========================================================================= + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(20, 20, 20); + DEMSim.SetGravitationalAcceleration(make_float3(0.f, 0.f, 0.f)); + // Mesh-mesh contacts must be enabled for two mesh spheres + DEMSim.SetMeshUniversalContact(true); + // Use the simple patch ID-based triangle combination (RefBranch-style) + DEMSim.SetSimplePatchCombination(true); + + auto mat = DEMSim.LoadMaterial({{"E", E}, {"nu", nu}, {"CoR", CoR}, {"mu", mu}, {"Crr", 0.f}}); + + // ========================================================================= + // Custom force model: write patch penetration and area to owner wildcards. + // No actual contact force is generated. + // AOwner and BOwner are the two sphere mesh owners respectively. + // ========================================================================= + auto force_model = DEMSim.DefineContactForceModel(R"( + // Record patch-based penetration depth and contact area in owner wildcards. + // Both sphere owners receive the same (patch-level) depth and area. + if (overlapDepth > 0) { + penetration[AOwner] = (float)overlapDepth; + area[AOwner] = (float)overlapArea; + penetration[BOwner] = (float)overlapDepth; + area[BOwner] = (float)overlapArea; + } else { + penetration[AOwner] = 0.f; + area[AOwner] = 0.f; + penetration[BOwner] = 0.f; + area[BOwner] = 0.f; + } + // Explicitly zero the force so the solver does not emit a warning. + force = make_float3(0.f, 0.f, 0.f); + )"); + force_model->SetPerOwnerWildcards({"penetration", "area"}); + + // ========================================================================= + // Sphere A: fixed at (0, 0, R), family 1 + // Sphere B: initially set for the first test depth, family 2 + // ========================================================================= + const float sphere_mass = 2600.f * (4.f / 3.f) * PI * R * R * R; + const float sphere_moi = 2.f / 5.f * sphere_mass * R * R; + + const float d0 = depths[0]; + + // Sphere A centre: (0, 0, R) — just resting on the z=0 level + auto sphereA = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/sphere_highres.obj").string(), mat); + sphereA->Scale(R); + sphereA->SetMass(sphere_mass); + sphereA->SetMOI(make_float3(sphere_moi, sphere_moi, sphere_moi)); + sphereA->SetInitPos(make_float3(0.f, 0.f, R)); + sphereA->SetInitQuat(make_float4(0.7071, 0.7071, 0, 0)); + sphereA->SetFamily(1); + DEMSim.SetFamilyFixed(1); + + auto trackerA = DEMSim.Track(sphereA); + + // Sphere B centre: (0, 0, 3R - d0), so |AB| = 2R - d0 → penetration = d0 + auto sphereB = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/sphere_highres.obj").string(), mat); + sphereB->Scale(R); + sphereB->SetMass(sphere_mass); + sphereB->SetMOI(make_float3(sphere_moi, sphere_moi, sphere_moi)); + sphereB->SetInitPos(make_float3(0.f, 0.f, 3.f * R - d0)); + sphereB->SetInitQuat(make_float4(0.7071, 0.7071, 0, 0)); + sphereB->SetFamily(2); + DEMSim.SetFamilyFixed(2); + + auto trackerB = DEMSim.Track(sphereB); + + // ========================================================================= + // Initialise (JIT-compiles the GPU kernels once) + // ========================================================================= + DEMSim.SetTimeStepSize(step_size); + DEMSim.SetCDUpdateFreq(1); + DEMSim.SetExpandSafetyAdder(300.); + DEMSim.SetErrorOutAvgContacts(20000); + DEMSim.Initialize(); + + // Output directory + path out_dir = current_path() / "modular_test_output" / "DEMTest_SphereSphereArea"; + std::error_code dir_ec; + create_directories(out_dir, dir_ec); + + // ========================================================================= + // Loop over all penetration depths + // ========================================================================= + const int accuracy = 7, t_width = 18; + std::cout << std::endl; + std::cout << std::setw(t_width) << "depth_d(m)" << std::setw(t_width) << "pen_solver(m)" << std::setw(t_width) + << "pen_analyt(m)" << std::setw(t_width) << "pen_err(%)" << std::setw(t_width) << "area_solver(m2)" + << std::setw(t_width) << "area_analyt(m2)" << std::setw(t_width) << "area_err(%)" << std::endl; + std::cout << std::string(125, '-') << std::endl; + + for (float d : depths) { + // Sphere A stays at (0,0,R); move sphere B so centre distance = 2R - d + trackerA->SetPos(make_float3(0.f, 0.f, R)); + trackerB->SetPos(make_float3(0.f, 0.f, 3.f * R - d)); + + // Run one dynamics step so the force model executes + DEMSim.DoDynamicsThenSync(step_size); + + // Read back owner wildcards: sphere A is family 1, sphere B is family 2 + std::vector penA_vec = DEMSim.GetFamilyOwnerWildcardValue(1, "penetration"); + std::vector areaA_vec = DEMSim.GetFamilyOwnerWildcardValue(1, "area"); + + // Use sphere A's values (both spheres receive the same patch values) + const float pen_solver = penA_vec.empty() ? 0.f : penA_vec[0]; + const float area_solver = areaA_vec.empty() ? 0.f : areaA_vec[0]; + + // Analytical values for two equal spheres of radius R with penetration d + const float pen_analyt = d; + const float area_analyt = PI * (R * d - d * d / 4.f); + + const float pen_err = (pen_analyt > 1e-12f) ? 100.f * std::abs(pen_solver - pen_analyt) / pen_analyt : 0.f; + const float area_err = (area_analyt > 1e-12f) ? 100.f * std::abs(area_solver - area_analyt) / area_analyt : 0.f; + + std::cout << std::setw(t_width) << std::fixed << std::setprecision(accuracy) << d << std::setw(t_width) + << std::setprecision(accuracy) << pen_solver << std::setw(t_width) << std::setprecision(accuracy) + << pen_analyt << std::setw(t_width) << std::setprecision(accuracy) << pen_err << std::setw(t_width) + << std::setprecision(accuracy) << area_solver << std::setw(t_width) << std::setprecision(accuracy) + << area_analyt << std::setw(t_width) << std::setprecision(accuracy) << area_err << std::endl; + } + + std::cout << std::string(125, '-') << std::endl; + std::cout << std::endl; + std::cout << "Note: for mesh-mesh sphere contacts the solver's patch-based" << std::endl; + std::cout << " penetration and area are approximate (not exact)." << std::endl; + std::cout << " Analytical area = pi*(R*d - d^2/4) for equal spheres." << std::endl; + + DEMSim.ShowTimingStats(); + std::cout << "=====================================================" << std::endl; + std::cout << "DEMTest_SphereSphereArea exiting..." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_StateFinite.cpp b/src/demo/ModularTests/DEMTest_StateFinite.cpp new file mode 100644 index 00000000..7d9bb394 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_StateFinite.cpp @@ -0,0 +1,54 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause + +#include "DEM/API.h" + +#include +#include +#include +#include + +using namespace deme; + +// Run one isolated no-contact case. Invalid cases intentionally trap on the GPU; the Python driver checks +// their diagnostics in separate processes because a device trap invalidates the CUDA context. +int main(int argc, char** argv) { + if (argc != 4) { + std::cerr << "Usage: DEMTest_StateFinite \n"; + return 2; + } + const std::string kind = argv[1]; + const std::string margin = argv[2]; + const float invalid = std::string(argv[3]) == "inf" ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + DEMSolver solver; + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(2., 2., 2.); + solver.SetInitBinNumTarget(8); + solver.SetTimeStepSize(1.e-4); + solver.SetGravitationalAcceleration(make_float3(0.f)); + if (margin == "fixed") + solver.SetExpandFactor(0.01f); + if (margin == "no-angular") + solver.SetUseAngularVelocityMargin(false); + auto material = solver.LoadMaterial({{"E", 1.e7f}, {"nu", 0.3f}, {"CoR", 0.2f}, {"mu", 0.4f}, {"Crr", 0.f}}); + auto sphere = solver.LoadSphereType(1.f, 0.05f, material); + auto batch = solver.AddClumps(sphere, make_float3(0.f)); + auto tracker = solver.Track(batch); + solver.Initialize(); + // Inject after initialization to exercise the runtime dT handoff, not setup validation. + tracker->SetVel(make_float3(kind == "linear" ? invalid : 0.1f, 0.f, 0.f)); + tracker->SetAngVel(make_float3(0.f, kind == "angular" ? invalid : 0.2f, 0.f)); + solver.DoDynamicsThenSync(1.e-3); + if (kind != "healthy") { + std::cerr << "FAIL: invalid velocity escaped detection\n"; + return 1; + } + const auto pos = tracker->Pos(); + if (!std::isfinite(pos.x) || std::abs(pos.x - 0.1 * solver.GetSimTime()) > 1.e-6) { + std::cerr << "FAIL: healthy free motion changed: x=" << pos.x << ", t=" << solver.GetSimTime() << "\n"; + return 1; + } + std::cout << "PASS: healthy no-contact state\n"; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_TrianglePVTracking.cpp b/src/demo/ModularTests/DEMTest_TrianglePVTracking.cpp new file mode 100644 index 00000000..bbf16dc9 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_TrianglePVTracking.cpp @@ -0,0 +1,124 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +// ============================================================================= +// Triangle P/V/P*V tracking and mesh-wear API smoke test. +// +// Two cube meshes are placed in shallow face contact. The lower cube is fixed +// and the upper cube is given a tangential velocity, so at least one tracked +// triangle on the upper cube should accumulate contact pressure and P*V. +// ============================================================================= + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace deme; + +namespace { + +int fail(const std::string& message) { + std::cerr << "DEMTest_TrianglePVTracking failed: " << message << std::endl; + return 1; +} + +bool allFiniteAndNonnegative(const std::vector& values) { + return std::all_of(values.begin(), values.end(), [](float val) { return std::isfinite(val) && val >= 0.f; }); +} + +float maxValue(const std::vector& values) { + return values.empty() ? 0.f : *std::max_element(values.begin(), values.end()); +} + +} // namespace + +int main() { + constexpr float cube_side = 0.2f; + constexpr float overlap = 0.004f; + constexpr float density = 2500.f; + constexpr float mass = density * cube_side * cube_side * cube_side; + constexpr float moi = mass * cube_side * cube_side / 6.f; + constexpr float step_size = 1e-5f; + constexpr float run_time = 2e-4f; + + DEMSolver DEMSim; + DEMSim.SetVerbosity("ERROR"); + DEMSim.SetOutputFormat(OUTPUT_FORMAT::CSV); + DEMSim.InstructBoxDomainDimension(2.f, 2.f, 2.f); + DEMSim.SetGravitationalAcceleration(make_float3(0.f, 0.f, 0.f)); + DEMSim.SetMeshUniversalContact(true); + DEMSim.SetExpandSafetyType("auto"); + DEMSim.SetCDUpdateFreq(1); + DEMSim.SetInitBinNumTarget(8); + DEMSim.SetErrorOutAvgContacts(10000); + + auto mat = DEMSim.LoadMaterial({{"E", 1e8f}, {"nu", 0.3f}, {"CoR", 0.1f}, {"mu", 0.5f}, {"Crr", 0.f}}); + + auto lower_cube = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat); + lower_cube->Scale(cube_side); + lower_cube->SetMass(mass); + lower_cube->SetMOI(make_float3(moi, moi, moi)); + lower_cube->SetInitPos(make_float3(0.f, 0.f, 0.f)); + lower_cube->SetFamily(100); + DEMSim.SetFamilyFixed(100); + + auto upper_cube = DEMSim.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/cube.obj").string(), mat); + upper_cube->Scale(cube_side); + upper_cube->SetMass(mass); + upper_cube->SetMOI(make_float3(moi, moi, moi)); + upper_cube->SetInitPos(make_float3(0.f, 0.f, cube_side - overlap)); + upper_cube->SetFamily(1); + auto upper_tracker = DEMSim.Track(upper_cube); + + DEMSim.SetTimeStepSize(step_size); + DEMSim.Initialize(); + DEMSim.SetTriTriPenetration(overlap * 2.f); + + const bodyID_t upper_owner = upper_tracker->GetOwnerID(); + const size_t upper_triangles = upper_cube->GetNumTriangles(); + DEMSim.SetTrianglePVTrackingOwners({upper_owner}); + + upper_tracker->SetVel(make_float3(0.05f, 0.f, 0.f)); + DEMSim.DoDynamicsThenSync(run_time); + + std::vector avgP; + std::vector avgV; + std::vector avgPV; + if (!DEMSim.GetTrackedOwnerTrianglePV(upper_owner, avgP, avgV, avgPV, false)) { + return fail("tracked owner query returned false"); + } + if (avgP.size() != upper_triangles || avgV.size() != upper_triangles || avgPV.size() != upper_triangles) { + return fail("tracked vector sizes do not match mesh triangle count"); + } + if (!allFiniteAndNonnegative(avgP) || !allFiniteAndNonnegative(avgV) || !allFiniteAndNonnegative(avgPV)) { + return fail("tracked vectors contain non-finite or negative values"); + } + if (maxValue(avgP) <= 0.f) { + return fail("tracked pressure remained zero"); + } + if (maxValue(avgPV) <= 0.f) { + return fail("tracked P*V remained zero"); + } + + // Smoke-test the wear-control API using zero wear rate. This exercises the + // enable/flush/disable path without intentionally deforming the mesh. + DEMSim.EnableMeshWearModel(upper_owner, 0.0, step_size); + DEMSim.DoDynamicsThenSync(step_size); + DEMSim.FlushMeshWearModels(); + DEMSim.DisableMeshWearModel(upper_owner); + DEMSim.DisableAllMeshWearModels(); + DEMSim.DisableTrianglePVTracking(); + + std::cout << "DEMTest_TrianglePVTracking passed. " + << "triangles=" << upper_triangles << " maxP=" << maxValue(avgP) << " maxV=" << maxValue(avgV) + << " maxPV=" << maxValue(avgPV) << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_VisualizationScene.cpp b/src/demo/ModularTests/DEMTest_VisualizationScene.cpp new file mode 100644 index 00000000..3c936b95 --- /dev/null +++ b/src/demo/ModularTests/DEMTest_VisualizationScene.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include + +#include "DEM/API.h" + +using namespace deme; +namespace { +void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} +bool near(float a, float b) { + return std::abs(a - b) < 1.e-4f; +} + +// Independently apply the quaternion-vector formula and compare cached local geometry against legacy snapshots. +float3 transform(float3 p, float3 position, float4 q) { + float3 t = make_float3(2 * (q.y * p.z - q.z * p.y), 2 * (q.z * p.x - q.x * p.z), 2 * (q.x * p.y - q.y * p.x)); + return make_float3(position.x + p.x + q.w * t.x + q.y * t.z - q.z * t.y, + position.y + p.y + q.w * t.y + q.z * t.x - q.x * t.z, + position.z + p.z + q.w * t.z + q.x * t.y - q.y * t.x); +} +bool near(float3 a, float3 b) { + return near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z); +} +void compare(DEMSolver& solver, const DEMVisualizationScene& scene, DEMVisualizationFrame& frame) { + solver.GetVisualizationFrame(frame, true); + auto snapshot = solver.GetVisualizationSnapshot(); + require(frame.revision == scene.revision, "unexpected geometry invalidation"); + require(scene.spheres.size() == snapshot.spheres.size(), "sphere count mismatch"); + for (size_t i = 0; i < scene.spheres.size(); ++i) { + const auto& s = scene.spheres[i]; + require(near(transform(s.offset, frame.positions[s.owner], frame.orientations[s.owner]), + snapshot.spheres[i].position), + "cached sphere transform mismatch"); + require(near(s.radius, snapshot.spheres[i].radius), "sphere radius mismatch"); + require(frame.families[s.owner] == snapshot.spheres[i].family, "family mismatch"); + } + require(scene.triangles.size() == snapshot.triangles.size(), "triangle count mismatch"); + for (size_t i = 0; i < scene.triangles.size(); ++i) { + const auto& t = scene.triangles[i]; + auto p = frame.positions[t.owner]; + auto q = frame.orientations[t.owner]; + require(near(transform(t.a, p, q), snapshot.triangles[i].a) && + near(transform(t.b, p, q), snapshot.triangles[i].b) && + near(transform(t.c, p, q), snapshot.triangles[i].c), + "cached triangle transform mismatch"); + } +} +} // namespace + +// Exercise rigid motion, combined members, runtime families, same-count deformation, and insertion without a window. +int main() try { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(20, 20, 20); + solver.SetGravitationalAcceleration(make_float3(0)); + solver.SetTimeStepSize(1.e-5); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto clump = solver.LoadClumpType(1, make_float3(1), {0.2f, 0.3f}, + {make_float3(-0.4f, 0, 0), make_float3(0.4f, 0, 0)}, material); + auto batch = solver.AddClumps(clump, std::vector{make_float3(1, 2, 3)}); + auto track = solver.Track(batch); + auto sphere = solver.LoadSphereType(1, 0.2f, material); + auto combined_type = + solver.LoadCombinedClumpType({sphere, sphere}, {make_float3(-0.5f, 0, 0), make_float3(0.5f, 0, 0)}, {}, 0); + auto combined = solver.AddCombinedFromTemplate(combined_type, make_float3(-3, 0, 2), make_float4(0, 0, 0, 1)); + auto combined_track = solver.Track(combined); + auto mesh = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + mesh->Scale(0.1f); + mesh->SetFamily(1); + solver.SetFamilyFixed(1); + auto mesh_track = solver.Track(mesh); + solver.Initialize(); + auto scene = solver.GetVisualizationScene(); + DEMVisualizationFrame frame; + compare(solver, scene, frame); + auto* storage = frame.positions.data(); + track->SetOriQ(make_float4(0, 0, std::sqrt(0.5f), std::sqrt(0.5f))); + track->SetPos(make_float3(2, 1, 3)); + track->SetFamily(7); + track->SetVel(make_float3(0.1f, 0.2f, 0.3f)); + combined_track->SetOriQ(make_float4(0, 0, std::sqrt(0.5f), std::sqrt(0.5f))); + compare(solver, scene, frame); + require(frame.positions.data() == storage, "unchanged frame reallocated owner storage"); + auto owner = track->GetOwnerID(); + require(near(frame.velocities[owner], make_float3(0.1f, 0.2f, 0.3f)), "velocity field mismatch"); + solver.GetVisualizationFrame(frame, false); + require(frame.velocities.empty(), "unrequested velocities retained"); + solver.DoDynamicsThenSync(1.e-4); + compare(solver, scene, frame); + auto nodes = mesh->m_vertices; + for (auto& p : nodes) + p.z += 0.3f; + solver.SetTriNodeRelPos(mesh_track->GetOwnerID(), 0, nodes); + solver.GetVisualizationFrame(frame); + require(frame.revision != scene.revision, "deformation did not invalidate same-count geometry"); + scene = solver.GetVisualizationScene(); + compare(solver, scene, frame); + size_t count = scene.spheres.size(); + auto revision = scene.revision; + solver.ClearCache(); + solver.AddClumps(clump, std::vector{make_float3(5, 3, 2)}); + solver.Update(); + scene = solver.GetVisualizationScene(); + require(scene.revision != revision && scene.spheres.size() == count + 2, "insertion did not refresh geometry"); + compare(solver, scene, frame); + std::cout << "PASS: cached scene/frame transforms, families, combined owners, deformation, insertion.\n"; + return 0; +} catch (const std::exception& e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp b/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp new file mode 100644 index 00000000..caec708e --- /dev/null +++ b/src/demo/ModularTests/DEMTest_VisualizationSnapshot.cpp @@ -0,0 +1,132 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include +#include +#include + +#include "DEM/API.h" + +using namespace deme; + +namespace { + +bool near(float actual, float expected, float tolerance = 1.0e-5f) { + return std::abs(actual - expected) <= tolerance; +} + +} // namespace + +// Verify that visualization snapshots expose current transformed geometry and honor category filtering. +int main() { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(8.0f, 8.0f, 8.0f); + + auto material = solver.LoadMaterial({{"E", 1.0e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto sphere = solver.LoadSphereType(1.0f, 0.25f, material); + solver.AddClumps(sphere, std::vector{make_float3(1.0f, 2.0f, 3.0f)}); + + auto mesh = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + mesh->Scale(0.1f); + mesh->SetFamily(1); + mesh->AddGeometryWildcard("mesh_test_value", 7.0f); + solver.SetMeshOutputContent({"QUAT", "ABSV", "VEL", "ANG_VEL", "ABS_ACC", "ACC", "ANG_ACC", "FAMILY", "MAT", + "GEO_WILDCARD", "OWNER", "MESH_ID", "TRI_ID", "PATCH_ID"}); + solver.SetFamilyFixed(1); + solver.AddBCPlane(make_float3(0, 0, 0), make_float3(0, 0, 1), material); + solver.Initialize(); + + const auto sphere_snapshot = solver.GetVisualizationSnapshot(true, false); + if (sphere_snapshot.spheres.size() != 1 || !sphere_snapshot.triangles.empty()) { + std::cerr << "FAIL: sphere-only snapshot returned the wrong geometry categories." << std::endl; + return 1; + } + const auto& rendered_sphere = sphere_snapshot.spheres.front(); + if (!near(rendered_sphere.position.x, 1.0f) || !near(rendered_sphere.position.y, 2.0f) || + !near(rendered_sphere.position.z, 3.0f) || !near(rendered_sphere.radius, 0.25f)) { + std::cerr << "FAIL: visualization sphere does not match its initialized world transform." << std::endl; + return 1; + } + + // Exercise the public asynchronous writer and check the core legacy-VTK sections that ParaView consumes. + const auto vtk_path = std::filesystem::temp_directory_path() / + ("deme_sphere_output_" + std::to_string(reinterpret_cast(&solver)) + ".vtk"); + solver.SetOutputFormat(OUTPUT_FORMAT::VTK); + solver.WriteSphereFile(vtk_path); + solver.WaitForPendingOutput(); + std::ifstream vtk_file(vtk_path); + std::ostringstream vtk_contents; + vtk_contents << vtk_file.rdbuf(); + std::filesystem::remove(vtk_path); + const std::string vtk_text = vtk_contents.str(); + if (vtk_text.find("DATASET POLYDATA") == std::string::npos || + vtk_text.find("POINTS 1 float") == std::string::npos || + vtk_text.find("SCALARS r float 1") == std::string::npos) { + std::cerr << "FAIL: VTK sphere output is missing required ParaView point-data sections." << std::endl; + return 1; + } + + const auto analytical_vtk_path = + std::filesystem::temp_directory_path() / + ("deme_analytical_output_" + std::to_string(reinterpret_cast(&solver)) + ".vtk"); + solver.WriteAnalyticalFile(analytical_vtk_path); + solver.WaitForPendingOutput(); + std::ifstream analytical_vtk_file(analytical_vtk_path); + std::ostringstream analytical_vtk_contents; + analytical_vtk_contents << analytical_vtk_file.rdbuf(); + std::filesystem::remove(analytical_vtk_path); + const std::string analytical_vtk_text = analytical_vtk_contents.str(); + if (analytical_vtk_text.find("DATASET POLYDATA") == std::string::npos || + analytical_vtk_text.find("POLYGONS 2 8") == std::string::npos || + analytical_vtk_text.find("SCALARS component_type int 1") == std::string::npos) { + std::cerr << "FAIL: analytical VTK output is missing the clipped plane surface or metadata." << std::endl; + return 1; + } + + const auto mesh_vtk_path = + std::filesystem::temp_directory_path() / + ("deme_mesh_output_" + std::to_string(reinterpret_cast(&solver)) + ".vtk"); + solver.WriteMeshFile(mesh_vtk_path); + solver.WaitForPendingOutput(); + std::ifstream mesh_vtk_file(mesh_vtk_path); + std::ostringstream mesh_vtk_contents; + mesh_vtk_contents << mesh_vtk_file.rdbuf(); + std::filesystem::remove(mesh_vtk_path); + const std::string mesh_vtk_text = mesh_vtk_contents.str(); + const std::vector required_mesh_fields = {"CELL_DATA 2", + "SCALARS quaternion float 4", + "SCALARS absv float 1", + "VECTORS velocity float", + "VECTORS angular_velocity float", + "SCALARS abs_acc float 1", + "VECTORS acceleration float", + "VECTORS angular_acceleration float", + "SCALARS family int 1", + "SCALARS material int 1", + "SCALARS owner int 1", + "SCALARS mesh_id int 1", + "SCALARS tri_id int 1", + "SCALARS patch_id int 1", + "SCALARS mesh_test_value float 1"}; + for (const auto& required_field : required_mesh_fields) { + if (mesh_vtk_text.find(required_field) == std::string::npos) { + std::cerr << "FAIL: mesh VTK output is missing requested CELL_DATA field: " << required_field << std::endl; + return 1; + } + } + + const auto triangle_snapshot = solver.GetVisualizationSnapshot(false, true); + if (!triangle_snapshot.spheres.empty() || triangle_snapshot.triangles.size() != mesh->GetNumTriangles()) { + std::cerr << "FAIL: triangle-only snapshot returned the wrong geometry categories." << std::endl; + return 1; + } + + std::cout << "PASS: visualization snapshots expose and filter current solver geometry." << std::endl; + return 0; +} diff --git a/src/demo/ModularTests/DEMTest_Visualizer.cpp b/src/demo/ModularTests/DEMTest_Visualizer.cpp new file mode 100644 index 00000000..71febbcf --- /dev/null +++ b/src/demo/ModularTests/DEMTest_Visualizer.cpp @@ -0,0 +1,144 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include +#include +#include +#include +#include +#include + +#include "DEM/API.h" +#include "../VisualizerDemoLoop.h" +#include "DEM/utils/DEMVisualizer.h" + +using namespace deme; +namespace { +void require(bool condition, const char* message) { + if (!condition) + throw std::runtime_error(message); +} +} // namespace + +// Requires a display and CUDA. Exercise actual shaders, depth picking, filters, UI, resize, and resource recreation. +int main(int argc, char** argv) try { + DEMSolver solver(1); + solver.SetVerbosity("ERROR"); + solver.InstructBoxDomainDimension(20, 20, 20); + solver.SetGravitationalAcceleration(make_float3(0)); + auto material = solver.LoadMaterial({{"E", 1.e7}, {"nu", 0.3}, {"CoR", 0.5}, {"mu", 0.4}, {"Crr", 0.0}}); + auto type = solver.LoadSphereType(1, 0.5f, material); + auto sphere = solver.AddClumps(type, std::vector{make_float3(0)}); + auto track = solver.Track(sphere); + auto mesh = solver.AddWavefrontMeshObject((GET_DATA_PATH() / "mesh/plane_20by20.obj").string(), material); + mesh->Scale(0.15f); + mesh->SetInitPos(make_float3(0, 1, 0)); + mesh->SetInitQuat(make_float4(std::sqrt(0.5f), 0, 0, std::sqrt(0.5f))); + mesh->SetFamily(1); + solver.SetFamilyFixed(1); + auto mesh_track = solver.Track(mesh); + solver.Initialize(); + DEMVisualizer viewer(solver); + viewer.SetWindowSize(1000, 700); + viewer.SetTargetFPS(120); + viewer.SetCameraPosition(make_float3(0, -5, 0)); + viewer.SetCameraTarget(make_float3(0)); + viewer.Initialize(); + viewer.Render(); + viewer.Render(); + require(viewer.ShouldStep(), "initial stepping disabled"); + viewer.SetPaused(true); + require(!viewer.ShouldStep(), "pause did not stop step requests"); + viewer.RequestStep(); + require(viewer.ShouldStep() && !viewer.ShouldStep(), "single step was not consumed exactly once"); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == track->GetOwnerID() && viewer.IsSelectedSphere(), + "front sphere picking failed"); + // Pose and family changes must reach the existing GPU buffers without rebuilding scene geometry. + track->SetPos(make_float3(2, 0, 0)); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == mesh_track->GetOwnerID(), "owner pose upload did not move the sphere"); + track->SetPos(make_float3(0)); + track->SetFamily(7); + viewer.SetFamilyVisible(7, false); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == mesh_track->GetOwnerID(), "runtime family filter did not update"); + track->SetFamily(0); + viewer.SetFamilyColor(0, {80, 160, 230, 0}); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == mesh_track->GetOwnerID(), "zero-alpha geometry remained pickable"); + viewer.SetFamilyColor(0, {80, 160, 230, 255}); + viewer.SetFamilyVisible(0, false); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == mesh_track->GetOwnerID() && !viewer.IsSelectedSphere(), + "hidden sphere occluded mesh picking"); + // Same-count deformation must invalidate mesh buffers; move the backdrop out of the center ray and restore it. + auto nodes = mesh->m_vertices; + auto shifted = nodes; + for (auto& p : shifted) + p.x += 5; + solver.SetTriNodeRelPos(mesh_track->GetOwnerID(), 0, shifted); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == NULL_BODYID, "deformed geometry stayed in the old picking location"); + solver.SetTriNodeRelPos(mesh_track->GetOwnerID(), 0, nodes); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == mesh_track->GetOwnerID(), "restored mesh was not uploaded"); + viewer.SetRenderTriangles(false); + viewer.Render(); + viewer.PickAt(330, 350); + require(viewer.GetSelectedOwner() == NULL_BODYID, "background picking did not clear selection"); + viewer.SetRenderTriangles(true); + viewer.SetFamilyVisible(0, true); + viewer.SetColorMode(DEMVisualizerColorMode::SPEED); + viewer.Render(); + viewer.SetColorMode(DEMVisualizerColorMode::HEIGHT); + viewer.Render(); + viewer.SetColorMode(DEMVisualizerColorMode::FAMILY); + viewer.SetWindowSize(1200, 800); + viewer.Render(); + viewer.Render(); + viewer.PickAt(430, 400); + require(viewer.GetSelectedOwner() == track->GetOwnerID(), "picking after resize failed"); + viewer.FrameSelected(); + viewer.Render(); + viewer.FrameAll(); + viewer.Render(); + if (argc > 1) { + viewer.RequestScreenshot(argv[1]); + viewer.Render(); + require(std::filesystem::is_regular_file(argv[1]), "screenshot was not saved at the requested path"); + } + // Output need not occur at all while the timestep loop services wall-clock display deadlines. + viewer.SetPaused(false); + // A slow FPS limiter must not force a redraw on every solver step. + viewer.SetTargetFPS(30); + VisualizerDemoLoop loop; + const double initial_time = solver.GetSimTime(); + const float dt = static_cast(solver.GetTimeStepSize()); + int redraws = 0; + for (int i = 0; i < 20; ++i) { + redraws += loop.Update(viewer); + solver.DoDynamics(dt); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + solver.DoDynamicsThenSync(0.0); + require(redraws < 10, "display FPS limiter starved the physics work interval"); + require(redraws >= 4, "wall-clock redraws waited for a simulation output frame"); + require(std::abs(solver.GetSimTime() - initial_time - 20 * static_cast(dt)) < dt * 0.1, + "display refresh changed the number of dynamics steps"); + viewer.Close(); + viewer.Close(); + viewer.Initialize(); + viewer.Render(); + viewer.Close(); + std::cout << "PASS: native rendering, depth picking, filtering, controls, resize and recreation.\n"; + return 0; +} catch (const std::exception& e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; +} diff --git a/src/demo/ModularTests/test_state_finite.py b/src/demo/ModularTests/test_state_finite.py new file mode 100644 index 00000000..cdc89d16 --- /dev/null +++ b/src/demo/ModularTests/test_state_finite.py @@ -0,0 +1,34 @@ +"""Run GPU state guards in fresh processes: python3 test_state_finite.py build/bin/DEMTest_StateFinite.""" + +import os +import tempfile +import subprocess +import sys +from pathlib import Path + + +def main(): + # A trap poisons the child's CUDA context, so never reuse a process between injection cases. + executable = str(Path(sys.argv[1]).resolve()) + # Share discovered headers only within this test run to keep fresh-process GPU cases affordable. + with tempfile.TemporaryDirectory(prefix="deme-state-finite-") as cache_dir: + env = dict(os.environ, DEME_PERSISTENT_JITIFY_CACHE=str(Path(cache_dir) / "headers.bin")) + run_cases(executable, env) + + +def run_cases(executable, env): + for margin in ("auto", "fixed", "no-angular"): + for kind, value in (("healthy", "nan"), ("linear", "nan"), ("angular", "nan"), + ("linear", "inf"), ("angular", "inf")): + result = subprocess.run([executable, kind, margin, value], stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, timeout=600, env=env) + expected = "PASS: healthy no-contact state" if kind == "healthy" else f"Non-finite {kind} velocity" + if expected not in result.stdout or (kind == "healthy" and result.returncode != 0): + raise RuntimeError(f"FAIL: {kind}/{margin}/{value}, exit {result.returncode}\n{result.stdout}") + if kind != "healthy" and result.returncode == 0: + raise RuntimeError(f"FAIL: invalid case returned success: {kind}/{margin}/{value}") + print(f"PASS: {kind}/{margin}/{value}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/src/demo/SampleSbatchScripts/BD.sh b/src/demo/SampleSbatchScripts/BD.sh new file mode 100644 index 00000000..e1c61f9a --- /dev/null +++ b/src/demo/SampleSbatchScripts/BD.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=10000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=BD_RTX2080.out + +./src/demo/DEMdemo_BallDrop + diff --git a/src/demo/SampleSbatchScripts/CP.sh b/src/demo/SampleSbatchScripts/CP.sh new file mode 100644 index 00000000..c6a2fa8f --- /dev/null +++ b/src/demo/SampleSbatchScripts/CP.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=40000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:2 + +#SBATCH --output=CP_RTX2080.out + +./src/demo/DEMdemo_ConePenetration + diff --git a/src/demo/SampleSbatchScripts/Cen.sh b/src/demo/SampleSbatchScripts/Cen.sh new file mode 100644 index 00000000..a811f465 --- /dev/null +++ b/src/demo/SampleSbatchScripts/Cen.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=10000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=Centrifuge_RTX2080.out + +./src/demo/DEMdemo_Centrifuge + diff --git a/src/demo/SampleSbatchScripts/Drum.sh b/src/demo/SampleSbatchScripts/Drum.sh new file mode 100644 index 00000000..6422ea66 --- /dev/null +++ b/src/demo/SampleSbatchScripts/Drum.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=Drum_RTX2080.out + +./src/demo/DEMdemo_RotatingDrum + diff --git a/src/demo/SampleSbatchScripts/FM.sh b/src/demo/SampleSbatchScripts/FM.sh new file mode 100644 index 00000000..42a00cb1 --- /dev/null +++ b/src/demo/SampleSbatchScripts/FM.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=01-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=10000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=FM_RTX2080.out + +./src/demo/DEMdemo_FlexibleMesh + diff --git a/src/demo/SampleSbatchScripts/Ind.sh b/src/demo/SampleSbatchScripts/Ind.sh new file mode 100644 index 00000000..c5bc570b --- /dev/null +++ b/src/demo/SampleSbatchScripts/Ind.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=01-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=10000 +##SBATCH --gres=gpu:v100:2 +#SBATCH --gres=gpu:rtx2080ti:2 + +#SBATCH --output=Indentation_RTX2080.out + +./src/demo/DEMdemo_Indentation + diff --git a/src/demo/SampleSbatchScripts/Mixer.sh b/src/demo/SampleSbatchScripts/Mixer.sh new file mode 100644 index 00000000..c535ab3c --- /dev/null +++ b/src/demo/SampleSbatchScripts/Mixer.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=40000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 +#SBATCH --nodelist=euler16 +#SBATCH --output=Mixer.out + +./src/demo/DEMdemo_Mixer + diff --git a/src/demo/SampleSbatchScripts/P1.sh b/src/demo/SampleSbatchScripts/P1.sh new file mode 100644 index 00000000..9649b02b --- /dev/null +++ b/src/demo/SampleSbatchScripts/P1.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=P1.out + +./src/demo/DEMdemo_GRCPrep_Part1 + diff --git a/src/demo/SampleSbatchScripts/RP.sh b/src/demo/SampleSbatchScripts/RP.sh new file mode 100644 index 00000000..bf84e9c8 --- /dev/null +++ b/src/demo/SampleSbatchScripts/RP.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=RP_RTX2080.out + +./src/demo/DEMdemo_Repose + diff --git a/src/demo/SampleSbatchScripts/SS.sh b/src/demo/SampleSbatchScripts/SS.sh new file mode 100644 index 00000000..788f3644 --- /dev/null +++ b/src/demo/SampleSbatchScripts/SS.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=00-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=4000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=Solar_RTX2080.out + +./src/demo/DEMdemo_SolarSystem + diff --git a/src/demo/SampleSbatchScripts/Shake.sh b/src/demo/SampleSbatchScripts/Shake.sh new file mode 100644 index 00000000..cb3e4c95 --- /dev/null +++ b/src/demo/SampleSbatchScripts/Shake.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=Shake.out + +./src/demo/DEMdemo_Shake + diff --git a/src/demo/SampleSbatchScripts/Sieve.sh b/src/demo/SampleSbatchScripts/Sieve.sh new file mode 100644 index 00000000..ba464aec --- /dev/null +++ b/src/demo/SampleSbatchScripts/Sieve.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=01-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=10000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=Sieve_RTX2080.out + +./src/demo/DEMdemo_Sieve + diff --git a/src/demo/SampleSbatchScripts/TestPack.sh b/src/demo/SampleSbatchScripts/TestPack.sh new file mode 100644 index 00000000..e94321d4 --- /dev/null +++ b/src/demo/SampleSbatchScripts/TestPack.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=research +#SBATCH --time=02-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:gtx1080:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=RollUpIncline_mu=0.25.out + +./src/demo/DEMdemo_TestPack + diff --git a/src/demo/SampleSbatchScripts/WDP.sh b/src/demo/SampleSbatchScripts/WDP.sh new file mode 100644 index 00000000..aa8b5baf --- /dev/null +++ b/src/demo/SampleSbatchScripts/WDP.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=01-12:33:00 +#SBATCH --cpus-per-task=2 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=20000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=WDP_RTX2080.out + +./src/demo/DEMdemo_WheelDPSimplified + diff --git a/src/demo/SampleSbatchScripts/WSS.sh b/src/demo/SampleSbatchScripts/WSS.sh new file mode 100644 index 00000000..67563d2e --- /dev/null +++ b/src/demo/SampleSbatchScripts/WSS.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=06-12:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=40000 +#SBATCH --gres=gpu:rtx2080ti:2 +##SBATCH --gres=gpu:a100:2 +##SBATCH --gres=gpu:gtx1080:2 + +#SBATCH --output=WSS.out + +./src/demo/DEMdemo_WheelSlopeSlip + diff --git a/src/demo/SampleSbatchScripts/elec.sh b/src/demo/SampleSbatchScripts/elec.sh new file mode 100644 index 00000000..0e3b7de2 --- /dev/null +++ b/src/demo/SampleSbatchScripts/elec.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +##SBATCH --partition=anything +#SBATCH --partition=sbel +#SBATCH --time=00-2:33:00 +#SBATCH --cpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH --mem=4000 +##SBATCH --gres=gpu:2 +##SBATCH --gres=gpu:a100:2 +#SBATCH --gres=gpu:rtx2080ti:2 + +#SBATCH --output=ES_RTX2080.out + +./src/demo/DEMdemo_Electrostatic + diff --git a/src/demo/VisualizerDemoLoop.h b/src/demo/VisualizerDemoLoop.h new file mode 100644 index 00000000..19080927 --- /dev/null +++ b/src/demo/VisualizerDemoLoop.h @@ -0,0 +1,35 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#pragma once + +#include + +#include "DEM/utils/DEMVisualizer.h" + +namespace deme { + +// Service input on wall time, independently of simulation output intervals. Call before each small dynamics advance; +// a long blocking solver call still prevents the window thread from handling input during that call. +class VisualizerDemoLoop { + public: + bool Update(DEMVisualizer& viewer) { + const auto now = Clock::now(); + if (now < next_frame || !viewer.Run()) + return false; + // DoDynamics has already joined dT. Capturing its state needs no reset of the contact-detection worker. + viewer.Render(); + // A paused single-step request permits exactly the caller's next dynamics advance. + while (viewer.Run() && !viewer.ShouldStep()) + viewer.Render(); + // Leave a physics work interval after drawing. Render may spend a full frame in its FPS limiter; + // scheduling from the start would then redraw after every single timestep and starve the solver. + next_frame = Clock::now() + std::chrono::milliseconds(16); + return true; + } + + private: + using Clock = std::chrono::steady_clock; + Clock::time_point next_frame{}; +}; + +} // namespace deme diff --git a/src/kernel/CUDAMathHelpers.cuh b/src/kernel/CUDAMathHelpers.cuh index 167bc53d..c16ef292 100644 --- a/src/kernel/CUDAMathHelpers.cuh +++ b/src/kernel/CUDAMathHelpers.cuh @@ -45,7 +45,7 @@ #undef strtok_r #endif -#include +#include "cuda_runtime.h" #ifndef EXIT_WAIVED #define EXIT_WAIVED 2 @@ -54,7 +54,7 @@ using uint = unsigned int; using ushort = unsigned short; -#if !defined(__CUDACC__) && !defined(__HIPCC_RTC__) && !defined(__HIP_DEVICE_COMPILE__) +#ifndef __CUDACC__ //////////////////////////////////////////////////////////////////////////////// // override implementations of CUDA functions //////////////////////////////////////////////////////////////////////////////// @@ -70,13 +70,10 @@ inline float rsqrtf(float x) { #endif #if defined(CUDART_VERSION) && CUDART_VERSION >= 13000 -/////////////////////////////////////////////////// -// New Toolkits -using double4_vec = double4_16a; // or double4_32a, but 32 byte alignment is not good for GPUs before Blackwell - // (128bit --> 256 bit register) +// CUDA 13 split vector aliases by alignment. Keep DEM-Engine's helper signatures stable while selecting the +// 16-byte-aligned variant, which avoids requiring newer 32-byte register alignment assumptions. +using double4_vec = double4_16a; #else -//////////////////////////////////////////////////// -// Old Toolkits using double4_vec = double4; #endif @@ -241,12 +238,8 @@ inline __host__ __device__ int4 operator-(int4& a) { //////////////////////////////////////////////////////////////////////////////// // addition -// Note: HIP provides operator+, operator+= for HIP_vector_type in -// amd_hip_vector_types.h, so we guard vector-vector operators to avoid -// ambiguity. Scalar versions may still be needed. //////////////////////////////////////////////////////////////////////////////// -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float2 operator+(float2 a, float2 b) { return make_float2(a.x + b.x, a.y + b.y); } @@ -254,7 +247,6 @@ inline __host__ __device__ void operator+=(float2& a, float2 b) { a.x += b.x; a.y += b.y; } -#endif inline __host__ __device__ float2 operator+(float2 a, float b) { return make_float2(a.x + b, a.y + b); } @@ -302,7 +294,6 @@ inline __host__ __device__ void operator+=(uint2& a, uint b) { a.y += b; } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float3 operator+(float3 a, float3 b) { return make_float3(a.x + b.x, a.y + b.y, a.z + b.z); } @@ -311,7 +302,6 @@ inline __host__ __device__ void operator+=(float3& a, float3 b) { a.y += b.y; a.z += b.z; } -#endif inline __host__ __device__ float3 operator+(float3 a, float b) { return make_float3(a.x + b, a.y + b, a.z + b); } @@ -365,7 +355,6 @@ inline __host__ __device__ float3 operator+(float b, float3 a) { return make_float3(a.x + b, a.y + b, a.z + b); } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float4 operator+(float4 a, float4 b) { return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); } @@ -375,7 +364,6 @@ inline __host__ __device__ void operator+=(float4& a, float4 b) { a.z += b.z; a.w += b.w; } -#endif inline __host__ __device__ float4 operator+(float4 a, float b) { return make_float4(a.x + b, a.y + b, a.z + b, a.w + b); } @@ -437,7 +425,6 @@ inline __host__ __device__ void operator+=(uint4& a, uint b) { // subtract //////////////////////////////////////////////////////////////////////////////// -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float2 operator-(float2 a, float2 b) { return make_float2(a.x - b.x, a.y - b.y); } @@ -445,7 +432,6 @@ inline __host__ __device__ void operator-=(float2& a, float2 b) { a.x -= b.x; a.y -= b.y; } -#endif inline __host__ __device__ float2 operator-(float2 a, float b) { return make_float2(a.x - b, a.y - b); } @@ -493,7 +479,6 @@ inline __host__ __device__ void operator-=(uint2& a, uint b) { a.y -= b; } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float3 operator-(float3 a, float3 b) { return make_float3(a.x - b.x, a.y - b.y, a.z - b.z); } @@ -502,7 +487,6 @@ inline __host__ __device__ void operator-=(float3& a, float3 b) { a.y -= b.y; a.z -= b.z; } -#endif inline __host__ __device__ float3 operator-(float3 a, float b) { return make_float3(a.x - b, a.y - b, a.z - b); } @@ -555,7 +539,6 @@ inline __host__ __device__ void operator-=(uint3& a, uint b) { a.z -= b; } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float4 operator-(float4 a, float4 b) { return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); } @@ -565,7 +548,6 @@ inline __host__ __device__ void operator-=(float4& a, float4 b) { a.z -= b.z; a.w -= b.w; } -#endif inline __host__ __device__ float4 operator-(float4 a, float b) { return make_float4(a.x - b, a.y - b, a.z - b, a.w - b); } @@ -622,11 +604,8 @@ inline __host__ __device__ void operator-=(uint4& a, uint b) { //////////////////////////////////////////////////////////////////////////////// // multiply -// Note: HIP provides operator*(floatN, floatN) in amd_hip_vector_types.h -// so we guard the vector-to-vector operators to avoid ambiguity //////////////////////////////////////////////////////////////////////////////// -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float2 operator*(float2 a, float2 b) { return make_float2(a.x * b.x, a.y * b.y); } @@ -634,7 +613,6 @@ inline __host__ __device__ void operator*=(float2& a, float2 b) { a.x *= b.x; a.y *= b.y; } -#endif inline __host__ __device__ float2 operator*(float2 a, float b) { return make_float2(a.x * b, a.y * b); } @@ -682,7 +660,6 @@ inline __host__ __device__ void operator*=(uint2& a, uint b) { a.y *= b; } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float3 operator*(float3 a, float3 b) { return make_float3(a.x * b.x, a.y * b.y, a.z * b.z); } @@ -691,7 +668,6 @@ inline __host__ __device__ void operator*=(float3& a, float3 b) { a.y *= b.y; a.z *= b.z; } -#endif inline __host__ __device__ float3 operator*(float3 a, float b) { return make_float3(a.x * b, a.y * b, a.z * b); } @@ -744,7 +720,6 @@ inline __host__ __device__ void operator*=(uint3& a, uint b) { a.z *= b; } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float4 operator*(float4 a, float4 b) { return make_float4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); } @@ -754,7 +729,6 @@ inline __host__ __device__ void operator*=(float4& a, float4 b) { a.z *= b.z; a.w *= b.w; } -#endif inline __host__ __device__ float4 operator*(float4 a, float b) { return make_float4(a.x * b, a.y * b, a.z * b, a.w * b); } @@ -814,10 +788,8 @@ inline __host__ __device__ void operator*=(uint4& a, uint b) { //////////////////////////////////////////////////////////////////////////////// // divide -// Note: HIP provides operator/(floatN, floatN) in amd_hip_vector_types.h //////////////////////////////////////////////////////////////////////////////// -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float2 operator/(float2 a, float2 b) { return make_float2(a.x / b.x, a.y / b.y); } @@ -825,7 +797,6 @@ inline __host__ __device__ void operator/=(float2& a, float2 b) { a.x /= b.x; a.y /= b.y; } -#endif inline __host__ __device__ float2 operator/(float2 a, float b) { return make_float2(a.x / b, a.y / b); } @@ -837,7 +808,6 @@ inline __host__ __device__ float2 operator/(float b, float2 a) { return make_float2(b / a.x, b / a.y); } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float3 operator/(float3 a, float3 b) { return make_float3(a.x / b.x, a.y / b.y, a.z / b.z); } @@ -846,7 +816,6 @@ inline __host__ __device__ void operator/=(float3& a, float3 b) { a.y /= b.y; a.z /= b.z; } -#endif inline __host__ __device__ float3 operator/(float3 a, float b) { return make_float3(a.x / b, a.y / b, a.z / b); } @@ -859,7 +828,6 @@ inline __host__ __device__ float3 operator/(float b, float3 a) { return make_float3(b / a.x, b / a.y, b / a.z); } -#ifndef __HIP_PLATFORM_AMD__ inline __host__ __device__ float4 operator/(float4 a, float4 b) { return make_float4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w); } @@ -869,7 +837,6 @@ inline __host__ __device__ void operator/=(float4& a, float4 b) { a.z /= b.z; a.w /= b.w; } -#endif inline __host__ __device__ float4 operator/(float4 a, float b) { return make_float4(a.x / b, a.y / b, a.z / b, a.w / b); } @@ -1403,8 +1370,6 @@ inline __host__ __device__ float4 operator/(float4 a, double b) { return make_float4(a.x / b, a.y / b, a.z / b, a.w / b); } #if defined(CUDART_VERSION) && CUDART_VERSION >= 13000 -/////////////////////////////////////////////////// -// New Toolkits inline __host__ __device__ double4_vec operator/(double4_vec a, float b) { return make_double4_16a(a.x / b, a.y / b, a.z / b, a.w / b); } @@ -1412,8 +1377,6 @@ inline __host__ __device__ double4_vec operator/(double4_vec a, double b) { return make_double4_16a(a.x / b, a.y / b, a.z / b, a.w / b); } #else -//////////////////////////////////////////////////// -// Old Toolkits inline __host__ __device__ double4 operator/(double4 a, float b) { return make_double4(a.x / b, a.y / b, a.z / b, a.w / b); } @@ -1483,20 +1446,17 @@ inline __host__ __device__ T2 to_real3(const T1& a) { } // Cause an error inside a kernel -#if defined(__HIP_PLATFORM_AMD__) || defined(USE_HIP) +#if defined(__CUDA_ARCH__) || defined(__CUDACC__) #define DEME_ABORT_KERNEL(...) \ { \ printf(__VA_ARGS__); \ __threadfence(); \ - __builtin_trap(); \ + asm volatile("trap;"); \ } #else #define DEME_ABORT_KERNEL(...) \ - { \ - printf(__VA_ARGS__); \ - __threadfence(); \ - asm volatile("trap;"); \ - } + do { \ + } while (0) #endif #endif diff --git a/src/kernel/DEMBinSphereKernels.cu b/src/kernel/DEMBinSphereKernels.cu index b5942ef5..43399474 100644 --- a/src/kernel/DEMBinSphereKernels.cu +++ b/src/kernel/DEMBinSphereKernels.cu @@ -1,6 +1,6 @@ // DEM bin--sphere relations-related custom kernels #include -#include +#include _kernelIncludes_; // If clump templates are jitified, they will be below @@ -8,10 +8,10 @@ _clumpTemplateDefs_; // Definitions of analytical entites are below _analyticalEntityDefs_; -__global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::binsSphereTouches_t* numBinsSphereTouches, - deme::objID_t* numAnalGeoSphereTouches) { +DEME_KERNEL void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::binsSphereTouches_t* numBinsSphereTouches, + deme::objID_t* numAnalGeoSphereTouches) { deme::bodyID_t sphereID = blockIdx.x * blockDim.x + threadIdx.x; if (sphereID < simParams->nSpheresGM) { // Register sphere--analytical geometry contacts @@ -32,7 +32,7 @@ __global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, // Use an input named exactly `sphereID' which is the id of this sphere component { _componentAcqStrat_; - myRadius += granData->marginSize[myOwnerID]; + myRadius += granData->marginSizeSphere[sphereID]; } { @@ -43,42 +43,48 @@ __global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, const float myOriQx = granData->oriQx[myOwnerID]; const float myOriQy = granData->oriQy[myOwnerID]; const float myOriQz = granData->oriQz[myOwnerID]; - applyOriQToVector3(myRelPos.x, myRelPos.y, myRelPos.z, myOriQw, myOriQx, myOriQy, - myOriQz); + applyOriQToVector3(myRelPos, make_float4(myOriQx, myOriQy, myOriQz, myOriQw)); myPosXYZ = ownerXYZ + to_double3(myRelPos); } deme::binsSphereTouches_t numX, numY, numZ; { - // The bin number that I live in (with fractions)? - double myBinX = myPosXYZ.x / simParams->binSize; - double myBinY = myPosXYZ.y / simParams->binSize; - double myBinZ = myPosXYZ.z / simParams->binSize; - // How many bins my radius spans (with fractions)? - double myRadiusSpan = myRadius / simParams->binSize; - // A non-finite position or span makes every comparison in the range clamping - // below evaluate false, which silently assigns this sphere the ENTIRE bin grid - // and turns the per-bin contact sweep into an all-pairs workload that presents - // as a GPU-pegged stall. Erroring out loudly is the only sane option here. - if (!isfinite(myBinX) || !isfinite(myBinY) || !isfinite(myBinZ) || !isfinite(myRadiusSpan)) { + const double invBinSize = simParams->dyn.inv_binSize; + const int nbX = (int)simParams->nbX; + const int nbY = (int)simParams->nbY; + const int nbZ = (int)simParams->nbZ; + + // Non-finite geometry makes float-to-integer bin conversion undefined and can turn contact detection + // into a pathological whole-domain sweep. Surface a diverged simulation before computing bounds. + if (!isfinite(myPosXYZ.x) || !isfinite(myPosXYZ.y) || !isfinite(myPosXYZ.z) || !isfinite(myRadius)) { DEME_ABORT_KERNEL( - "Sphere %u has a non-finite position or contact margin (bin coordinates %f, %f, %f, radius " - "span %f).\nThis usually means the simulation has diverged (a NaN orientation quaternion or " - "angular velocity is a common cause), and relaxing the physics may help, such as decreasing " - "the step size and modifying material properties.\n", - sphereID, myBinX, myBinY, myBinZ, myRadiusSpan); + "Sphere %llu has a non-finite position or contact-margin radius (%f, %f, %f; radius %f).\n" + "This usually means the simulation diverged; consider a smaller step size or less stiff " + "material parameters.\n", + static_cast(sphereID), myPosXYZ.x, myPosXYZ.y, myPosXYZ.z, myRadius); + } + + const deme::AxisBounds bx = axis_bounds(myPosXYZ.x, myRadius, nbX, invBinSize); + if (bx.imax < bx.imin) { + numBinsSphereTouches[sphereID] = 0; + numAnalGeoSphereTouches[sphereID] = 0; + return; } - // printf("myRadius: %f\n", myRadiusSpan); - // Now, figure out how many bins I touch in each direction - numX = ((myBinX + myRadiusSpan < (double)simParams->nbX) ? (unsigned int)(myBinX + myRadiusSpan) - : (unsigned int)simParams->nbX - 1) - - (unsigned int)((myBinX - myRadiusSpan > 0.0) ? myBinX - myRadiusSpan : 0.0) + 1; - numY = ((myBinY + myRadiusSpan < (double)simParams->nbY) ? (unsigned int)(myBinY + myRadiusSpan) - : (unsigned int)simParams->nbY - 1) - - (unsigned int)((myBinY - myRadiusSpan > 0.0) ? myBinY - myRadiusSpan : 0.0) + 1; - numZ = ((myBinZ + myRadiusSpan < (double)simParams->nbZ) ? (unsigned int)(myBinZ + myRadiusSpan) - : (unsigned int)simParams->nbZ - 1) - - (unsigned int)((myBinZ - myRadiusSpan > 0.0) ? myBinZ - myRadiusSpan : 0.0) + 1; + const deme::AxisBounds by = axis_bounds(myPosXYZ.y, myRadius, nbY, invBinSize); + if (by.imax < by.imin) { + numBinsSphereTouches[sphereID] = 0; + numAnalGeoSphereTouches[sphereID] = 0; + return; + } + const deme::AxisBounds bz = axis_bounds(myPosXYZ.z, myRadius, nbZ, invBinSize); + if (bz.imax < bz.imin) { + numBinsSphereTouches[sphereID] = 0; + numAnalGeoSphereTouches[sphereID] = 0; + return; + } + numX = bx.imax - bx.imin + 1; + numY = by.imax - by.imin + 1; + numZ = bz.imax - bz.imin + 1; //// TODO: Add an error message if numX * numY * numZ > MAX(binsSphereTouches_t) } @@ -94,7 +100,7 @@ __global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, unsigned int objFamilyNum = granData->familyID[objBOwner]; unsigned int maskMatID = locateMaskPair((unsigned int)sphFamilyNum, (unsigned int)objFamilyNum); - // If marked no contact, skip ths iteration + // If marked no contact, skip this iteration if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { continue; } @@ -106,27 +112,22 @@ __global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, const float ownerOriQx = granData->oriQx[objBOwner]; const float ownerOriQy = granData->oriQy[objBOwner]; const float ownerOriQz = granData->oriQz[objBOwner]; - float objBRelPosX = objRelPosX[objB]; - float objBRelPosY = objRelPosY[objB]; - float objBRelPosZ = objRelPosZ[objB]; - float objBRotX = objRotX[objB]; - float objBRotY = objRotY[objB]; - float objBRotZ = objRotZ[objB]; - applyOriQToVector3(objBRelPosX, objBRelPosY, objBRelPosZ, ownerOriQw, ownerOriQx, - ownerOriQy, ownerOriQz); - applyOriQToVector3(objBRotX, objBRotY, objBRotZ, ownerOriQw, ownerOriQx, ownerOriQy, - ownerOriQz); - double3 objBPosXYZ = ownerXYZ + make_double3(objBRelPosX, objBRelPosY, objBRelPosZ); + const float4 ownerOriQ = make_float4(ownerOriQx, ownerOriQy, ownerOriQz, ownerOriQw); + float3 objBRelPos = make_float3(objRelPosX[objB], objRelPosY[objB], objRelPosZ[objB]); + float3 objBRot = make_float3(objRotX[objB], objRotY[objB], objRotZ[objB]); + applyOriQToVector3(objBRelPos, ownerOriQ); + applyOriQToVector3(objBRot, ownerOriQ); + double3 objBPosXYZ = ownerXYZ + make_double3(objBRelPos.x, objBRelPos.y, objBRelPos.z); - double overlapDepth; + double overlapDepth, overlapArea; // overlapArea matters not here deme::contact_t contact_type; { double3 cntPnt; // cntPnt here is a placeholder float3 cntNorm; // cntNorm is placeholder too contact_type = checkSphereEntityOverlap( - myPosXYZ, myRadius, objType[objB], objBPosXYZ, make_float3(objBRotX, objBRotY, objBRotZ), - objSize1[objB], objSize2[objB], objSize3[objB], objNormal[objB], granData->marginSize[objBOwner], - cntPnt, cntNorm, overlapDepth); + myPosXYZ, myRadius, objType[objB], objBPosXYZ, objBRot, objSize1[objB], objSize2[objB], + objSize3[objB], objNormal[objB], granData->marginSizeAnalytical[objB], cntPnt, cntNorm, + overlapDepth, overlapArea); } // overlapDepth (which has both entities' full margins) needs to be larger than the smaller one of the two // added margin to be considered in-contact. @@ -142,15 +143,15 @@ __global__ void getNumberOfBinsEachSphereTouches(deme::DEMSimParams* simParams, } } -__global__ void populateBinSphereTouchingPairs(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::binSphereTouchPairs_t* numBinsSphereTouchesScan, - deme::binSphereTouchPairs_t* numAnalGeoSphereTouchesScan, - deme::binID_t* binIDsEachSphereTouches, - deme::bodyID_t* sphereIDsEachBinTouches, - deme::bodyID_t* idGeoA, - deme::bodyID_t* idGeoB, - deme::contact_t* contactType) { +DEME_KERNEL void populateBinSphereTouchingPairs(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::binSphereTouchPairs_t* numBinsSphereTouchesScan, + deme::binSphereTouchPairs_t* numAnalGeoSphereTouchesScan, + deme::binID_t* binIDsEachSphereTouches, + deme::bodyID_t* sphereIDsEachBinTouches, + deme::bodyID_t* idGeoA, + deme::bodyID_t* idGeoB, + deme::contact_t* contactTypePrimitive) { deme::bodyID_t sphereID = blockIdx.x * blockDim.x + threadIdx.x; if (sphereID < simParams->nSpheresGM) { double3 myPosXYZ; @@ -169,7 +170,7 @@ __global__ void populateBinSphereTouchingPairs(deme::DEMSimParams* simParams, // Use an input named exactly `sphereID' which is the id of this sphere component { _componentAcqStrat_; - myRadius += granData->marginSize[myOwnerID]; + myRadius += granData->marginSizeSphere[sphereID]; } // Get the offset of my spot where I should start writing back to the global bin--sphere pair registration @@ -185,106 +186,121 @@ __global__ void populateBinSphereTouchingPairs(deme::DEMSimParams* simParams, const float myOriQx = granData->oriQx[myOwnerID]; const float myOriQy = granData->oriQy[myOwnerID]; const float myOriQz = granData->oriQz[myOwnerID]; - applyOriQToVector3(myRelPos.x, myRelPos.y, myRelPos.z, myOriQw, myOriQx, myOriQy, - myOriQz); + applyOriQToVector3(myRelPos, make_float4(myOriQx, myOriQy, myOriQz, myOriQw)); myPosXYZ = ownerXYZ + to_double3(myRelPos); } - // The bin number that I live in (with fractions)? - double myBinX = myPosXYZ.x / simParams->binSize; - double myBinY = myPosXYZ.y / simParams->binSize; - double myBinZ = myPosXYZ.z / simParams->binSize; - // How many bins my radius spans (with fractions)? - double myRadiusSpan = myRadius / simParams->binSize; - // Now, write the IDs of those bins that I touch, back to the global memory - deme::binID_t thisBinID; - for (deme::binID_t k = (deme::binID_t)((myBinZ - myRadiusSpan > 0.0) ? myBinZ - myRadiusSpan : 0.0); - (k <= (deme::binID_t)(myBinZ + myRadiusSpan)) && (k < simParams->nbZ); k++) { - for (deme::binID_t j = (deme::binID_t)((myBinY - myRadiusSpan > 0.0) ? myBinY - myRadiusSpan : 0.0); - (j <= (deme::binID_t)(myBinY + myRadiusSpan)) && (j < simParams->nbY); j++) { - for (deme::binID_t i = (deme::binID_t)((myBinX - myRadiusSpan > 0.0) ? myBinX - myRadiusSpan : 0.0); - (i <= (deme::binID_t)(myBinX + myRadiusSpan)) && (i < simParams->nbX); i++) { - if (myReportOffset >= myReportOffset_end) { - continue; // No stepping on the next one's domain + const double invBinSize = simParams->dyn.inv_binSize; + const int nbX = (int)simParams->nbX; + const int nbY = (int)simParams->nbY; + const int nbZ = (int)simParams->nbZ; + const int nbXY = nbX * nbY; + + const deme::AxisBounds bx = axis_bounds(myPosXYZ.x, myRadius, nbX, invBinSize); + if (bx.imax < bx.imin) { + return; + } + const deme::AxisBounds by = axis_bounds(myPosXYZ.y, myRadius, nbY, invBinSize); + if (by.imax < by.imin) { + return; + } + const deme::AxisBounds bz = axis_bounds(myPosXYZ.z, myRadius, nbZ, invBinSize); + if (bz.imax < bz.imin) { + return; + } + + const int ix0 = bx.imin; + const int ix1 = bx.imax; + const int iy0 = by.imin; + const int iy1 = by.imax; + const int iz0 = bz.imin; + const int iz1 = bz.imax; + + // Now, write the IDs of those bins that I touch, back to the global memory. + for (int k = iz0; k <= iz1; ++k) { + const int baseZ = k * nbXY; + for (int j = iy0; j <= iy1; ++j) { + const int baseYZ = baseZ + j * nbX; + for (int i = ix0; i <= ix1; ++i) { + // Keep running even if counting/populate mismatch happens, so later cleanup still executes. + if (myReportOffset < myReportOffset_end) { + const deme::binID_t binLin = (deme::binID_t)(baseYZ + i); + binIDsEachSphereTouches[myReportOffset] = binLin; + sphereIDsEachBinTouches[myReportOffset] = sphereID; + ++myReportOffset; } - thisBinID = - binIDFrom3Indices(i, j, k, simParams->nbX, simParams->nbY, simParams->nbZ); - binIDsEachSphereTouches[myReportOffset] = thisBinID; - sphereIDsEachBinTouches[myReportOffset] = sphereID; - myReportOffset++; } } } + // First found that this `not filled' problem can happen in the triangle bin--tri intersection detections // part... Quite peculiar. - for (; myReportOffset < myReportOffset_end; myReportOffset++) { + for (; myReportOffset < myReportOffset_end; ++myReportOffset) { binIDsEachSphereTouches[myReportOffset] = deme::NULL_BINID; sphereIDsEachBinTouches[myReportOffset] = sphereID; } } + // Analytical geometry contacts deme::binSphereTouchPairs_t mySphereGeoReportOffset = numAnalGeoSphereTouchesScan[sphereID]; deme::binSphereTouchPairs_t mySphereGeoReportOffset_end = numAnalGeoSphereTouchesScan[sphereID + 1]; - // Each sphere entity should also check if it overlaps with an analytical boundary-type geometry - for (deme::objID_t objB = 0; objB < simParams->nAnalGM; objB++) { - deme::bodyID_t objBOwner = objOwner[objB]; - // Grab family number from memory (not jitified: b/c family number can change frequently in a sim) - unsigned int objFamilyNum = granData->familyID[objBOwner]; - unsigned int maskMatID = locateMaskPair(sphFamilyNum, objFamilyNum); - // If marked no contact, skip ths iteration - if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { - continue; - } - double3 ownerXYZ; - voxelIDToPosition( - ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[objBOwner], granData->locX[objBOwner], - granData->locY[objBOwner], granData->locZ[objBOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - const float ownerOriQw = granData->oriQw[objBOwner]; - const float ownerOriQx = granData->oriQx[objBOwner]; - const float ownerOriQy = granData->oriQy[objBOwner]; - const float ownerOriQz = granData->oriQz[objBOwner]; - float objBRelPosX = objRelPosX[objB]; - float objBRelPosY = objRelPosY[objB]; - float objBRelPosZ = objRelPosZ[objB]; - float objBRotX = objRotX[objB]; - float objBRotY = objRotY[objB]; - float objBRotZ = objRotZ[objB]; - applyOriQToVector3(objBRelPosX, objBRelPosY, objBRelPosZ, ownerOriQw, ownerOriQx, - ownerOriQy, ownerOriQz); - applyOriQToVector3(objBRotX, objBRotY, objBRotZ, ownerOriQw, ownerOriQx, ownerOriQy, - ownerOriQz); - double3 objBPosXYZ = ownerXYZ + make_double3(objBRelPosX, objBRelPosY, objBRelPosZ); + if (mySphereGeoReportOffset < mySphereGeoReportOffset_end) { + // Each sphere entity should also check if it overlaps with an analytical boundary-type geometry + for (deme::objID_t objB = 0; objB < simParams->nAnalGM; ++objB) { + deme::bodyID_t objBOwner = objOwner[objB]; + // Grab family number from memory (not jitified: because family number can change frequently in a sim) + unsigned int objFamilyNum = granData->familyID[objBOwner]; + unsigned int maskMatID = locateMaskPair(sphFamilyNum, objFamilyNum); + // If marked no contact, skip this iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + double3 ownerXYZ; + voxelIDToPosition( + ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[objBOwner], granData->locX[objBOwner], + granData->locY[objBOwner], granData->locZ[objBOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + const float ownerOriQw = granData->oriQw[objBOwner]; + const float ownerOriQx = granData->oriQx[objBOwner]; + const float ownerOriQy = granData->oriQy[objBOwner]; + const float ownerOriQz = granData->oriQz[objBOwner]; + const float4 ownerOriQ = make_float4(ownerOriQx, ownerOriQy, ownerOriQz, ownerOriQw); + float3 objBRelPos = make_float3(objRelPosX[objB], objRelPosY[objB], objRelPosZ[objB]); + float3 objBRot = make_float3(objRotX[objB], objRotY[objB], objRotZ[objB]); + applyOriQToVector3(objBRelPos, ownerOriQ); + applyOriQToVector3(objBRot, ownerOriQ); + double3 objBPosXYZ = ownerXYZ + make_double3(objBRelPos.x, objBRelPos.y, objBRelPos.z); - double overlapDepth; - deme::contact_t contact_type; - { - double3 cntPnt; // cntPnt here is a placeholder - float3 cntNorm; // cntNorm is placeholder too - contact_type = checkSphereEntityOverlap( - myPosXYZ, myRadius, objType[objB], objBPosXYZ, make_float3(objBRotX, objBRotY, objBRotZ), - objSize1[objB], objSize2[objB], objSize3[objB], objNormal[objB], granData->marginSize[objBOwner], - cntPnt, cntNorm, overlapDepth); - } - // overlapDepth (which has both entities' full margins) needs to be larger than the smaller one of the two - // added margin to be considered in-contact. - double marginThres = - (granData->familyExtraMarginSize[sphFamilyNum] < granData->familyExtraMarginSize[objFamilyNum]) - ? granData->familyExtraMarginSize[sphFamilyNum] - : granData->familyExtraMarginSize[objFamilyNum]; - if (contact_type && overlapDepth > marginThres) { - idGeoA[mySphereGeoReportOffset] = sphereID; - idGeoB[mySphereGeoReportOffset] = (deme::bodyID_t)objB; - contactType[mySphereGeoReportOffset] = contact_type; - mySphereGeoReportOffset++; - if (mySphereGeoReportOffset >= mySphereGeoReportOffset_end) { - return; // Don't step on the next sphere's domain + double overlapDepth, overlapArea; // overlapArea matters not here + deme::contact_t contact_type; + { + double3 cntPnt; // cntPnt here is a placeholder + float3 cntNorm; // cntNorm is placeholder too + contact_type = checkSphereEntityOverlap( + myPosXYZ, myRadius, objType[objB], objBPosXYZ, objBRot, objSize1[objB], objSize2[objB], + objSize3[objB], objNormal[objB], granData->marginSizeAnalytical[objB], cntPnt, cntNorm, + overlapDepth, overlapArea); + } + // overlapDepth (which has both entities' full margins) needs to be larger than the smaller one of the + // two added margin to be considered in-contact. + double marginThres = + (granData->familyExtraMarginSize[sphFamilyNum] < granData->familyExtraMarginSize[objFamilyNum]) + ? granData->familyExtraMarginSize[sphFamilyNum] + : granData->familyExtraMarginSize[objFamilyNum]; + if (contact_type && overlapDepth > marginThres) { + // Keep going on rare count/populate mismatches so trailing slots are still normalized. + if (mySphereGeoReportOffset < mySphereGeoReportOffset_end) { + idGeoA[mySphereGeoReportOffset] = sphereID; + idGeoB[mySphereGeoReportOffset] = (deme::bodyID_t)objB; + contactTypePrimitive[mySphereGeoReportOffset] = contact_type; + ++mySphereGeoReportOffset; + } } } - } - // In practice, I've never seen non-illed contact slots that need to be resolved this way. It's purely for ultra - // safety. - for (; mySphereGeoReportOffset < mySphereGeoReportOffset_end; mySphereGeoReportOffset++) { - contactType[mySphereGeoReportOffset] = deme::NOT_A_CONTACT; + // In practice, I've never seen non-filled contact slots that need to be resolved this way. It's purely for + // ultra safety. + for (; mySphereGeoReportOffset < mySphereGeoReportOffset_end; ++mySphereGeoReportOffset) { + contactTypePrimitive[mySphereGeoReportOffset] = deme::NOT_A_CONTACT; + } } } } diff --git a/src/kernel/DEMBinTriangleKernels.cu b/src/kernel/DEMBinTriangleKernels.cu index c3afcb86..39d6d47e 100644 --- a/src/kernel/DEMBinTriangleKernels.cu +++ b/src/kernel/DEMBinTriangleKernels.cu @@ -1,9 +1,12 @@ // DEM bin--sphere relations-related custom kernels #include -#include +#include #include _kernelIncludes_; +// Definitions of analytical entites are below +_analyticalEntityDefs_; + inline __device__ float3 sandwichVertex(float3 vertex, const float3& incenter, const float3& side, const float3& normal, float beta) { // The vector along which we enlarge the triangle @@ -19,21 +22,41 @@ sandwichVertex(float3 vertex, const float3& incenter, const float3& side, const return vertex; } -__global__ void makeTriangleSandwich(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - float3* sandwichANode1, - float3* sandwichANode2, - float3* sandwichANode3, - float3* sandwichBNode1, - float3* sandwichBNode2, - float3* sandwichBNode3) { +inline __device__ float distSquaredPoint(const float3& p, const float3& c) { + const float dx = p.x - c.x; + const float dy = p.y - c.y; + const float dz = p.z - c.z; + return dx * dx + dy * dy + dz * dz; +} + +inline __device__ void updatePlaneMinMax(const float3& p, + const float3& origin, + const float3& n, + float& min_d, + float& max_d) { + const float3 pg = p - origin; + const float d = dot(pg, n); + min_d = DEME_MIN(min_d, d); + max_d = DEME_MAX(max_d, d); +} + +DEME_KERNEL void makeTriangleSandwich(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + float3* sandwichANode1, + float3* sandwichANode2, + float3* sandwichANode3, + float3* sandwichBNode1) { deme::bodyID_t triID = blockIdx.x * blockDim.x + threadIdx.x; if (triID < simParams->nTriGM) { // Get my component offset info from global array const float3 p1 = granData->relPosNode1[triID]; const float3 p2 = granData->relPosNode2[triID]; const float3 p3 = granData->relPosNode3[triID]; - const deme::bodyID_t myOwnerID = granData->ownerMesh[triID]; + float margin = granData->marginSizeTriangle[triID]; + if (granData->ownerMeshShellHalfThickness) { + const deme::bodyID_t ownerID = granData->ownerTriMesh[triID]; + margin += fmaxf(granData->ownerMeshShellHalfThickness[ownerID], 0.f); + } // Get the incenter of this triangle. // This is because we use the incenter to enalrge a triangle. See for example, this @@ -42,17 +65,120 @@ __global__ void makeTriangleSandwich(deme::DEMSimParams* simParams, // Generate normal using RHR from nodes 1, 2, and 3 float3 triNormal = face_normal(p1, p2, p3); - sandwichANode1[triID] = sandwichVertex(p1, incenter, p2 - p1, triNormal, granData->marginSize[myOwnerID]); - sandwichANode2[triID] = sandwichVertex(p2, incenter, p3 - p2, triNormal, granData->marginSize[myOwnerID]); - sandwichANode3[triID] = sandwichVertex(p3, incenter, p1 - p3, triNormal, granData->marginSize[myOwnerID]); + sandwichANode1[triID] = sandwichVertex(p1, incenter, p2 - p1, triNormal, margin); + sandwichANode2[triID] = sandwichVertex(p2, incenter, p3 - p2, triNormal, margin); + sandwichANode3[triID] = sandwichVertex(p3, incenter, p1 - p3, triNormal, margin); // The other sandwich triangle needs to have an opposite normal direction - sandwichBNode1[triID] = sandwichVertex(p1, incenter, p2 - p1, -triNormal, granData->marginSize[myOwnerID]); - sandwichBNode2[triID] = sandwichVertex(p3, incenter, p1 - p3, -triNormal, granData->marginSize[myOwnerID]); - sandwichBNode3[triID] = sandwichVertex(p2, incenter, p3 - p2, -triNormal, granData->marginSize[myOwnerID]); + sandwichBNode1[triID] = sandwichVertex(p1, incenter, p2 - p1, -triNormal, margin); } } -inline __device__ void figureOutNodeAndBoundingBox(deme::DEMSimParams* simParams, +// Precompute mesh-owner pose (position + rotation matrix rows) once per time step. +// Mesh owners are assumed to occupy the last `nTriMeshes` owner slots: +// [0 .. nOwnerClumps-1] clumps, [.. + nExtObj -1] external objects, [.. + nTriMeshes -1] meshes. +// This matches kT initialization (owner_offset_for_mesh_obj = nExistOwners + nClumps + nExtObj). +DEME_KERNEL void precomputeMeshOwnerPose(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + float3* meshOwnerPos, // length nTriMeshes + float3* meshR1, // length nTriMeshes (row 0) + float3* meshR2, // length nTriMeshes (row 1) + float3* meshR3) { // length nTriMeshes (row 2) + const deme::bodyID_t mesh_i = blockIdx.x * blockDim.x + threadIdx.x; + if (mesh_i >= simParams->nTriMeshes) { + return; + } + const deme::bodyID_t mesh_owner_start = simParams->nOwnerBodies - simParams->nTriMeshes; + const deme::bodyID_t ownerID = mesh_owner_start + mesh_i; + + float3 ownerXYZ; + voxelIDToPosition( + ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[ownerID], granData->locX[ownerID], + granData->locY[ownerID], granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + meshOwnerPos[mesh_i] = ownerXYZ; + + const float qw = granData->oriQw[ownerID]; + const float qx = granData->oriQx[ownerID]; + const float qy = granData->oriQy[ownerID]; + const float qz = granData->oriQz[ownerID]; + + // Convert quaternion to rotation matrix rows (right-handed, active rotation). + const float xx = qx * qx; + const float yy = qy * qy; + const float zz = qz * qz; + const float xy = qx * qy; + const float xz = qx * qz; + const float yz = qy * qz; + const float wx = qw * qx; + const float wy = qw * qy; + const float wz = qw * qz; + + meshR1[mesh_i] = make_float3(1.f - 2.f * (yy + zz), 2.f * (xy - wz), 2.f * (xz + wy)); + meshR2[mesh_i] = make_float3(2.f * (xy + wz), 1.f - 2.f * (xx + zz), 2.f * (yz - wx)); + meshR3[mesh_i] = make_float3(2.f * (xz - wy), 2.f * (yz + wx), 1.f - 2.f * (xx + yy)); +} + +inline __device__ float3 applyRotRows(const float3& v, const float3& r1, const float3& r2, const float3& r3) { + return make_float3(dot(r1, v), dot(r2, v), dot(r3, v)); +} + +// Compute triangle AABB -> bin index bounds using mixed precision (FP32 fast path + FP64 fallback). +// This mirrors the sphere binning approach (axis_bounds) to avoid precision issues when a bound lies +// close to a bin boundary. +inline __device__ bool boundingBoxIntersectBinAxisBounds(deme::binID_t* L, + deme::binID_t* U, + const float3& vA, + const float3& vB, + const float3& vC, + deme::DEMSimParams* simParams) { + float3 min_pt; + min_pt.x = DEME_MIN(vA.x, DEME_MIN(vB.x, vC.x)); + min_pt.y = DEME_MIN(vA.y, DEME_MIN(vB.y, vC.y)); + min_pt.z = DEME_MIN(vA.z, DEME_MIN(vB.z, vC.z)); + + float3 max_pt; + max_pt.x = DEME_MAX(vA.x, DEME_MAX(vB.x, vC.x)); + max_pt.y = DEME_MAX(vA.y, DEME_MAX(vB.y, vC.y)); + max_pt.z = DEME_MAX(vA.z, DEME_MAX(vB.z, vC.z)); + + // Enlarge bounding box, so that no triangle lies right between 2 layers of bins + const float enlarge = (float)DEME_BIN_ENLARGE_RATIO_FOR_FACETS * (float)simParams->dyn.binSize; + min_pt -= enlarge; + max_pt += enlarge; + + const double invBinSize = simParams->dyn.inv_binSize; + const int nbX = (int)simParams->nbX; + const int nbY = (int)simParams->nbY; + const int nbZ = (int)simParams->nbZ; + + // Convert [min,max] to (center, half-range) and use axis_bounds (FP32 fast path with FP64 fallback). + const double cx = 0.5 * ((double)min_pt.x + (double)max_pt.x); + const double rx = 0.5 * ((double)max_pt.x - (double)min_pt.x); + const deme::AxisBounds bx = axis_bounds(cx, rx, nbX, invBinSize); + if (bx.imax < bx.imin) + return false; + + const double cy = 0.5 * ((double)min_pt.y + (double)max_pt.y); + const double ry = 0.5 * ((double)max_pt.y - (double)min_pt.y); + const deme::AxisBounds by = axis_bounds(cy, ry, nbY, invBinSize); + if (by.imax < by.imin) + return false; + + const double cz = 0.5 * ((double)min_pt.z + (double)max_pt.z); + const double rz = 0.5 * ((double)max_pt.z - (double)min_pt.z); + const deme::AxisBounds bz = axis_bounds(cz, rz, nbZ, invBinSize); + if (bz.imax < bz.imin) + return false; + + L[0] = (deme::binID_t)bx.imin; + U[0] = (deme::binID_t)bx.imax; + L[1] = (deme::binID_t)by.imin; + U[1] = (deme::binID_t)by.imax; + L[2] = (deme::binID_t)bz.imin; + U[2] = (deme::binID_t)bz.imax; + return true; +} + +inline __device__ bool figureOutNodeAndBoundingBox(deme::DEMSimParams* simParams, deme::DEMDataKT* granData, const deme::bodyID_t& triID, float3& vA, @@ -64,139 +190,515 @@ inline __device__ void figureOutNodeAndBoundingBox(deme::DEMSimParams* simParams float3 loc_vB, float3 loc_vC) { // My sphere voxel ID and my relPos - deme::bodyID_t myOwnerID = granData->ownerMesh[triID]; + deme::bodyID_t myOwnerID = granData->ownerTriMesh[triID]; - double3 ownerXYZ; - voxelIDToPosition( + float3 ownerXYZ; + voxelIDToPosition( ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[myOwnerID], granData->locX[myOwnerID], granData->locY[myOwnerID], granData->locZ[myOwnerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); const float myOriQw = granData->oriQw[myOwnerID]; const float myOriQx = granData->oriQx[myOwnerID]; const float myOriQy = granData->oriQy[myOwnerID]; const float myOriQz = granData->oriQz[myOwnerID]; - applyOriQToVector3(loc_vA.x, loc_vA.y, loc_vA.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(loc_vB.x, loc_vB.y, loc_vB.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(loc_vC.x, loc_vC.y, loc_vC.z, myOriQw, myOriQx, myOriQy, myOriQz); + const float4 myOriQ = make_float4(myOriQx, myOriQy, myOriQz, myOriQw); + applyOriQToVector3(loc_vA, myOriQ); + applyOriQToVector3(loc_vB, myOriQ); + applyOriQToVector3(loc_vC, myOriQ); vA = ownerXYZ + loc_vA; vB = ownerXYZ + loc_vB; vC = ownerXYZ + loc_vC; - boundingBoxIntersectBin(L, U, vA, vB, vC, simParams); + return boundingBoxIntersectBinAxisBounds(L, U, vA, vB, vC, simParams); } -__global__ void getNumberOfBinsEachTriangleTouches(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::binsTriangleTouches_t* numBinsTriTouches, - float3* nodeA1, - float3* nodeB1, - float3* nodeC1, - float3* nodeA2, - float3* nodeB2, - float3* nodeC2) { +DEME_KERNEL void precomputeTriangleSandwichData( + deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + // World-space vertices for A-face triangle + float3* vA1_all, + float3* vB1_all, + float3* vC1_all, + // Per-triangle translation B = A + shift_world (world-space) + float3* shift_world_all, + // Per-triangle bounds for A and B (only valid if ok flag true) + int3* LA_all, + int3* UA_all, + int3* LB_all, + int3* UB_all, + // ok flags for A and B + unsigned char* ok1_all, + unsigned char* ok2_all, + // Precomputed mesh-owner pose (length nTriMeshes); may be nullptr if no meshes + const float3* meshOwnerPos, + const float3* meshR1, + const float3* meshR2, + const float3* meshR3, + // sandwich nodes (local, as produced by makeTriangleSandwich) + const float3* nodeA1, + const float3* nodeA2, + const float3* nodeA3, + const float3* nodeB1_only) { + const deme::bodyID_t triID = blockIdx.x * blockDim.x + threadIdx.x; + if (triID >= simParams->nTriGM) { + return; + } + + const deme::bodyID_t ownerID = granData->ownerTriMesh[triID]; + const deme::bodyID_t mesh_owner_start = simParams->nOwnerBodies - simParams->nTriMeshes; + + float3 ownerXYZ; + float3 r1, r2, r3; + + // Fast path: mesh owners live in [mesh_owner_start, mesh_owner_start + nTriMeshes) + if (simParams->nTriMeshes > 0 && meshOwnerPos && ownerID >= mesh_owner_start && + ownerID < (mesh_owner_start + (deme::bodyID_t)simParams->nTriMeshes)) { + const deme::bodyID_t mi = ownerID - mesh_owner_start; + ownerXYZ = meshOwnerPos[mi]; + r1 = meshR1[mi]; + r2 = meshR2[mi]; + r3 = meshR3[mi]; + } else { + // Fallback: compute pose directly + voxelIDToPosition( + ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[ownerID], granData->locX[ownerID], + granData->locY[ownerID], granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + + const float qw = granData->oriQw[ownerID]; + const float qx = granData->oriQx[ownerID]; + const float qy = granData->oriQy[ownerID]; + const float qz = granData->oriQz[ownerID]; + + const float xx = qx * qx; + const float yy = qy * qy; + const float zz = qz * qz; + const float xy = qx * qy; + const float xz = qx * qz; + const float yz = qy * qz; + const float wx = qw * qx; + const float wy = qw * qy; + const float wz = qw * qz; + + r1 = make_float3(1.f - 2.f * (yy + zz), 2.f * (xy - wz), 2.f * (xz + wy)); + r2 = make_float3(2.f * (xy + wz), 1.f - 2.f * (xx + zz), 2.f * (yz - wx)); + r3 = make_float3(2.f * (xz - wy), 2.f * (yz + wx), 1.f - 2.f * (xx + yy)); + } + + // Transform A-face nodes to world. + const float3 lA1 = nodeA1[triID]; + const float3 lA2 = nodeA2[triID]; + const float3 lA3 = nodeA3[triID]; + + const float3 vA1 = ownerXYZ + applyRotRows(lA1, r1, r2, r3); + const float3 vB1 = ownerXYZ + applyRotRows(lA2, r1, r2, r3); + const float3 vC1 = ownerXYZ + applyRotRows(lA3, r1, r2, r3); + + vA1_all[triID] = vA1; + vB1_all[triID] = vB1; + vC1_all[triID] = vC1; + + // Compute shift_world from B1 - A1 in local, then rotate (no translation). + const float3 lB1 = nodeB1_only[triID]; + float3 shift_local = make_float3(lB1.x - lA1.x, lB1.y - lA1.y, lB1.z - lA1.z); + const float3 shift_world = applyRotRows(shift_local, r1, r2, r3); + shift_world_all[triID] = shift_world; + + // Compute bounds for A and B (B is reconstructed from A + shift; note permutation for opposite normal). + deme::binID_t L1[3], U1[3], L2[3], U2[3]; + const bool ok1 = boundingBoxIntersectBinAxisBounds(L1, U1, vA1, vB1, vC1, simParams); + + const float3 vA2 = vA1 + shift_world; + const float3 vB2 = vC1 + shift_world; // swap 2<->3 for inverted normal + const float3 vC2 = vB1 + shift_world; + const bool ok2 = boundingBoxIntersectBinAxisBounds(L2, U2, vA2, vB2, vC2, simParams); + + ok1_all[triID] = (unsigned char)(ok1 ? 1 : 0); + ok2_all[triID] = (unsigned char)(ok2 ? 1 : 0); + + if (ok1) { + LA_all[triID] = make_int3(L1[0], L1[1], L1[2]); + UA_all[triID] = make_int3(U1[0], U1[1], U1[2]); + } + if (ok2) { + LB_all[triID] = make_int3(L2[0], L2[1], L2[2]); + UB_all[triID] = make_int3(U2[0], U2[1], U2[2]); + } +} + +DEME_KERNEL void getNumberOfBinsEachTriangleTouches(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::binsTriangleTouches_t* numBinsTriTouches, + deme::objID_t* numAnalGeoTriTouches, + // precomputed + const float3* vA1_all, + const float3* vB1_all, + const float3* vC1_all, + const float3* shift_world_all, + const int3* LA_all, + const int3* UA_all, + const int3* LB_all, + const int3* UB_all, + const unsigned char* ok1_all, + const unsigned char* ok2_all, + bool meshUniversalContact) { deme::bodyID_t triID = blockIdx.x * blockDim.x + threadIdx.x; - if (triID < simParams->nTriGM) { - // 3 vertices of the triangle - float3 vA1, vB1, vC1, vA2, vB2, vC2; - deme::binID_t L1[3], L2[3], U1[3], U2[3]; - figureOutNodeAndBoundingBox(simParams, granData, triID, vA1, vB1, vC1, L1, U1, nodeA1[triID], nodeB1[triID], - nodeC1[triID]); - figureOutNodeAndBoundingBox(simParams, granData, triID, vA2, vB2, vC2, L2, U2, nodeA2[triID], nodeB2[triID], - nodeC2[triID]); - L1[0] = DEME_MIN(L1[0], L2[0]); - L1[1] = DEME_MIN(L1[1], L2[1]); - L1[2] = DEME_MIN(L1[2], L2[2]); - U1[0] = DEME_MAX(U1[0], U2[0]); - U1[1] = DEME_MAX(U1[1], U2[1]); - U1[2] = DEME_MAX(U1[2], U2[2]); - - unsigned int numSDsTouched = 0; - // Triangle may span a collection of bins... - // BTW, I don't know why Chrono::GPU had to check the so-called 3 cases, and create thread divergence like that. - // Just sweep through all potential bins and you are fine. - float BinCenter[3]; - float BinHalfSizes[3]; - BinHalfSizes[0] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - BinHalfSizes[1] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - BinHalfSizes[2] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - for (deme::binID_t i = L1[0]; i <= U1[0]; i++) { - for (deme::binID_t j = L1[1]; j <= U1[1]; j++) { - for (deme::binID_t k = L1[2]; k <= U1[2]; k++) { - BinCenter[0] = simParams->binSize * i + simParams->binSize / 2.; - BinCenter[1] = simParams->binSize * j + simParams->binSize / 2.; - BinCenter[2] = simParams->binSize * k + simParams->binSize / 2.; - - if (check_TriangleBoxOverlap(BinCenter, BinHalfSizes, vA1, vB1, vC1) || - check_TriangleBoxOverlap(BinCenter, BinHalfSizes, vA2, vB2, vC2)) { + if (triID >= simParams->nTriGM) { + return; + } + + const bool ok1 = (ok1_all[triID] != 0); + const bool ok2 = (ok2_all[triID] != 0); + + if (!ok1 && !ok2) { + numBinsTriTouches[triID] = 0; + if (meshUniversalContact) { + numAnalGeoTriTouches[triID] = 0; + } + return; + } + + const float3 vA1 = vA1_all[triID]; + const float3 vB1 = vB1_all[triID]; + const float3 vC1 = vC1_all[triID]; + const float3 shift_world = shift_world_all[triID]; + const float3 vA2 = vA1 + shift_world; + const float3 vB2 = vC1 + shift_world; // swapped + const float3 vC2 = vB1 + shift_world; + + int3 LA = make_int3(0, 0, 0), UA = make_int3(-1, -1, -1); + int3 LB = make_int3(0, 0, 0), UB = make_int3(-1, -1, -1); + if (ok1) { + LA = LA_all[triID]; + UA = UA_all[triID]; + } + if (ok2) { + LB = LB_all[triID]; + UB = UB_all[triID]; + } + + // Union bounds + deme::binID_t Lx, Ly, Lz, Ux, Uy, Uz; + if (ok1 && ok2) { + Lx = (deme::binID_t)DEME_MIN(LA.x, LB.x); + Ly = (deme::binID_t)DEME_MIN(LA.y, LB.y); + Lz = (deme::binID_t)DEME_MIN(LA.z, LB.z); + Ux = (deme::binID_t)DEME_MAX(UA.x, UB.x); + Uy = (deme::binID_t)DEME_MAX(UA.y, UB.y); + Uz = (deme::binID_t)DEME_MAX(UA.z, UB.z); + } else if (ok1) { + Lx = (deme::binID_t)LA.x; + Ly = (deme::binID_t)LA.y; + Lz = (deme::binID_t)LA.z; + Ux = (deme::binID_t)UA.x; + Uy = (deme::binID_t)UA.y; + Uz = (deme::binID_t)UA.z; + } else { + Lx = (deme::binID_t)LB.x; + Ly = (deme::binID_t)LB.y; + Lz = (deme::binID_t)LB.z; + Ux = (deme::binID_t)UB.x; + Uy = (deme::binID_t)UB.y; + Uz = (deme::binID_t)UB.z; + } + + unsigned int numSDsTouched = 0; + const float binSizeF = (float)simParams->dyn.binSize; + const float binHalfSpan = binSizeF * (0.5f + (float)DEME_BIN_ENLARGE_RATIO_FOR_FACETS); + const float startX = binSizeF * (float)Lx + 0.5f * binSizeF; + const float startY = binSizeF * (float)Ly + 0.5f * binSizeF; + const float startZ = binSizeF * (float)Lz + 0.5f * binSizeF; + + // Incremental bin-local coordinates: avoid recomputing (v - c) for every bin. + for (deme::binID_t i = Lx, ix = 0; i <= Ux; i++, ix++) { + const float cx = startX + (float)ix * binSizeF; + + const float a0x = vA1.x - cx; + const float a1x = vB1.x - cx; + const float a2x = vC1.x - cx; + + float cy = startY; + for (deme::binID_t j = Ly; j <= Uy; j++) { + const float a0y = vA1.y - cy; + const float a1y = vB1.y - cy; + const float a2y = vC1.y - cy; + + float3 a0 = make_float3(a0x, a0y, vA1.z - startZ); + float3 a1 = make_float3(a1x, a1y, vB1.z - startZ); + float3 a2 = make_float3(a2x, a2y, vC1.z - startZ); + + for (deme::binID_t k = Lz; k <= Uz; k++) { + const bool inA = + ok1 && (i >= (deme::binID_t)LA.x && i <= (deme::binID_t)UA.x && j >= (deme::binID_t)LA.y && + j <= (deme::binID_t)UA.y && k >= (deme::binID_t)LA.z && k <= (deme::binID_t)UA.z); + const bool inB = + ok2 && (i >= (deme::binID_t)LB.x && i <= (deme::binID_t)UB.x && j >= (deme::binID_t)LB.y && + j <= (deme::binID_t)UB.y && k >= (deme::binID_t)LB.z && k <= (deme::binID_t)UB.z); + + if (inA || inB) { + const bool hit = + triBoxOverlapBinLocalEdgesUnionShiftFP32(a0, a1, a2, shift_world, binHalfSpan, inA, inB); + if (hit) { numSDsTouched++; } } + + // Advance bin center in +Z => (v.z - cz) decreases. + a0.z -= binSizeF; + a1.z -= binSizeF; + a2.z -= binSizeF; } + cy += binSizeF; } - numBinsTriTouches[triID] = numSDsTouched; + } + numBinsTriTouches[triID] = numSDsTouched; + + if (meshUniversalContact) { + deme::objID_t contact_count = 0; + for (deme::objID_t objB = 0; objB < simParams->nAnalGM; objB++) { + deme::bodyID_t objBOwner = objOwner[objB]; + unsigned int objFamilyNum = granData->familyID[objBOwner]; + deme::bodyID_t triOwnerID = granData->ownerTriMesh[triID]; + unsigned int triFamilyNum = granData->familyID[triOwnerID]; + unsigned int maskMatID = locateMaskPair(triFamilyNum, objFamilyNum); + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + float3 ownerXYZ; + voxelIDToPosition( + ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[objBOwner], granData->locX[objBOwner], + granData->locY[objBOwner], granData->locZ[objBOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + + const float ownerOriQw = granData->oriQw[objBOwner]; + const float ownerOriQx = granData->oriQx[objBOwner]; + const float ownerOriQy = granData->oriQy[objBOwner]; + const float ownerOriQz = granData->oriQz[objBOwner]; + + const float4 ownerOriQ = make_float4(ownerOriQx, ownerOriQy, ownerOriQz, ownerOriQw); + float3 objBRelPos = make_float3(objRelPosX[objB], objRelPosY[objB], objRelPosZ[objB]); + float3 objBRot = make_float3(objRotX[objB], objRotY[objB], objRotZ[objB]); + + applyOriQToVector3(objBRelPos, ownerOriQ); + applyOriQToVector3(objBRot, ownerOriQ); + + float3 objBPosXYZ = ownerXYZ + objBRelPos; + + deme::contact_t contact_type = checkTriEntityOverlapFP32( + vA1, vB1, vC1, objType[objB], objBPosXYZ, objBRot, objSize1[objB], objSize2[objB], objSize3[objB], + objNormal[objB], granData->marginSizeAnalytical[objB]); + + if (contact_type == deme::NOT_A_CONTACT) { + contact_type = checkTriEntityOverlapFP32(vA2, vB2, vC2, objType[objB], objBPosXYZ, objBRot, + objSize1[objB], objSize2[objB], objSize3[objB], + objNormal[objB], granData->marginSizeAnalytical[objB]); + } + + if (contact_type == deme::TRIANGLE_ANALYTICAL_CONTACT) { + contact_count++; + } + } + numAnalGeoTriTouches[triID] = contact_count; } } -__global__ void populateBinTriangleTouchingPairs(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::binsTriangleTouchPairs_t* numBinsTriTouchesScan, - deme::binID_t* binIDsEachTriTouches, - deme::bodyID_t* triIDsEachBinTouches, - float3* nodeA1, - float3* nodeB1, - float3* nodeC1, - float3* nodeA2, - float3* nodeB2, - float3* nodeC2) { +DEME_KERNEL void populateBinTriangleTouchingPairs(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::binsTriangleTouchPairs_t* numBinsTriTouchesScan, + deme::binsTriangleTouchPairs_t* numAnalGeoTriTouchesScan, + deme::binID_t* binIDsEachTriTouches, + deme::bodyID_t* triIDsEachBinTouches, + // precomputed + const float3* vA1_all, + const float3* vB1_all, + const float3* vC1_all, + const float3* shift_world_all, + const int3* LA_all, + const int3* UA_all, + const int3* LB_all, + const int3* UB_all, + const unsigned char* ok1_all, + const unsigned char* ok2_all, + // tri-anal output + deme::bodyID_t* idGeoA, + deme::bodyID_t* idGeoB, + deme::contact_t* contactTypePrimitive, + bool meshUniversalContact) { deme::bodyID_t triID = blockIdx.x * blockDim.x + threadIdx.x; - if (triID < simParams->nTriGM) { - // 3 vertices of the triangle - float3 vA1, vB1, vC1, vA2, vB2, vC2; - deme::binID_t L1[3], L2[3], U1[3], U2[3]; - figureOutNodeAndBoundingBox(simParams, granData, triID, vA1, vB1, vC1, L1, U1, nodeA1[triID], nodeB1[triID], - nodeC1[triID]); - figureOutNodeAndBoundingBox(simParams, granData, triID, vA2, vB2, vC2, L2, U2, nodeA2[triID], nodeB2[triID], - nodeC2[triID]); - L1[0] = DEME_MIN(L1[0], L2[0]); - L1[1] = DEME_MIN(L1[1], L2[1]); - L1[2] = DEME_MIN(L1[2], L2[2]); - U1[0] = DEME_MAX(U1[0], U2[0]); - U1[1] = DEME_MAX(U1[1], U2[1]); - U1[2] = DEME_MAX(U1[2], U2[2]); - - deme::binsTriangleTouchPairs_t myReportOffset = numBinsTriTouchesScan[triID]; - // In case this sweep does not agree with the previous one, we need to intercept such potential segfaults - const deme::binsTriangleTouchPairs_t myReportOffset_end = numBinsTriTouchesScan[triID + 1]; - - // Triangle may span a collection of bins... - float BinCenter[3]; - float BinHalfSizes[3]; - BinHalfSizes[0] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - BinHalfSizes[1] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - BinHalfSizes[2] = simParams->binSize / 2. + DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - for (deme::binID_t i = L1[0]; i <= U1[0]; i++) { - for (deme::binID_t j = L1[1]; j <= U1[1]; j++) { - for (deme::binID_t k = L1[2]; k <= U1[2]; k++) { - BinCenter[0] = simParams->binSize * i + simParams->binSize / 2.; - BinCenter[1] = simParams->binSize * j + simParams->binSize / 2.; - BinCenter[2] = simParams->binSize * k + simParams->binSize / 2.; - - if (check_TriangleBoxOverlap(BinCenter, BinHalfSizes, vA1, vB1, vC1) || - check_TriangleBoxOverlap(BinCenter, BinHalfSizes, vA2, vB2, vC2)) { - binIDsEachTriTouches[myReportOffset] = - binIDFrom3Indices(i, j, k, simParams->nbX, simParams->nbY, simParams->nbZ); - triIDsEachBinTouches[myReportOffset] = triID; - myReportOffset++; - if (myReportOffset >= myReportOffset_end) { - return; // Don't step on the next triangle's domain + if (triID >= simParams->nTriGM) { + return; + } + + const bool ok1 = (ok1_all[triID] != 0); + const bool ok2 = (ok2_all[triID] != 0); + + if (!ok1 && !ok2) { + return; + } + + const float3 vA1 = vA1_all[triID]; + const float3 vB1 = vB1_all[triID]; + const float3 vC1 = vC1_all[triID]; + const float3 shift_world = shift_world_all[triID]; + const float3 vA2 = vA1 + shift_world; + const float3 vB2 = vC1 + shift_world; // swapped + const float3 vC2 = vB1 + shift_world; + + int3 LA = make_int3(0, 0, 0), UA = make_int3(-1, -1, -1); + int3 LB = make_int3(0, 0, 0), UB = make_int3(-1, -1, -1); + if (ok1) { + LA = LA_all[triID]; + UA = UA_all[triID]; + } + if (ok2) { + LB = LB_all[triID]; + UB = UB_all[triID]; + } + + // Union bounds + deme::binID_t Lx, Ly, Lz, Ux, Uy, Uz; + if (ok1 && ok2) { + Lx = (deme::binID_t)DEME_MIN(LA.x, LB.x); + Ly = (deme::binID_t)DEME_MIN(LA.y, LB.y); + Lz = (deme::binID_t)DEME_MIN(LA.z, LB.z); + Ux = (deme::binID_t)DEME_MAX(UA.x, UB.x); + Uy = (deme::binID_t)DEME_MAX(UA.y, UB.y); + Uz = (deme::binID_t)DEME_MAX(UA.z, UB.z); + } else if (ok1) { + Lx = (deme::binID_t)LA.x; + Ly = (deme::binID_t)LA.y; + Lz = (deme::binID_t)LA.z; + Ux = (deme::binID_t)UA.x; + Uy = (deme::binID_t)UA.y; + Uz = (deme::binID_t)UA.z; + } else { + Lx = (deme::binID_t)LB.x; + Ly = (deme::binID_t)LB.y; + Lz = (deme::binID_t)LB.z; + Ux = (deme::binID_t)UB.x; + Uy = (deme::binID_t)UB.y; + Uz = (deme::binID_t)UB.z; + } + + // Write tri-bin pairs + const deme::binsTriangleTouchPairs_t myReportOffset = numBinsTriTouchesScan[triID]; + const deme::binsTriangleTouchPairs_t myUpperBound = numBinsTriTouchesScan[triID + 1]; + + deme::binsTriangleTouchPairs_t count = 0; + const float binSizeF = (float)simParams->dyn.binSize; + const float binHalfSpan = binSizeF * (0.5f + (float)DEME_BIN_ENLARGE_RATIO_FOR_FACETS); + const float startX = binSizeF * (float)Lx + 0.5f * binSizeF; + const float startY = binSizeF * (float)Ly + 0.5f * binSizeF; + const float startZ = binSizeF * (float)Lz + 0.5f * binSizeF; + + // Incremental bin-local coordinates: avoid recomputing (v - c) for every bin. + for (deme::binID_t i = Lx, ix = 0; i <= Ux; i++, ix++) { + const float cx = startX + (float)ix * binSizeF; + + const float a0x = vA1.x - cx; + const float a1x = vB1.x - cx; + const float a2x = vC1.x - cx; + + float cy = startY; + for (deme::binID_t j = Ly; j <= Uy; j++) { + const float a0y = vA1.y - cy; + const float a1y = vB1.y - cy; + const float a2y = vC1.y - cy; + + float3 a0 = make_float3(a0x, a0y, vA1.z - startZ); + float3 a1 = make_float3(a1x, a1y, vB1.z - startZ); + float3 a2 = make_float3(a2x, a2y, vC1.z - startZ); + + for (deme::binID_t k = Lz; k <= Uz; k++) { + const bool inA = + ok1 && (i >= (deme::binID_t)LA.x && i <= (deme::binID_t)UA.x && j >= (deme::binID_t)LA.y && + j <= (deme::binID_t)UA.y && k >= (deme::binID_t)LA.z && k <= (deme::binID_t)UA.z); + const bool inB = + ok2 && (i >= (deme::binID_t)LB.x && i <= (deme::binID_t)UB.x && j >= (deme::binID_t)LB.y && + j <= (deme::binID_t)UB.y && k >= (deme::binID_t)LB.z && k <= (deme::binID_t)UB.z); + + if (inA || inB) { + const bool hit = + triBoxOverlapBinLocalEdgesUnionShiftFP32(a0, a1, a2, shift_world, binHalfSpan, inA, inB); + if (hit) { + const deme::binsTriangleTouchPairs_t outIdx = myReportOffset + count; + if (outIdx < myUpperBound) { + binIDsEachTriTouches[outIdx] = binIDFrom3Indices( + i, j, k, simParams->nbX, simParams->nbY, simParams->nbZ); + triIDsEachBinTouches[outIdx] = triID; } + count++; } } + + a0.z -= binSizeF; + a1.z -= binSizeF; + a2.z -= binSizeF; + } + cy += binSizeF; + } + } + // As an ultra-safety net, neutralize any reserved-but-unwritten slots. + // Small count/populate mismatches (e.g., due floating-point branch jitter) should not leak stale bin IDs. + for (deme::binsTriangleTouchPairs_t outIdx = myReportOffset + count; outIdx < myUpperBound; ++outIdx) { + binIDsEachTriTouches[outIdx] = deme::NULL_BINID; + triIDsEachBinTouches[outIdx] = triID; + } + + // Tri-anal contacts: keep identical to original populate kernel + if (meshUniversalContact) { + const deme::binsTriangleTouchPairs_t myAnalOffset = numAnalGeoTriTouchesScan[triID]; + const deme::binsTriangleTouchPairs_t myAnalUpperBound = numAnalGeoTriTouchesScan[triID + 1]; + deme::binsTriangleTouchPairs_t analCount = 0; + for (deme::objID_t objB = 0; objB < simParams->nAnalGM; objB++) { + deme::bodyID_t objBOwner = objOwner[objB]; + unsigned int objFamilyNum = granData->familyID[objBOwner]; + deme::bodyID_t triOwnerID = granData->ownerTriMesh[triID]; + unsigned int triFamilyNum = granData->familyID[triOwnerID]; + unsigned int maskMatID = locateMaskPair(triFamilyNum, objFamilyNum); + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + float3 ownerXYZ; + voxelIDToPosition( + ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[objBOwner], granData->locX[objBOwner], + granData->locY[objBOwner], granData->locZ[objBOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + + const float ownerOriQw = granData->oriQw[objBOwner]; + const float ownerOriQx = granData->oriQx[objBOwner]; + const float ownerOriQy = granData->oriQy[objBOwner]; + const float ownerOriQz = granData->oriQz[objBOwner]; + + const float4 ownerOriQ = make_float4(ownerOriQx, ownerOriQy, ownerOriQz, ownerOriQw); + float3 objBRelPos = make_float3(objRelPosX[objB], objRelPosY[objB], objRelPosZ[objB]); + float3 objBRot = make_float3(objRotX[objB], objRotY[objB], objRotZ[objB]); + + applyOriQToVector3(objBRelPos, ownerOriQ); + applyOriQToVector3(objBRot, ownerOriQ); + + float3 objBPosXYZ = ownerXYZ + objBRelPos; + + deme::contact_t contact_type = checkTriEntityOverlapFP32( + vA1, vB1, vC1, objType[objB], objBPosXYZ, objBRot, objSize1[objB], objSize2[objB], objSize3[objB], + objNormal[objB], granData->marginSizeAnalytical[objB]); + if (contact_type == deme::NOT_A_CONTACT) { + contact_type = checkTriEntityOverlapFP32(vA2, vB2, vC2, objType[objB], objBPosXYZ, objBRot, + objSize1[objB], objSize2[objB], objSize3[objB], + objNormal[objB], granData->marginSizeAnalytical[objB]); + } + + if (contact_type == deme::TRIANGLE_ANALYTICAL_CONTACT) { + const deme::binsTriangleTouchPairs_t outIdx = myAnalOffset + analCount; + if (outIdx < myAnalUpperBound) { + idGeoA[outIdx] = triID; + idGeoB[outIdx] = (deme::bodyID_t)objB; + contactTypePrimitive[outIdx] = contact_type; + } + analCount++; } } - // This can happen for like 1 in 10^9 chance, for the tri--bin contact algorithm has stochasticity on GPU - for (; myReportOffset < myReportOffset_end; myReportOffset++) { - binIDsEachTriTouches[myReportOffset] = deme::NULL_BINID; - triIDsEachBinTouches[myReportOffset] = triID; + // Keep unwritten reserved slots deterministic to avoid stale/invalid contact types. + for (deme::binsTriangleTouchPairs_t outIdx = myAnalOffset + analCount; outIdx < myAnalUpperBound; ++outIdx) { + contactTypePrimitive[outIdx] = deme::NOT_A_CONTACT; } } } diff --git a/src/kernel/DEMCalcForceKernels.cu b/src/kernel/DEMCalcForceKernels.cu deleted file mode 100644 index f39835c3..00000000 --- a/src/kernel/DEMCalcForceKernels.cu +++ /dev/null @@ -1,267 +0,0 @@ -// DEM force computation related custom kernels -#include -#include -#include -_kernelIncludes_; - -// If clump templates are jitified, they will be below -_clumpTemplateDefs_; -// Definitions of analytical entites are below -_analyticalEntityDefs_; -// Material properties are below -_materialDefs_; -// If mass properties are jitified, then they are below -_massDefs_; -_moiDefs_; -// If the user has some utility functions, they will be included here -_forceModelPrerequisites_; - -template -inline __device__ void equipOwnerPosRot(deme::DEMSimParams* simParams, - deme::DEMDataDT* granData, - const deme::bodyID_t& myOwner, - T1& relPos, - double3& ownerPos, - double3& bodyPos, - float4& oriQ) { - voxelIDToPosition( - ownerPos.x, ownerPos.y, ownerPos.z, granData->voxelID[myOwner], granData->locX[myOwner], - granData->locY[myOwner], granData->locZ[myOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - // Do this and we get the `true' pos... - ownerPos.x += simParams->LBFX; - ownerPos.y += simParams->LBFY; - ownerPos.z += simParams->LBFZ; - oriQ.w = granData->oriQw[myOwner]; - oriQ.x = granData->oriQx[myOwner]; - oriQ.y = granData->oriQy[myOwner]; - oriQ.z = granData->oriQz[myOwner]; - applyOriQToVector3(relPos.x, relPos.y, relPos.z, oriQ.w, oriQ.x, oriQ.y, oriQ.z); - bodyPos.x = ownerPos.x + (double)relPos.x; - bodyPos.y = ownerPos.y + (double)relPos.y; - bodyPos.z = ownerPos.z + (double)relPos.z; -} - -__global__ void calculateContactForces(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, size_t nContactPairs) { - deme::contactPairs_t myContactID = blockIdx.x * blockDim.x + threadIdx.x; - if (myContactID < nContactPairs) { - // Identify contact type first - deme::contact_t ContactType = granData->contactType[myContactID]; - // The following quantities are always calculated, regardless of force model - double3 contactPnt; - float3 B2A; // Unit vector pointing from body B to body A (contact normal) - double overlapDepth; - double3 AOwnerPos, bodyAPos, BOwnerPos, bodyBPos; - float AOwnerMass, ARadius, BOwnerMass, BRadius; - float4 AOriQ, BOriQ; - deme::materialsOffset_t bodyAMatType, bodyBMatType; - // The user-specified extra margin size (how much we should be lenient in determining `in-contact') - float extraMarginSize = 0.; - // Then allocate the optional quantities that will be needed in the force model (note: this one can't be in a - // curly bracket, obviously...) - _forceModelIngredientDefinition_; - // Take care of 2 bodies in order, bodyA first, grab location and velocity to local cache - // We know in this kernel, bodyA will be a sphere; B can be something else - { - deme::bodyID_t sphereID = granData->idGeometryA[myContactID]; - deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; - - float3 myRelPos; - float myRadius; - // Get my component offset info from either jitified arrays or global memory - // Outputs myRelPos, myRadius - // Use an input named exactly `sphereID' which is the id of this sphere component - { _componentAcqStrat_; } - - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { - float myMass; - _massAcqStrat_; - AOwnerMass = myMass; - } - - // Optional force model ingredients are loaded here... - _forceModelIngredientAcqForA_; - - equipOwnerPosRot(simParams, granData, myOwner, myRelPos, AOwnerPos, bodyAPos, AOriQ); - - ARadius = myRadius; - bodyAMatType = granData->sphereMaterialOffset[sphereID]; - extraMarginSize = granData->familyExtraMarginSize[AOwnerFamily]; - } - - // Then B, location and velocity - if (ContactType == deme::SPHERE_SPHERE_CONTACT) { - deme::bodyID_t sphereID = granData->idGeometryB[myContactID]; - deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; - - float3 myRelPos; - float myRadius; - // Get my component offset info from either jitified arrays or global memory - // Outputs myRelPos, myRadius - // Use an input named exactly `sphereID' which is the id of this sphere component - { _componentAcqStrat_; } - - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { - float myMass; - _massAcqStrat_; - BOwnerMass = myMass; - } - _forceModelIngredientAcqForB_; - _forceModelGeoWildcardAcqForSph_; - - equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); - - BRadius = myRadius; - bodyBMatType = granData->sphereMaterialOffset[sphereID]; - - // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick - // the larger of the 2 familyExtraMarginSize. - extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) - ? extraMarginSize - : granData->familyExtraMarginSize[BOwnerFamily]; - - checkSpheresOverlap(bodyAPos.x, bodyAPos.y, bodyAPos.z, ARadius, bodyBPos.x, bodyBPos.y, - bodyBPos.z, BRadius, contactPnt.x, contactPnt.y, contactPnt.z, B2A.x, - B2A.y, B2A.z, overlapDepth); - // If overlapDepth is negative then it might still be considered in contact, if the extra margins of A and B - // combined is larger than abs(overlapDepth) - if (overlapDepth < -extraMarginSize) { - ContactType = deme::NOT_A_CONTACT; - } - - } else if (ContactType == deme::SPHERE_MESH_CONTACT) { - // Geometry ID here is called sphereID, although it is not a sphere, it's more like triID. But naming it - // sphereID makes the acquisition process cleaner. - deme::bodyID_t sphereID = granData->idGeometryB[myContactID]; - deme::bodyID_t myOwner = granData->ownerMesh[sphereID]; - //// TODO: Is this OK? - BRadius = DEME_HUGE_FLOAT; - bodyBMatType = granData->triMaterialOffset[sphereID]; - - // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick - // the larger of the 2 familyExtraMarginSize. - extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) - ? extraMarginSize - : granData->familyExtraMarginSize[BOwnerFamily]; - - double3 triNode1 = to_double3(granData->relPosNode1[sphereID]); - double3 triNode2 = to_double3(granData->relPosNode2[sphereID]); - double3 triNode3 = to_double3(granData->relPosNode3[sphereID]); - - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { - float myMass; - _massAcqStrat_; - BOwnerMass = myMass; - } - _forceModelIngredientAcqForB_; - _forceModelGeoWildcardAcqForTri_; - - // bodyBPos is for a place holder for the outcome triNode1 position - equipOwnerPosRot(simParams, granData, myOwner, triNode1, BOwnerPos, bodyBPos, BOriQ); - triNode1 = bodyBPos; - // Do this to node 2 and 3 as well - applyOriQToVector3(triNode2.x, triNode2.y, triNode2.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); - triNode2 += BOwnerPos; - applyOriQToVector3(triNode3.x, triNode3.y, triNode3.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); - triNode3 += BOwnerPos; - // Assign the correct bodyBPos - bodyBPos = triangleCentroid(triNode1, triNode2, triNode3); - - double3 contact_normal; - bool in_contact = triangle_sphere_CD(triNode1, triNode2, triNode3, bodyAPos, ARadius, - contact_normal, overlapDepth, contactPnt); - B2A = to_float3(contact_normal); - - // Sphere--triangle is a bit tricky. Extra margin should only take effect when it comes from the positive - // direction of the mesh facet. If not, sphere-setting-on-needle case will give huge penetration since in - // that case, overlapDepth is very negative and this will be considered in-contact. So the cases we exclude - // are: too far away while at the positive direction; not in contact while at the negative side. - if ((overlapDepth > extraMarginSize) || (!in_contact && overlapDepth < 0.)) { - ContactType = deme::NOT_A_CONTACT; - } - overlapDepth = -overlapDepth; // triangle_sphere_CD gives neg. number for overlapping cases - } else if (ContactType > deme::SPHERE_ANALYTICAL_CONTACT) { - // Geometry ID here is called sphereID, although it is not a sphere, it's more like analyticalID. But naming - // it sphereID makes the acquisition process cleaner. - deme::objID_t sphereID = granData->idGeometryB[myContactID]; - deme::bodyID_t myOwner = objOwner[sphereID]; - // If B is analytical entity, its owner, relative location, material info is jitified. - bodyBMatType = objMaterial[sphereID]; - BOwnerMass = objMass[sphereID]; - //// TODO: Is this OK? - BRadius = DEME_HUGE_FLOAT; - float3 myRelPos; - float3 bodyBRot; - myRelPos.x = objRelPosX[sphereID]; - myRelPos.y = objRelPosY[sphereID]; - myRelPos.z = objRelPosZ[sphereID]; - _forceModelIngredientAcqForB_; - _forceModelGeoWildcardAcqForAnal_; - - equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); - - // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick - // the larger of the 2 familyExtraMarginSize. - extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) - ? extraMarginSize - : granData->familyExtraMarginSize[BOwnerFamily]; - - // B's orientation (such as plane normal) is rotated with its owner too - bodyBRot.x = objRotX[sphereID]; - bodyBRot.y = objRotY[sphereID]; - bodyBRot.z = objRotZ[sphereID]; - applyOriQToVector3(bodyBRot.x, bodyBRot.y, bodyBRot.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); - - // Note for this test on dT side we don't enlarge entities - checkSphereEntityOverlap(bodyAPos, ARadius, objType[sphereID], bodyBPos, bodyBRot, - objSize1[sphereID], objSize2[sphereID], objSize3[sphereID], - objNormal[sphereID], 0.0, contactPnt, B2A, overlapDepth); - // Fix ContactType if needed - if (overlapDepth < -extraMarginSize) { - ContactType = deme::NOT_A_CONTACT; - } - } // else it must be NOT_A_CONTACT - - _forceModelContactWildcardAcq_; - if (ContactType != deme::NOT_A_CONTACT) { - float3 force = make_float3(0, 0, 0); - float3 torque_only_force = make_float3(0, 0, 0); - // Local position of the contact point is always a piece of info we require... regardless of force model - float3 locCPA = to_float3(contactPnt - AOwnerPos); - float3 locCPB = to_float3(contactPnt - BOwnerPos); - // Now map this contact point location to bodies' local ref - applyOriQToVector3(locCPA.x, locCPA.y, locCPA.z, AOriQ.w, -AOriQ.x, -AOriQ.y, - -AOriQ.z); - applyOriQToVector3(locCPB.x, locCPB.y, locCPB.z, BOriQ.w, -BOriQ.x, -BOriQ.y, - -BOriQ.z); - // The following part, the force model, is user-specifiable - // NOTE!! "force" and all wildcards must be properly set by this piece of code - { _DEMForceModel_; } - - // Write contact location values back to global memory - _contactInfoWrite_; - - // If force model modifies owner wildcards, write them back here - _forceModelOwnerWildcardWrite_; - - // Optionally, the forces can be reduced to acc right here (may be faster) - _forceCollectInPlaceStrat_; - } else { - // The contact is no longer active, so we need to destroy its contact history recording - _forceModelContactWildcardDestroy_; - } - - // Updated contact wildcards need to be write back to global mem. It is here because contact wildcard may need - // to be destroyed for non-contact, so it has to go last. - _forceModelContactWildcardWrite_; - } -} diff --git a/src/kernel/DEMCalcForceKernels_PatchBased.cu b/src/kernel/DEMCalcForceKernels_PatchBased.cu new file mode 100644 index 00000000..58f722b8 --- /dev/null +++ b/src/kernel/DEMCalcForceKernels_PatchBased.cu @@ -0,0 +1,283 @@ +// DEM patch-based force computation related custom kernels +#include +#include +#include +_kernelIncludes_; + +// If clump templates are jitified, they will be below +_clumpTemplateDefs_; +// Definitions of analytical entites are below +_analyticalEntityDefs_; +// Material properties are below +_materialDefs_; +// If mass properties are jitified, then they are below +_massDefs_; +_moiDefs_; +// If the user has some utility functions, they will be included here +_forceModelPrerequisites_; + +// Template device function for patch-based contact force calculation +template +__device__ __forceinline__ void calculatePatchContactForces_impl(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + const double* finalAreas, + const float3* finalNormals, + const double* finalPenetrations, + const double3* finalContactPoints, + deme::contactPairs_t myPatchContactID, + deme::contactPairs_t startOffsetPatch) { + // Contact type is known at compile time + deme::contact_t ContactType = CONTACT_TYPE; + + // Calculate relative index for accessing the temp arrays (finalAreas, finalNormals, finalPenetrations, + // finalContactPoints) + deme::contactPairs_t relativeIndex = myPatchContactID - startOffsetPatch; + + // The following quantities are provided from the patch voting process + float3 B2A = finalNormals[relativeIndex]; // contact normal felt by A, pointing from B to A + double overlapDepth = finalPenetrations[relativeIndex]; // penetration depth + double overlapArea = finalAreas[relativeIndex]; // total contact area for this patch pair + + // Contact point is computed via weighted average (weight = penetration * area) + double3 contactPnt = finalContactPoints[relativeIndex]; + double3 AOwnerPos, bodyAPos, BOwnerPos, bodyBPos; + float AOwnerMass, ARadius, BOwnerMass, BRadius; + float4 AOriQ, BOriQ; + + deme::materialsOffset_t bodyAMatType, bodyBMatType; + // The user-specified extra margin size (how much we should be lenient in determining `in-contact') + float extraMarginSize = 0.; + + // Then allocate the optional quantities that will be needed in the force model + _forceModelIngredientDefinition_; + + deme::bodyID_t ownerA = deme::NULL_BODYID; + deme::bodyID_t ownerB = deme::NULL_BODYID; + + const deme::bodyID_t idA_raw = granData->idPatchA[myPatchContactID]; + const deme::bodyID_t idB_raw = granData->idPatchB[myPatchContactID]; + + // Decompose ContactType to get the types of A and B (known at compile time) + constexpr deme::geoType_t AType = (CONTACT_TYPE >> 4); + constexpr deme::geoType_t BType = (CONTACT_TYPE & 0xF); + + // ---------------------------------------------------------------- + // Based on A's type, equip info + // ---------------------------------------------------------------- + if constexpr (AType == deme::GEO_T_SPHERE) { + // For sphere-mesh contacts, patch A is a sphere + // Note: For spheres, the patch ID is the same as the sphere ID + deme::bodyID_t sphereID = idA_raw; + deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; + ownerA = myOwner; + deme::bodyID_t myPatchID = sphereID; // For spheres, patch ID == sphere ID + + float3 myRelPos; + float myRadius; + // Get my component offset info from either jitified arrays or global memory + { _componentAcqStrat_; } + + // Get my mass info + { + float myMass; + _massAcqStrat_; + AOwnerMass = myMass; + } + + // Optional force model ingredients are loaded here... + _forceModelIngredientAcqForA_; + // Geo wildcard acquisition is intentionally disabled. Do not resurrect this hot-path hook unless geo + // wildcards are reintroduced end-to-end. + // _forceModelGeoWildcardAcqForASph_; + + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, AOwnerPos, bodyAPos, AOriQ); + + ARadius = myRadius; + bodyAMatType = granData->sphereMaterialOffset[myPatchID]; + extraMarginSize = granData->familyExtraMarginSize[AOwnerFamily]; + } else if constexpr (AType == deme::GEO_T_TRIANGLE) { + // For mesh-mesh or mesh-analytical contacts, patch A is a mesh patch + deme::bodyID_t myPatchID = idA_raw; + deme::bodyID_t myOwner = granData->ownerPatchMesh[myPatchID]; + ownerA = myOwner; + ARadius = DEME_HUGE_FLOAT; + bodyAMatType = granData->patchMaterialOffset[myPatchID]; + + float3 myRelPos = granData->relPosPatch[myPatchID]; + + // Get my mass info + { + float myMass; + _massAcqStrat_; + AOwnerMass = myMass; + } + _forceModelIngredientAcqForA_; + // Geo wildcard acquisition is intentionally disabled. Any future replacement must define its indexing + // semantics explicitly before this hot-path hook is restored. + // _forceModelGeoWildcardAcqForAMeshPatch_; + + // In mesh case, bodyAPos is the patch position (not necessarily needed in the force model though) + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, AOwnerPos, bodyAPos, AOriQ); + extraMarginSize = granData->familyExtraMarginSize[AOwnerFamily]; + } else { + // Unsupported type + ContactType = deme::NOT_A_CONTACT; + } + + // ---------------------------------------------------------------- + // Then B, location and velocity, depending on type + // ---------------------------------------------------------------- + if constexpr (BType == deme::GEO_T_TRIANGLE) { + // For mesh-related contacts, patch B is a mesh patch + deme::bodyID_t myPatchID = idB_raw; + deme::bodyID_t myOwner = granData->ownerPatchMesh[myPatchID]; + ownerB = myOwner; + BRadius = DEME_HUGE_FLOAT; + bodyBMatType = granData->patchMaterialOffset[myPatchID]; + + float3 myRelPos = granData->relPosPatch[myPatchID]; + + // Get my mass info + { + float myMass; + _massAcqStrat_; + BOwnerMass = myMass; + } + _forceModelIngredientAcqForB_; + // Geo wildcard acquisition is intentionally disabled. Any future replacement must define its indexing + // semantics explicitly before this hot-path hook is restored. + // _forceModelGeoWildcardAcqForBMeshPatch_; + + // In mesh case, bodyBPos is the patch position (not necessarily needed in the force model though) + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); + + // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick + // the larger of the 2 familyExtraMarginSize. + extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) + ? extraMarginSize + : granData->familyExtraMarginSize[BOwnerFamily]; + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + + } else if constexpr (BType == deme::GEO_T_ANALYTICAL) { + // For mesh-analytical contacts, patch B is an analytical entity + deme::objID_t analyticalID = granData->idPatchB[myPatchContactID]; + deme::bodyID_t myOwner = objOwner[analyticalID]; + ownerB = myOwner; + deme::bodyID_t myPatchID = analyticalID; + + bodyBMatType = objMaterial[analyticalID]; + BOwnerMass = objMass[analyticalID]; + BRadius = DEME_HUGE_FLOAT; + float3 myRelPos; + myRelPos.x = objRelPosX[analyticalID]; + myRelPos.y = objRelPosY[analyticalID]; + myRelPos.z = objRelPosZ[analyticalID]; + _forceModelIngredientAcqForB_; + // Geo wildcard acquisition is intentionally disabled. Do not resurrect this hot-path hook unless geo + // wildcards are reintroduced end-to-end. + // _forceModelGeoWildcardAcqForBAnal_; + + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); + + // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick + // the larger of the 2 familyExtraMarginSize. + extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) + ? extraMarginSize + : granData->familyExtraMarginSize[BOwnerFamily]; + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + } + + // Now compute forces using the patch-based contact data + _forceModelContactWildcardAcq_; + + // Essentials for storing and calculating contact info + float3 force = make_float3(0, 0, 0); + float3 torque_only_force = make_float3(0, 0, 0); + // Local position of the contact point + float3 locCPA = to_float3(contactPnt - AOwnerPos); + float3 locCPB = to_float3(contactPnt - BOwnerPos); + // Map contact point location to bodies' local reference frames + applyOriQToVector3(locCPA, make_float4(-AOriQ.x, -AOriQ.y, -AOriQ.z, AOriQ.w)); + applyOriQToVector3(locCPB, make_float4(-BOriQ.x, -BOriQ.y, -BOriQ.z, BOriQ.w)); + + if (ContactType != deme::NOT_A_CONTACT) { + // The force model is user-specifiable + // NOTE!! "force" and all wildcards must be properly set by this piece of code + { _DEMForceModel_; } + + // If force model modifies owner wildcards, write them back here + _forceModelOwnerWildcardWrite_; + } else { + // Note in DEME3, we do not clear force array anymore in each timestep, so always writing back force and contact + // points, even for zero-force non-contacts, is needed (unless of course, the user instructed no force record). + // This design has implications in our new two-step patch-based force calculation algorithm, as we re-use some + // force-storing arrays for intermediate values. + force = make_float3(0.f, 0.f, 0.f); + torque_only_force = make_float3(0.f, 0.f, 0.f); + locCPA = make_float3(0.f, 0.f, 0.f); + locCPB = make_float3(0.f, 0.f, 0.f); + _forceModelContactWildcardDestroy_; + } + + // Write contact location values back to global memory (after periodic wrap correction). + _contactInfoWrite_; + + // Optionally, the forces can be reduced to acc right here (may be faster) + _forceCollectInPlaceStrat_; + + // Updated contact wildcards need to be write back to global mem + _forceModelContactWildcardWrite_; +} + +// 3 specialized kernels for patch-based contact types +DEME_KERNEL void calculatePatchContactForces_SphTri(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + const double* finalAreas, + const float3* finalNormals, + const double* finalPenetrations, + const double3* finalContactPoints, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPatchContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPatchContactID < startOffset + nContactPairs) { + calculatePatchContactForces_impl(simParams, granData, finalAreas, finalNormals, + finalPenetrations, finalContactPoints, + myPatchContactID, startOffset); + } +} + +DEME_KERNEL void calculatePatchContactForces_TriTri(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + const double* finalAreas, + const float3* finalNormals, + const double* finalPenetrations, + const double3* finalContactPoints, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPatchContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPatchContactID < startOffset + nContactPairs) { + calculatePatchContactForces_impl(simParams, granData, finalAreas, finalNormals, + finalPenetrations, finalContactPoints, + myPatchContactID, startOffset); + } +} + +DEME_KERNEL void calculatePatchContactForces_TriAnal(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + const double* finalAreas, + const float3* finalNormals, + const double* finalPenetrations, + const double3* finalContactPoints, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPatchContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPatchContactID < startOffset + nContactPairs) { + calculatePatchContactForces_impl( + simParams, granData, finalAreas, finalNormals, finalPenetrations, finalContactPoints, myPatchContactID, + startOffset); + } +} diff --git a/src/kernel/DEMCalcForceKernels_Primitive.cu b/src/kernel/DEMCalcForceKernels_Primitive.cu new file mode 100644 index 00000000..f9da75ad --- /dev/null +++ b/src/kernel/DEMCalcForceKernels_Primitive.cu @@ -0,0 +1,573 @@ +// DEM force computation related custom kernels +#include +#include +#include +_kernelIncludes_; + +inline __device__ float ownerShellHalfThickness(const deme::DEMSimParams* simParams, + const deme::DEMDataDT* granData, + deme::bodyID_t ownerID) { + if (!granData->ownerMeshShellHalfThickness || ownerID == deme::NULL_BODYID || ownerID >= simParams->nOwnerBodies) { + return 0.f; + } + return fmaxf(granData->ownerMeshShellHalfThickness[ownerID], 0.f); +} + +// If clump templates are jitified, they will be below +_clumpTemplateDefs_; +// Definitions of analytical entites are below +_analyticalEntityDefs_; +// Material properties are below +_materialDefs_; +// If mass properties are jitified, then they are below +_massDefs_; +_moiDefs_; +// If the user has some utility functions, they will be included here +_forceModelPrerequisites_; + +// Template device function for contact force calculation - will be called by 5 specialized kernels +template +__device__ __forceinline__ void calculatePrimitiveContactForces_impl(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t myPrimitiveContactID) { + // Contact type is known at compile time + deme::contact_t ContactType = CONTACT_TYPE; + // The following quantities are always calculated, regardless of force model + // Note it is strangely important to give init values to contactPnt here, otherwise on some systems it seems nvcc + // optimizes it out before potentially using it to assign VRAM values, giving NaN results. + double3 contactPnt = make_double3(0.0, 0.0, 0.0); // contact point in global coords + float3 B2A = make_float3(0.f, 0.f, 1.f); // contact normal felt by A, pointing from B to A, unit vector + // Penetration depth + // Positive number for overlapping cases, negative for non-overlapping cases + double overlapDepth = 0.0; + // Area of the contact surface, or in the mesh--mesh case, area of the clipping polygon projection + double overlapArea = 0.0; + // Per-triangle shallowest vertex penetration into the other triangle's plane. Each is non-zero only when that + // triangle is completely submerged, in which case kT needs this depth to keep it traceable by the broad phase. + double minDepthA = 0.0; + double minDepthB = 0.0; + // `Body pos' in the primitive contact kernel means the position of the primitive itself, e.g., sphere center or + // triangle nodes + double3 AOwnerPos, bodyAPos, BOwnerPos, bodyBPos; + // Radius always means radius of curvature; for triangle and analytical entity, it's set to a huge number + float AOwnerMass, ARadius, BOwnerMass, BRadius; + float4 AOriQ, BOriQ; + deme::materialsOffset_t bodyAMatType, bodyBMatType; + // The user-specified extra margin size (how much we should be lenient in determining `in-contact') + float extraMarginSize = 0.; + // Triangle A's three points are defined outside, as may be reused in B's acquisition and penetration calc. + double3 triANode1, triANode2, triANode3; + // Then allocate the optional quantities that will be needed in the force model (note: this one can't be in a + // curly bracket, obviously...) + _forceModelIngredientDefinition_; + deme::bodyID_t ownerA = deme::NULL_BODYID; + deme::bodyID_t ownerB = deme::NULL_BODYID; + const deme::bodyID_t idA_raw = granData->idPrimitiveA[myPrimitiveContactID]; + const deme::bodyID_t idB_raw = granData->idPrimitiveB[myPrimitiveContactID]; + // Take care of 2 bodies in order, bodyA first, grab location and velocity to local cache + // Decompose ContactType to get the types of A and B (known at compile time) + constexpr deme::geoType_t AType = (CONTACT_TYPE >> 4); + constexpr deme::geoType_t BType = (CONTACT_TYPE & 0xF); + // One important thing: When writing results, the contact index is oganized by patch-based contact IDs. + // For sph-sph and sph-anal contacts, this primitive sweep already generates the final results, so putting the + // resulting into the correct place needs to be done here. + deme::contactPairs_t myPatchContactID = granData->geomToPatchMap[myPrimitiveContactID]; + + // ---------------------------------------------------------------- + // Based on A's type, equip info + // ---------------------------------------------------------------- + if constexpr (AType == deme::GEO_T_SPHERE) { + deme::bodyID_t sphereID = idA_raw; + deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; + ownerA = myOwner; + // Clump sphere's patch ID is just the sphereID itself + deme::bodyID_t myPatchID = sphereID; + + float3 myRelPos; + float myRadius; + // Get my component offset info from either jitified arrays or global memory + // Outputs myRelPos, myRadius + // Use an input named exactly `sphereID' which is the id of this sphere component + { _componentAcqStrat_; } + + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { + float myMass; + _massAcqStrat_; + AOwnerMass = myMass; + } + + // Optional force model ingredients are loaded here... + _forceModelIngredientAcqForA_; + // Geo wildcard acquisition is intentionally disabled. Do not resurrect this hot-path hook unless geo + // wildcards are reintroduced end-to-end. + // _forceModelGeoWildcardAcqForASph_; + + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, AOwnerPos, bodyAPos, AOriQ); + + ARadius = myRadius; + // In priciple, material is patch-associated; but in clump, for now, we don't distinguish sphere and patch + bodyAMatType = granData->sphereMaterialOffset[myPatchID]; + extraMarginSize = granData->familyExtraMarginSize[AOwnerFamily]; + } else if constexpr (AType == deme::GEO_T_TRIANGLE) { + deme::bodyID_t triID = idA_raw; + deme::bodyID_t myOwner = granData->ownerTriMesh[triID]; + ownerA = myOwner; + //// TODO: Is this OK? + ARadius = DEME_HUGE_FLOAT; + // If this is a triangle then it has a patch ID + deme::bodyID_t myPatchID = granData->triPatchID[triID]; + bodyAMatType = granData->patchMaterialOffset[myPatchID]; + extraMarginSize = granData->familyExtraMarginSize[AOwnerFamily]; + + triANode1 = to_double3(granData->relPosNode1[triID]); + triANode2 = to_double3(granData->relPosNode2[triID]); + triANode3 = to_double3(granData->relPosNode3[triID]); + + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { + float myMass; + _massAcqStrat_; + AOwnerMass = myMass; + } + _forceModelIngredientAcqForA_; + // Geo wildcard acquisition is intentionally disabled. Any future replacement must define its indexing + // semantics explicitly before this hot-path hook is restored. + // _forceModelGeoWildcardAcqForAMeshPatch_; + + // bodyAPos is for a place holder for the outcome triANode1 position + equipOwnerPosRot(simParams, granData, myOwner, triANode1, AOwnerPos, bodyAPos, AOriQ); + triANode1 = bodyAPos; + // Do this to node 2 and 3 as well + applyOriQToVector3(triANode2, AOriQ); + triANode2 += AOwnerPos; + applyOriQToVector3(triANode3, AOriQ); + triANode3 += AOwnerPos; + // Assign the correct bodyAPos + bodyAPos = triangleCentroid(triANode1, triANode2, triANode3); + } else { + // Currently, we only support sphere and mesh for body A + ContactType = deme::NOT_A_CONTACT; + } + + // ---------------------------------------------------------------- + // Then B, location and velocity, depending on type + // ---------------------------------------------------------------- + if constexpr (BType == deme::GEO_T_SPHERE) { + deme::bodyID_t sphereID = idB_raw; + deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; + ownerB = myOwner; + // Clump sphere's patch ID is just the sphereID itself + deme::bodyID_t myPatchID = sphereID; + + float3 myRelPos; + float myRadius; + // Get my component offset info from either jitified arrays or global memory + // Outputs myRelPos, myRadius + // Use an input named exactly `sphereID' which is the id of this sphere component + { _componentAcqStrat_; } + + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { + float myMass; + _massAcqStrat_; + BOwnerMass = myMass; + } + _forceModelIngredientAcqForB_; + // Geo wildcard acquisition is intentionally disabled. Do not resurrect this hot-path hook unless geo + // wildcards are reintroduced end-to-end. + // _forceModelGeoWildcardAcqForBSph_; + + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); + + BRadius = myRadius; + // In priciple, material is patch-associated; but in clump, for now, we don't distinguish sphere and patch + bodyBMatType = granData->sphereMaterialOffset[myPatchID]; + + // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick + // the larger of the 2 familyExtraMarginSize. + extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) + ? extraMarginSize + : granData->familyExtraMarginSize[BOwnerFamily]; + + // If B is a sphere, then A can only be a sphere + checkSpheresOverlap(bodyAPos.x, bodyAPos.y, bodyAPos.z, ARadius, bodyBPos.x, bodyBPos.y, + bodyBPos.z, BRadius, contactPnt.x, contactPnt.y, contactPnt.z, B2A.x, B2A.y, + B2A.z, overlapDepth, overlapArea); + // If overlapDepth is negative then it might still be considered in contact, if the extra margins of A and B + // combined is larger than abs(overlapDepth) + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + + } else if constexpr (BType == deme::GEO_T_TRIANGLE) { + deme::bodyID_t triID = idB_raw; + deme::bodyID_t myOwner = granData->ownerTriMesh[triID]; + ownerB = myOwner; + //// TODO: Is this OK? + BRadius = DEME_HUGE_FLOAT; + // If this is a triangle then it has a patch ID + deme::bodyID_t myPatchID = granData->triPatchID[triID]; + bodyBMatType = granData->patchMaterialOffset[myPatchID]; + + // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick + // the larger of the 2 familyExtraMarginSize. + extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) + ? extraMarginSize + : granData->familyExtraMarginSize[BOwnerFamily]; + const float shell_half_B = ownerShellHalfThickness(simParams, granData, ownerB); + const double shellDepthAdd = static_cast( + (AType == deme::GEO_T_TRIANGLE) ? (shell_half_B + ownerShellHalfThickness(simParams, granData, ownerA)) + : 0.f); + + double3 triBNode1 = to_double3(granData->relPosNode1[triID]); + double3 triBNode2 = to_double3(granData->relPosNode2[triID]); + double3 triBNode3 = to_double3(granData->relPosNode3[triID]); + + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { + float myMass; + _massAcqStrat_; + BOwnerMass = myMass; + } + _forceModelIngredientAcqForB_; + // Geo wildcard acquisition is intentionally disabled. Any future replacement must define its indexing + // semantics explicitly before this hot-path hook is restored. + // _forceModelGeoWildcardAcqForBMeshPatch_; + + // bodyBPos is for a place holder for the outcome triBNode1 position + equipOwnerPosRot(simParams, granData, myOwner, triBNode1, BOwnerPos, bodyBPos, BOriQ); + triBNode1 = bodyBPos; + // Do this to node 2 and 3 as well + applyOriQToVector3(triBNode2, BOriQ); + triBNode2 += BOwnerPos; + applyOriQToVector3(triBNode3, BOriQ); + triBNode3 += BOwnerPos; + // Assign the correct bodyBPos + bodyBPos = triangleCentroid(triBNode1, triBNode2, triBNode3); + + // If B is a triangle, then A can be a sphere or a triangle. + if constexpr (AType == deme::GEO_T_SPHERE) { + double3 contact_normal; + const double shellInflatedRadius = static_cast(ARadius) + static_cast(shell_half_B); + // Note checkTriSphereOverlap gives positive number for overlapping cases. + // Using checkTriSphereOverlap rather than the directional version is effectively saying if the overlap is + // more than 2R, we don't want it; this is useful in preventing contact being detection from the other side + // of a thin mesh. + bool in_contact = + checkTriSphereOverlap(triBNode1, triBNode2, triBNode3, bodyAPos, shellInflatedRadius, + contact_normal, overlapDepth, overlapArea, contactPnt); + B2A = to_float3(contact_normal); + + // If the solver says in contact, we do not question it + if (!in_contact) { + // Extra margin takes effect + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + } + + } else if constexpr (AType == deme::GEO_T_TRIANGLE) { + // Triangle--triangle contact, a bit more complex... + double3 contact_normal; + bool in_contact = checkTriangleTriangleOverlap( + triANode1, triANode2, triANode3, triBNode1, triBNode2, triBNode3, contact_normal, overlapDepth, + overlapArea, contactPnt, minDepthA, minDepthB); + bool shell_contact_candidate = false; + B2A = to_float3(contact_normal); + if (in_contact) { + overlapDepth += shellDepthAdd; + } else if (shellDepthAdd > DEME_TINY_FLOAT) { + // Shell fallback for non-intersecting triangles: + // use closest feature distance and a shell-thickness skin model to obtain a stable normal/depth. + double3 closestA, closestB; + const double sep_dist = closestPtTriTriDistance( + triANode1, triANode2, triANode3, triBNode1, triBNode2, triBNode3, closestA, closestB); + if (isfinite(sep_dist)) { + const double3 sep_vec = closestA - closestB; + const double sep2 = dot(sep_vec, sep_vec); + if (sep2 > 1e-24) { + B2A = to_float3(sep_vec / sqrt(sep2)); + } + overlapDepth = shellDepthAdd - sep_dist; + contactPnt = (closestA + closestB) * 0.5; + if (overlapDepth > 0.0) { + shell_contact_candidate = true; + // Non-zero area regularization for shell-edge/vertex contacts (important for Hertzian + // variants). + const double effR = fmax(shellDepthAdd * 0.5, 1e-12); + const double dcap = fmin(overlapDepth, 2.0 * effR); + const double a = static_cast(deme::PI) * (2.0 * effR * dcap - dcap * dcap); + overlapArea = fmax(a, 0.0); + } else { + overlapArea = 0.0; + } + } + } + + // Remove suspicious back-face false pairings: reject the contact if the penetration depth is too + // large relative to the distance between the contact point and a triangle's patch center. + // We use triPatchID[triangleID] to look up the mesh-defined patch center from relPosPatch, because + // idPatchA/B in the contact array represents island-merged synthetic group IDs in the complex flooding + // path and does not directly index relPosPatch; only the triangle-derived patch ID is reliable here. + // For non-watertight meshes the patch center is not representative, so the corresponding half of the + // guard is skipped. The check for the watertight side is still applied. + if ((in_contact || shell_contact_candidate) && simParams->triTriContactRejectionRatio >= 0.f && + granData->relPosPatch && granData->triPatchID) { + const double ratio = (double)simParams->triTriContactRejectionRatio; + bool reject = false; + // Check mesh A side only if mesh A's owner is watertight + const deme::bodyID_t patchA = granData->triPatchID[idA_raw]; + const deme::bodyID_t patchOwnerA = granData->ownerPatchMesh[patchA]; + if (!granData->ownerMeshWatertight || granData->ownerMeshWatertight[patchOwnerA]) { + float3 relPatchCenterA = granData->relPosPatch[patchA]; + applyOriQToVector3(relPatchCenterA, AOriQ); + const double3 patchCenterA = AOwnerPos + to_double3(relPatchCenterA); + const double distA = length(contactPnt - patchCenterA); + if (overlapDepth > ratio * distA) { + reject = true; + } + } + // Check mesh B side only if mesh B's owner is watertight + if (!reject) { + const deme::bodyID_t patchB = granData->triPatchID[idB_raw]; + const deme::bodyID_t patchOwnerB = granData->ownerPatchMesh[patchB]; + if (!granData->ownerMeshWatertight || granData->ownerMeshWatertight[patchOwnerB]) { + float3 relPatchCenterB = granData->relPosPatch[patchB]; + applyOriQToVector3(relPatchCenterB, BOriQ); + const double3 patchCenterB = BOwnerPos + to_double3(relPatchCenterB); + const double distB = length(contactPnt - patchCenterB); + if (overlapDepth > ratio * distB) { + reject = true; + } + } + } + if (reject) { + in_contact = false; + shell_contact_candidate = false; + overlapDepth = -DEME_HUGE_FLOAT; + overlapArea = 0.0; + } + } + + // Fix ContactType if needed + // If the solver says in contact, we do not question it + if (!in_contact) { + // Then, if we have extra margin, we check that if the distance is within the extra margin + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + } + } + + } else if constexpr (BType == deme::GEO_T_ANALYTICAL) { + deme::objID_t analyticalID = granData->idPrimitiveB[myPrimitiveContactID]; + deme::bodyID_t myOwner = objOwner[analyticalID]; + ownerB = myOwner; + // For analytical entity, its patch ID is just its own component ID (but myPatchID is hardly used in this + // analytical case) + deme::bodyID_t myPatchID = analyticalID; + // If B is analytical entity, its owner, relative location, material info is jitified. + bodyBMatType = objMaterial[analyticalID]; + BOwnerMass = objMass[analyticalID]; + //// TODO: Is this OK? + BRadius = DEME_HUGE_FLOAT; + float3 myRelPos; + float3 bodyBRot; + myRelPos.x = objRelPosX[analyticalID]; + myRelPos.y = objRelPosY[analyticalID]; + myRelPos.z = objRelPosZ[analyticalID]; + _forceModelIngredientAcqForB_; + // Geo wildcard acquisition is intentionally disabled. Do not resurrect this hot-path hook unless geo + // wildcards are reintroduced end-to-end. + // _forceModelGeoWildcardAcqForBAnal_; + + equipOwnerPosRot(simParams, granData, myOwner, myRelPos, BOwnerPos, bodyBPos, BOriQ); + + // As the grace margin, the distance (negative overlap) just needs to be within the grace margin. So we pick + // the larger of the 2 familyExtraMarginSize. + extraMarginSize = (extraMarginSize > granData->familyExtraMarginSize[BOwnerFamily]) + ? extraMarginSize + : granData->familyExtraMarginSize[BOwnerFamily]; + + // B's orientation (such as plane normal) is rotated with its owner too + bodyBRot.x = objRotX[analyticalID]; + bodyBRot.y = objRotY[analyticalID]; + bodyBRot.z = objRotZ[analyticalID]; + applyOriQToVector3(bodyBRot, BOriQ); + + // If B is an analytical entity, then A can be a sphere or a triangle. + if constexpr (AType == deme::GEO_T_SPHERE) { + // Note for this test on dT side we don't enlarge entities + checkSphereEntityOverlap(bodyAPos, ARadius, objType[analyticalID], bodyBPos, + bodyBRot, objSize1[analyticalID], objSize2[analyticalID], + objSize3[analyticalID], objNormal[analyticalID], 0.0, + contactPnt, B2A, overlapDepth, overlapArea); + // Fix ContactType if needed + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + } else if constexpr (AType == deme::GEO_T_TRIANGLE) { + calcTriEntityOverlap(triANode1, triANode2, triANode3, objType[analyticalID], bodyBPos, + bodyBRot, objSize1[analyticalID], objSize2[analyticalID], + objSize3[analyticalID], objNormal[analyticalID], contactPnt, B2A, + overlapDepth, overlapArea); + overlapDepth += static_cast(ownerShellHalfThickness(simParams, granData, ownerA)); + // Fix ContactType if needed + if (overlapDepth <= -extraMarginSize) { + ContactType = deme::NOT_A_CONTACT; + } + } + } + + if constexpr (CONTACT_TYPE == deme::SPHERE_SPHERE_CONTACT || CONTACT_TYPE == deme::SPHERE_ANALYTICAL_CONTACT) { + _forceModelContactWildcardAcq_; + + // Essentials for storing and calculating contact info + float3 force = make_float3(0, 0, 0); + float3 torque_only_force = make_float3(0, 0, 0); + // Local position of the contact point is always a piece of info we require... regardless of force model + float3 locCPA = to_float3(contactPnt - AOwnerPos); + float3 locCPB = to_float3(contactPnt - BOwnerPos); + // Now map this contact point location to bodies' local ref + applyOriQToVector3(locCPA, make_float4(-AOriQ.x, -AOriQ.y, -AOriQ.z, AOriQ.w)); + applyOriQToVector3(locCPB, make_float4(-BOriQ.x, -BOriQ.y, -BOriQ.z, BOriQ.w)); + + // Force model execution (or skip for inactive periodic candidate) + if (ContactType != deme::NOT_A_CONTACT) { + // The following part, the force model, is user-specifiable + // NOTE!! "force" and all wildcards must be properly set by this piece of code + { _DEMForceModel_; } + + // If force model modifies owner wildcards, write them back here + _forceModelOwnerWildcardWrite_; + } else { + // No contribution from this candidate in this step. + // Note in DEME3, we do not clear force array anymore in each timestep, so always writing back force and + // contact points, even for zero-force non-contacts, is needed (unless of course, the user instructed no + // force record). This design has implications in our new two-step patch-based force calculation algorithm, + // as we re-use some force-storing arrays for intermediate values. + force = make_float3(0.f, 0.f, 0.f); + torque_only_force = make_float3(0.f, 0.f, 0.f); + locCPA = make_float3(0.f, 0.f, 0.f); + locCPB = make_float3(0.f, 0.f, 0.f); + _forceModelContactWildcardDestroy_; + } + + // Write contact location values back to global memory (after periodic wrap correction). + _contactInfoWrite_; + + // Optionally, the forces can be reduced to acc right here (may be faster) + _forceCollectInPlaceStrat_; + + // Updated contact wildcards need to be write back to global mem. It is here because contact wildcard may need + // to be destroyed for non-contact, so it has to go last. + _forceModelContactWildcardWrite_; + } else { // If this is the kernel for a mesh-related contact, another follow-up kernel is needed to compute force + // Use contactForces, contactPointGeometryAB to store the contact info for the next + // kernel to compute forces. contactForces is used to store the contact normal. contactPointGeometryA is used to + // store the (double) contact penetration. contactPointGeometryB is used to store the (double) contact area + // contactTorque_convToForce is used to store the contact point position (cast from double3 to float3) + + // For tri-tri contacts, atomically update the per-triangle shallowest-penetration array so kT can size its + // contact-detection margins. Each triangle receives its own shallowest (minimum) vertex penetration depth, + // independent of which projection direction supplied the contact force geometry. A non-zero value means that + // triangle is completely submerged in the other triangle's plane direction and needs at least this margin to + // remain traceable. Other contact pairs may contribute larger values through atomicMax. Non-negative floats + // maintain IEEE 754 bit-ordering under unsigned int reinterpretation, so atomicMax on their unsigned int bit + // representation gives the correct result. + // Skipped when meshParticlesLowPoly is enabled (the array is not used by kT in that mode). + if constexpr (CONTACT_TYPE == deme::TRIANGLE_TRIANGLE_CONTACT) { + if (!simParams->meshParticlesLowPoly && ContactType != deme::NOT_A_CONTACT) { + if (minDepthA > 0.0) { + unsigned int valBits = __float_as_uint(static_cast(minDepthA)); + atomicMax((unsigned int*)&granData->maxTriTriPenetration[idA_raw], valBits); + } + if (minDepthB > 0.0) { + unsigned int valBits = __float_as_uint(static_cast(minDepthB)); + atomicMax((unsigned int*)&granData->maxTriTriPenetration[idB_raw], valBits); + } + } + } + + // Store contact normal (B2A is already a float3) + granData->contactForces[myPrimitiveContactID] = B2A; + // Store contact penetration depth (double) in contactPointGeometryA (float3) + granData->contactPointGeometryA[myPrimitiveContactID] = doubleToFloat3Storage(overlapDepth); + // Store contact area (double) in contactPointGeometryB (float3) + // If this is not a contact, we store 0.0 in the area, so it has no voting power in the next kernels. Note the + // NOT_A_CONTACT control flow here and in the next few kernels is integrated in areas. + granData->contactPointGeometryB[myPrimitiveContactID] = + doubleToFloat3Storage((ContactType == deme::NOT_A_CONTACT || overlapArea <= 0.0) ? 0.0 : overlapArea); + // Store contact point (cast from double3 to float3). Could make the following check, but hopefully it's not + // necessary. if (!isfinite(contactPnt.x) || !isfinite(contactPnt.y) || !isfinite(contactPnt.z)) { + // DEME_ABORT_KERNEL( + // "Primitive contact No. %d of type %d has contact point with infinite component(s), something is " + // "wrong.\n", + // myPrimitiveContactID, ContactType); + // } + granData->contactTorque_convToForce[myPrimitiveContactID] = to_float3(contactPnt); + } +} + +// 5 specialized kernels for different contact types +DEME_KERNEL void calculatePrimitiveContactForces_SphSph(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPrimitiveContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPrimitiveContactID < startOffset + nContactPairs) { + calculatePrimitiveContactForces_impl(simParams, granData, myPrimitiveContactID); + } +} + +DEME_KERNEL void calculatePrimitiveContactForces_SphTri(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPrimitiveContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPrimitiveContactID < startOffset + nContactPairs) { + calculatePrimitiveContactForces_impl(simParams, granData, myPrimitiveContactID); + } +} + +DEME_KERNEL void calculatePrimitiveContactForces_SphAnal(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPrimitiveContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPrimitiveContactID < startOffset + nContactPairs) { + calculatePrimitiveContactForces_impl(simParams, granData, + myPrimitiveContactID); + } +} + +DEME_KERNEL void calculatePrimitiveContactForces_TriTri(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPrimitiveContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPrimitiveContactID < startOffset + nContactPairs) { + calculatePrimitiveContactForces_impl(simParams, granData, + myPrimitiveContactID); + } +} + +DEME_KERNEL void calculatePrimitiveContactForces_TriAnal(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::contactPairs_t startOffset, + deme::contactPairs_t nContactPairs) { + deme::contactPairs_t myPrimitiveContactID = startOffset + blockIdx.x * blockDim.x + threadIdx.x; + if (myPrimitiveContactID < startOffset + nContactPairs) { + calculatePrimitiveContactForces_impl(simParams, granData, + myPrimitiveContactID); + } +} diff --git a/src/kernel/DEMCollectForceKernels.cu b/src/kernel/DEMCollectForceKernels.cu index c33f8830..974d9399 100644 --- a/src/kernel/DEMCollectForceKernels.cu +++ b/src/kernel/DEMCollectForceKernels.cu @@ -3,146 +3,328 @@ #include _kernelIncludes_; -// For analytical entities' owners -__constant__ __device__ deme::bodyID_t objOwner[] = {_objOwner_}; // Mass properties are below, if jitified mass properties are in use _massDefs_; _moiDefs_; -__global__ void cashInOwnerIndexA(deme::bodyID_t* idOwner, - deme::bodyID_t* id, - deme::bodyID_t* ownerClumpBody, - deme::contact_t* contactType, - size_t nContactPairs) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - deme::bodyID_t thisBodyID = id[myID]; - idOwner[myID] = ownerClumpBody[thisBodyID]; +inline __device__ deme::bodyID_t getPatchOwnerSafe(const deme::DEMSimParams* simParams, + const deme::DEMDataDT* granData, + deme::bodyID_t patch_id, + deme::geoType_t type) { + switch (type) { + case deme::GEO_T_SPHERE: + if (patch_id < simParams->nSpheresGM) { + return granData->ownerClumpBody[patch_id]; + } + break; + case deme::GEO_T_TRIANGLE: + if (patch_id < simParams->nMeshPatches) { + return granData->ownerPatchMesh[patch_id]; + } + break; + case deme::GEO_T_ANALYTICAL: + if (patch_id < simParams->nAnalGM) { + return granData->ownerAnalBody[patch_id]; + } + break; + default: + break; } + return deme::NULL_BODYID; } -__global__ void cashInOwnerIndexB(deme::bodyID_t* idOwner, - deme::bodyID_t* id, - deme::bodyID_t* ownerClumpBody, - deme::bodyID_t* ownerMesh, - deme::contact_t* contactType, - size_t nContactPairs) { +// computes a ./ b +DEME_KERNEL void forceToAcc(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, size_t n) { deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - deme::bodyID_t thisBodyID = id[myID]; - deme::contact_t thisCntType = contactType[myID]; - if (thisCntType == deme::SPHERE_SPHERE_CONTACT) { - idOwner[myID] = ownerClumpBody[thisBodyID]; - } else if (thisCntType == deme::SPHERE_MESH_CONTACT) { - idOwner[myID] = ownerMesh[thisBodyID]; - } else { - // This is a sphere--analytical geometry contact, its owner is jitified - idOwner[myID] = objOwner[thisBodyID]; + if (myID < n) { + deme::contact_t thisCntType = granData->contactTypePatch[myID]; + if (thisCntType == deme::NOT_A_CONTACT) { + return; + } + if (!deme::isSupportedContactType(thisCntType)) { + return; + } + const float3 F = granData->contactForces[myID]; + const float3 torque_only_force = granData->contactTorque_convToForce[myID]; + const deme::bodyID_t idPatchA = granData->idPatchA[myID]; + const deme::bodyID_t idPatchB = granData->idPatchB[myID]; + float3 forceA = F; + float3 forceB = make_float3(-F.x, -F.y, -F.z); + float3 torqueA = torque_only_force; + float3 torqueB = make_float3(-torque_only_force.x, -torque_only_force.y, -torque_only_force.z); + const deme::geoType_t typeA = deme::decodeTypeA(thisCntType); + const deme::geoType_t typeB = deme::decodeTypeB(thisCntType); + const deme::bodyID_t ownerA = getPatchOwnerSafe(simParams, granData, idPatchA, typeA); + const deme::bodyID_t ownerB = getPatchOwnerSafe(simParams, granData, idPatchB, typeB); + if (ownerA == deme::NULL_BODYID || ownerB == deme::NULL_BODYID || ownerA >= simParams->nOwnerBodies || + ownerB >= simParams->nOwnerBodies) { + return; + } + + // Take care of A + { + float myMass; + const deme::bodyID_t idPatch = idPatchA; + const deme::bodyID_t myOwner = ownerA; + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { _massAcqStrat_; } + + if (!isfinite3(forceA) || !isfinite3(torqueA)) { + DEME_ABORT_KERNEL("forceToAcc found invalid force/torque for A: contact %llu, owner %llu, type %u.\n", + static_cast(myID), static_cast(myOwner), + static_cast(thisCntType)); + } + + atomicAdd(granData->aX + myOwner, forceA.x / myMass); + atomicAdd(granData->aY + myOwner, forceA.y / myMass); + atomicAdd(granData->aZ + myOwner, forceA.z / myMass); + + // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque + // only, not linear velocity. A zero resultant cannot produce angular acceleration, so do not read a + // contact-point slot that a margin-only pair may never have written. Besides avoiding cross(NaN, 0), this + // skips the orientation and angular atomics for the common zero-force case without dropping linear force. + // The in-force-kernel collection policy does not need this memory-safety guard because it consumes the + // locally computed contact point instead of reading a persistent contact-point array. + float3 myF = forceA + torqueA; + if (myF.x != 0.0f || myF.y != 0.0f || myF.z != 0.0f) { + float3 myMOI; + { _moiAcqStrat_; } + const float3 myCntPnt = granData->contactPointGeometryA[myID]; + if (!isfinite3(myCntPnt)) { + DEME_ABORT_KERNEL( + "forceToAcc found invalid contact point for A: contact %llu, owner %llu, type %u.\n", + static_cast(myID), static_cast(myOwner), + static_cast(thisCntType)); + } + + const deme::oriQ_t myOriQw = granData->oriQw[myOwner]; + const deme::oriQ_t myOriQx = granData->oriQx[myOwner]; + const deme::oriQ_t myOriQy = granData->oriQy[myOwner]; + const deme::oriQ_t myOriQz = granData->oriQz[myOwner]; + // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt + applyOriQToVector3(myF, make_float4(-myOriQx, -myOriQy, -myOriQz, myOriQw)); + const float3 angAcc = cross(myCntPnt, myF) / myMOI; + atomicAdd(granData->alphaX + myOwner, angAcc.x); + atomicAdd(granData->alphaY + myOwner, angAcc.y); + atomicAdd(granData->alphaZ + myOwner, angAcc.z); + } + } + + // Take care of B + { + float myMass; + const deme::bodyID_t idPatch = idPatchB; + deme::bodyID_t myOwner = ownerB; + + // Get my mass info from either jitified arrays or global memory + // Outputs myMass + // Use an input named exactly `myOwner' which is the id of this owner + { _massAcqStrat_; } + + if (!isfinite3(forceB) || !isfinite3(torqueB)) { + DEME_ABORT_KERNEL("forceToAcc found invalid force/torque for B: contact %llu, owner %llu, type %u.\n", + static_cast(myID), static_cast(myOwner), + static_cast(thisCntType)); + } + + atomicAdd(granData->aX + myOwner, forceB.x / myMass); + atomicAdd(granData->aY + myOwner, forceB.y / myMass); + atomicAdd(granData->aZ + myOwner, forceB.z / myMass); + + // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque + // only, not linear velocity. Keep B's guard independent because its contact point has separate storage. + float3 myF = forceB + torqueB; + if (myF.x != 0.0f || myF.y != 0.0f || myF.z != 0.0f) { + float3 myMOI; + { _moiAcqStrat_; } + const float3 myCntPnt = granData->contactPointGeometryB[myID]; + if (!isfinite3(myCntPnt)) { + DEME_ABORT_KERNEL( + "forceToAcc found invalid contact point for B: contact %llu, owner %llu, type %u.\n", + static_cast(myID), static_cast(myOwner), + static_cast(thisCntType)); + } + + const deme::oriQ_t myOriQw = granData->oriQw[myOwner]; + const deme::oriQ_t myOriQx = granData->oriQx[myOwner]; + const deme::oriQ_t myOriQy = granData->oriQy[myOwner]; + const deme::oriQ_t myOriQz = granData->oriQz[myOwner]; + // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt + applyOriQToVector3(myF, make_float4(-myOriQx, -myOriQy, -myOriQz, myOriQw)); + const float3 angAcc = cross(myCntPnt, myF) / myMOI; + atomicAdd(granData->alphaX + myOwner, angAcc.x); + atomicAdd(granData->alphaY + myOwner, angAcc.y); + atomicAdd(granData->alphaZ + myOwner, angAcc.z); + } } } } -/* -__global__ void cashInMassMoiIndex(float* massOwner, - float3* moiOwner, - deme::inertiaOffset_t* inertiaPropOffsets, - deme::bodyID_t* idOwner, - size_t nContactPairs) { - // _nDistinctMassProperties_ elements are in these arrays - const float moiX[] = {_moiX_}; - const float moiY[] = {_moiY_}; - const float moiZ[] = {_moiZ_}; - const float MassProperties[] = {_MassProperties_}; +DEME_KERNEL void aggregateCombinedOwnersAcc(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::bodyID_t nOwners) { + // Device-side aggregation pass: + // each non-master member contributes its already-accumulated linear/angular accelerations to its master. + // Contributions are mass/MOI weighted so that master-side integration follows the equivalent-group properties. + deme::bodyID_t owner = blockIdx.x * blockDim.x + threadIdx.x; + if (owner >= nOwners || simParams->nCombinedOwners == 0 || granData->ownerCombinedMaster == nullptr) { + return; + } - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - deme::bodyID_t thisOwnerID = idOwner[myID]; - deme::inertiaOffset_t myMassOffset = inertiaPropOffsets[thisOwnerID]; - float3 moi; - moi.x = moiX[myMassOffset]; - moi.y = moiY[myMassOffset]; - moi.z = moiZ[myMassOffset]; - massOwner[myID] = MassProperties[myMassOffset]; - moiOwner[myID] = moi; + const deme::bodyID_t master = granData->ownerCombinedMaster[owner]; + // Master owners are intentionally skipped in this pass: only non-master members contribute to master. + if (master == deme::NULL_BODYID || master == owner || master >= simParams->nOwnerBodies) { + return; } -} -*/ -// computes a ./ b -__global__ void forceToAcc(float3* acc, - float3* F, - deme::bodyID_t* owner, - float modifier, - size_t n, - deme::DEMDataDT* granData) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - float myMass; - const deme::bodyID_t myOwner = owner[myID]; - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { _massAcqStrat_; } - acc[myID] = F[myID] * modifier / myMass; + float myMass; + float3 myMOI; + deme::bodyID_t myOwner = owner; + { + _massAcqStrat_; + _moiAcqStrat_; } -} + const float memberMass = myMass; + const float3 memberMOI = myMOI; -// computes cross(a, b) ./ c -__global__ void forceToAngAcc(float3* angAcc, - float3* cntPnt, - deme::oriQ_t* oriQw, - deme::oriQ_t* oriQx, - deme::oriQ_t* oriQy, - deme::oriQ_t* oriQz, - float3* F, - float3* torque_inForceForm, - deme::bodyID_t* owner, - float modifier, - size_t n, - deme::DEMDataDT* granData) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque only, not - // linear velocity - float3 myF = F[myID] + torque_inForceForm[myID]; - // Zero-force entries must contribute exactly zero angular acceleration without touching - // cntPnt: entries the force kernel never writes (pairs registered by kT's contact margin - // that have no physical overlap) can hold uninitialized device memory in their cntPnt - // slot, and cross(NaN, 0) is NaN, which would poison this owner's angular acceleration. - // These entries are the majority of the list, so the early-out also skips their MOI and - // quaternion loads. - if (myF.x == 0.0f && myF.y == 0.0f && myF.z == 0.0f) { - angAcc[myID] = make_float3(0, 0, 0); - return; + float masterMass = 0.f; + float3 masterMOI = make_float3(0); + if (granData->ownerCombinedMasterMass != nullptr) { + masterMass = granData->ownerCombinedMasterMass[master]; + } + if (granData->ownerCombinedMasterMOI != nullptr) { + masterMOI = granData->ownerCombinedMasterMOI[master]; + } + if (masterMass <= DEME_TINY_FLOAT || !isfinite(masterMass)) { + // Defensive fallback: if equivalent master properties are unavailable, fall back to master's own properties. + myOwner = master; + { + _massAcqStrat_; + _moiAcqStrat_; } - const deme::bodyID_t myOwner = owner[myID]; - float3 myMOI; - // Get my mass info from either jitified arrays or global memory - // Outputs myMOI - // Use an input named exactly `myOwner' which is the id of this owner - { _moiAcqStrat_; } - const deme::oriQ_t myOriQw = oriQw[myOwner]; - const deme::oriQ_t myOriQx = oriQx[myOwner]; - const deme::oriQ_t myOriQy = oriQy[myOwner]; - const deme::oriQ_t myOriQz = oriQz[myOwner]; - - float3 myCntPnt = cntPnt[myID]; - myF = myF * modifier; - // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt - applyOriQToVector3(myF.x, myF.y, myF.z, myOriQw, -myOriQx, -myOriQy, -myOriQz); - angAcc[myID] = cross(myCntPnt, myF) / myMOI; + masterMass = myMass; + masterMOI = myMOI; + } + + if (memberMass > DEME_TINY_FLOAT && masterMass > DEME_TINY_FLOAT) { + // Convert member acceleration contribution to master acceleration space. + const float ratio_m = memberMass / masterMass; + atomicAdd(granData->aX + master, granData->aX[owner] * ratio_m); + atomicAdd(granData->aY + master, granData->aY[owner] * ratio_m); + atomicAdd(granData->aZ + master, granData->aZ[owner] * ratio_m); } + + if (masterMOI.x > DEME_TINY_FLOAT && masterMOI.y > DEME_TINY_FLOAT && masterMOI.z > DEME_TINY_FLOAT) { + float4 qMaster = make_float4(granData->oriQx[master], granData->oriQy[master], granData->oriQz[master], + granData->oriQw[master]); + float3 memberForceMaster = + memberMass * make_float3(granData->aX[owner], granData->aY[owner], granData->aZ[owner]); + applyOriQToVector3(memberForceMaster, make_float4(-qMaster.x, -qMaster.y, -qMaster.z, qMaster.w)); + + float3 torqueMaster = make_float3(0); + if (granData->ownerCombinedRelPos != nullptr) { + torqueMaster += cross(granData->ownerCombinedRelPos[owner], memberForceMaster); + } + + float3 memberTorqueMaster = + memberMOI * make_float3(granData->alphaX[owner], granData->alphaY[owner], granData->alphaZ[owner]); + if (granData->ownerCombinedRelOriQ != nullptr) { + applyOriQToVector3(memberTorqueMaster, granData->ownerCombinedRelOriQ[owner]); + } + torqueMaster += memberTorqueMaster; + + atomicAdd(granData->alphaX + master, torqueMaster.x / masterMOI.x); + atomicAdd(granData->alphaY + master, torqueMaster.y / masterMOI.y); + atomicAdd(granData->alphaZ + master, torqueMaster.z / masterMOI.z); + } + + granData->aX[owner] = 0.f; + granData->aY[owner] = 0.f; + granData->aZ[owner] = 0.f; + granData->alphaX[owner] = 0.f; + granData->alphaY[owner] = 0.f; + granData->alphaZ[owner] = 0.f; } -// Place information to an array based on an index array and a value array -__global__ void stashElem(float* out1, float* out2, float* out3, deme::bodyID_t* index, float3* value, size_t n) { - deme::bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - // my_index is unique, no race condition - deme::bodyID_t my_index = index[myID]; - float3 my_value = value[myID]; - out1[my_index] += my_value.x; - out2[my_index] += my_value.y; - out3[my_index] += my_value.z; +DEME_KERNEL void reimposeCombinedOwners(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + deme::bodyID_t nOwners) { + // Device-side rigid re-imposition pass: + // after integrating masters, overwrite each member's state from + // master state + fixed member transform in master frame. + // This enforces rigid relative motion without host/device synchronization. + deme::bodyID_t owner = blockIdx.x * blockDim.x + threadIdx.x; + if (owner >= nOwners || simParams->nCombinedOwners == 0 || granData->ownerCombinedMaster == nullptr || + granData->ownerCombinedRelPos == nullptr || granData->ownerCombinedRelOriQ == nullptr) { + return; } + + const deme::bodyID_t master = granData->ownerCombinedMaster[owner]; + // Master owners are intentionally skipped in this pass: only non-master members are re-imposed. + if (master == deme::NULL_BODYID || master == owner || master >= simParams->nOwnerBodies) { + return; + } + + double mX, mY, mZ; + voxelIDToPosition( + mX, mY, mZ, granData->voxelID[master], granData->locX[master], granData->locY[master], granData->locZ[master], + _nvXp2_, _nvYp2_, _voxelSize_, _l_); + mX += (double)simParams->LBFX; + mY += (double)simParams->LBFY; + mZ += (double)simParams->LBFZ; + + float4 qMaster = + make_float4(granData->oriQx[master], granData->oriQy[master], granData->oriQz[master], granData->oriQw[master]); + const float3 relLocal = granData->ownerCombinedRelPos[owner]; + float3 relWorld = relLocal; + applyOriQToVector3(relWorld, qMaster); + + double X = mX + (double)relWorld.x; + double Y = mY + (double)relWorld.y; + double Z = mZ + (double)relWorld.z; + X -= (double)simParams->LBFX; + Y -= (double)simParams->LBFY; + Z -= (double)simParams->LBFZ; + positionToVoxelID(granData->voxelID[owner], granData->locX[owner], + granData->locY[owner], granData->locZ[owner], X, Y, + Z, _nvXp2_, _nvYp2_, _voxelSize_, _l_); + + float4 qRel = granData->ownerCombinedRelOriQ[owner]; + float qW, qX, qY, qZ; + HamiltonProduct(qW, qX, qY, qZ, qMaster.w, qMaster.x, qMaster.y, qMaster.z, qRel.w, qRel.x, qRel.y, qRel.z); + const float qn = sqrtf(qW * qW + qX * qX + qY * qY + qZ * qZ); + if (qn > DEME_TINY_FLOAT && isfinite(qn)) { + const float inv_qn = 1.f / qn; + qW *= inv_qn; + qX *= inv_qn; + qY *= inv_qn; + qZ *= inv_qn; + } else { + qW = 1.f; + qX = qY = qZ = 0.f; + } + granData->oriQw[owner] = qW; + granData->oriQx[owner] = qX; + granData->oriQy[owner] = qY; + granData->oriQz[owner] = qZ; + + float3 wMasterLocal = make_float3(granData->omgBarX[master], granData->omgBarY[master], granData->omgBarZ[master]); + float3 wMasterWorld = wMasterLocal; + applyOriQToVector3(wMasterWorld, qMaster); + + float3 vMaster = make_float3(granData->vX[master], granData->vY[master], granData->vZ[master]); + // Rigid-body kinematics for member linear velocity: v = v_master + omega_master x r. + float3 vMember = vMaster + cross(wMasterWorld, relWorld); + granData->vX[owner] = vMember.x; + granData->vY[owner] = vMember.y; + granData->vZ[owner] = vMember.z; + + float4 qMemberConj = make_float4(-qX, -qY, -qZ, qW); + float3 wMemberLocal = wMasterWorld; + applyOriQToVector3(wMemberLocal, qMemberConj); + granData->omgBarX[owner] = wMemberLocal.x; + granData->omgBarY[owner] = wMemberLocal.y; + granData->omgBarZ[owner] = wMemberLocal.z; } diff --git a/src/kernel/DEMCollectForceKernels_Compact.cu b/src/kernel/DEMCollectForceKernels_Compact.cu deleted file mode 100644 index b829f259..00000000 --- a/src/kernel/DEMCollectForceKernels_Compact.cu +++ /dev/null @@ -1,102 +0,0 @@ -// DEM force computation related custom kernels -#include -#include -_kernelIncludes_; - -// For analytical entities' owners -__constant__ __device__ deme::bodyID_t objOwner[] = {_objOwner_}; -// Mass properties are below, if jitified mass properties are in use -_massDefs_; -_moiDefs_; - -// computes a ./ b -__global__ void forceToAcc(deme::DEMDataDT* granData, size_t n) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::contact_t thisCntType = granData->contactType[myID]; - const float3 F = granData->contactForces[myID]; - - // Take care of A - { - float myMass; - float3 myMOI; - const deme::bodyID_t idGeo = granData->idGeometryA[myID]; - const float3 myCntPnt = granData->contactPointGeometryA[myID]; - - const deme::bodyID_t myOwner = granData->ownerClumpBody[idGeo]; - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { - _massAcqStrat_; - _moiAcqStrat_; - } - - atomicAdd(granData->aX + myOwner, F.x / myMass); - atomicAdd(granData->aY + myOwner, F.y / myMass); - atomicAdd(granData->aZ + myOwner, F.z / myMass); - - // Then ang acc - const deme::oriQ_t myOriQw = granData->oriQw[myOwner]; - const deme::oriQ_t myOriQx = granData->oriQx[myOwner]; - const deme::oriQ_t myOriQy = granData->oriQy[myOwner]; - const deme::oriQ_t myOriQz = granData->oriQz[myOwner]; - - // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque only, - // not linear velocity - float3 myF = (F + granData->contactTorque_convToForce[myID]); - // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt - applyOriQToVector3(myF.x, myF.y, myF.z, myOriQw, -myOriQx, -myOriQy, -myOriQz); - const float3 angAcc = cross(myCntPnt, myF) / myMOI; - atomicAdd(granData->alphaX + myOwner, angAcc.x); - atomicAdd(granData->alphaY + myOwner, angAcc.y); - atomicAdd(granData->alphaZ + myOwner, angAcc.z); - } - - // Take care of B - { - float myMass; - float3 myMOI; - const deme::bodyID_t idGeo = granData->idGeometryB[myID]; - const float3 myCntPnt = granData->contactPointGeometryB[myID]; - - deme::bodyID_t myOwner; - if (thisCntType == deme::SPHERE_SPHERE_CONTACT) { - myOwner = granData->ownerClumpBody[idGeo]; - } else if (thisCntType == deme::SPHERE_MESH_CONTACT) { - myOwner = granData->ownerMesh[idGeo]; - } else { - // This is a sphere--analytical geometry contact, its owner is jitified - myOwner = objOwner[idGeo]; - } - - // Get my mass info from either jitified arrays or global memory - // Outputs myMass - // Use an input named exactly `myOwner' which is the id of this owner - { - _massAcqStrat_; - _moiAcqStrat_; - } - - atomicAdd(granData->aX + myOwner, -F.x / myMass); - atomicAdd(granData->aY + myOwner, -F.y / myMass); - atomicAdd(granData->aZ + myOwner, -F.z / myMass); - - // Then ang acc - const deme::oriQ_t myOriQw = granData->oriQw[myOwner]; - const deme::oriQ_t myOriQx = granData->oriQx[myOwner]; - const deme::oriQ_t myOriQy = granData->oriQy[myOwner]; - const deme::oriQ_t myOriQz = granData->oriQz[myOwner]; - - // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque only, - // not linear velocity - float3 myF = -1.f * (F + granData->contactTorque_convToForce[myID]); - // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt - applyOriQToVector3(myF.x, myF.y, myF.z, myOriQw, -myOriQx, -myOriQy, -myOriQz); - const float3 angAcc = cross(myCntPnt, myF) / myMOI; - atomicAdd(granData->alphaX + myOwner, angAcc.x); - atomicAdd(granData->alphaY + myOwner, angAcc.y); - atomicAdd(granData->alphaZ + myOwner, angAcc.z); - } - } -} diff --git a/src/kernel/DEMCollisionKernels.cu b/src/kernel/DEMCollisionKernels.cu deleted file mode 100644 index 2712b431..00000000 --- a/src/kernel/DEMCollisionKernels.cu +++ /dev/null @@ -1,236 +0,0 @@ -// DEM collision-related kernel collection - -#ifndef DEME_COLLI_KERNELS_CU -#define DEME_COLLI_KERNELS_CU - -#include - -/// This utility function takes the location 'P' and snaps it to the closest -/// point on the triangular face with given vertices (A, B, and C). The result -/// is returned in 'res'. Both 'P' and 'res' are assumed to be specified in -/// the same frame as the face vertices. This function returns 'true' if the -/// result is on an edge of this face and 'false' if the result is inside the -/// triangle. -/// Code from Ericson, "real-time collision detection", 2005, pp. 141 -template -__device__ bool snap_to_face(const T1& A, const T1& B, const T1& C, const T1& P, T1& res) { - T1 AB = B - A; - T1 AC = C - A; - - // Check if P in vertex region outside A - T1 AP = P - A; - T2 d1 = dot(AB, AP); - T2 d2 = dot(AC, AP); - if (d1 <= 0 && d2 <= 0) { - res = A; // barycentric coordinates (1,0,0) - return true; - } - - // Check if P in vertex region outside B - T1 BP = P - B; - T2 d3 = dot(AB, BP); - T2 d4 = dot(AC, BP); - if (d3 >= 0 && d4 <= d3) { - res = B; // barycentric coordinates (0,1,0) - return true; - } - - // Check if P in edge region of AB - T2 vc = d1 * d4 - d3 * d2; - if (vc <= 0 && d1 >= 0 && d3 <= 0) { - // Return projection of P onto AB - T2 v = d1 / (d1 - d3); - res = A + v * AB; // barycentric coordinates (1-v,v,0) - return true; - } - - // Check if P in vertex region outside C - T1 CP = P - C; - T2 d5 = dot(AB, CP); - T2 d6 = dot(AC, CP); - if (d6 >= 0 && d5 <= d6) { - res = C; // barycentric coordinates (0,0,1) - return true; - } - - // Check if P in edge region of AC - T2 vb = d5 * d2 - d1 * d6; - if (vb <= 0 && d2 >= 0 && d6 <= 0) { - // Return projection of P onto AC - T2 w = d2 / (d2 - d6); - res = A + w * AC; // barycentric coordinates (1-w,0,w) - return true; - } - - // Check if P in edge region of BC - T2 va = d3 * d6 - d5 * d4; - if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) { - // Return projection of P onto BC - T2 w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); - res = B + w * (C - B); // barycentric coordinates (0,1-w,w) - return true; - } - - // P inside face region. Return projection of P onto face - // barycentric coordinates (u,v,w) - T2 denom = __drcp_ru(va + vb + vc); - T2 v = __dmul_ru(vb, denom); - T2 w = __dmul_ru(vc, denom); - res = A + v * AB + w * AC; // = u*A + v*B + w*C where (u = 1 - v - w) - return false; -} - -/** -/brief TRIANGLE FACE--SPHERE COLLISION DETECTION - -The triangular face is defined by points A, B, C. The sequence is important as it defines the positive face via a -right-hand rule. -The sphere is centered at sphere_pos and has radius. -The index "1" is associated with the triangle. The index "2" is associated with the sphere. -The coordinates of the face and sphere are assumed to be provided in the same reference frame. - -Output: - - pt1: contact point on triangle - - depth: penetration distance (a negative value means that overlap exists) - - normal: contact normal, from pt2 to pt1 -A return value of "true" signals collision. -*/ -template -__device__ bool triangle_sphere_CD(const T1& A, ///< First vertex of the triangle - const T1& B, ///< Second vertex of the triangle - const T1& C, ///< Third vertex of the triangle - const T1& sphere_pos, ///< Location of the center of the sphere - const T2 radius, ///< Sphere radius - T1& normal, ///< contact normal - T2& depth, ///< penetration (negative if in contact) - T1& pt1 ///< contact point on triangle -) { - // Calculate face normal using RHR - T1 face_n = normalize(cross(B - A, C - A)); - - // Calculate signed height of sphere center above face plane - T2 h = dot(sphere_pos - A, face_n); - - // Find the closest point on the face to the sphere center and determine - // whether or not this location is inside the face or on an edge. - T1 faceLoc; - - // Triangle in contact with sphere or not - bool in_contact; - - // Still do the following since we need depth - if (!snap_to_face(A, B, C, sphere_pos, faceLoc)) { - // Nearest point on the triangle is on its face - // printf("FACE CONTACT\n"); - depth = h - radius; - normal.x = face_n.x; - normal.y = face_n.y; - normal.z = face_n.z; - pt1 = faceLoc; - if (h >= radius || h <= -radius) { - in_contact = false; - } else { - in_contact = true; - } - } else { - // printf("EDGE CONTACT\n"); - // Nearest point on the triangle is on an edge - { - T1 normal_d = sphere_pos - faceLoc; - normal.x = normal_d.x; - normal.y = normal_d.y; - normal.z = normal_d.z; - } - T2 dist = length(normal); - depth = dist - radius; - - normal = (1.0 / dist) * normal; - pt1 = faceLoc; - if (depth >= 0. || h >= radius || h <= -radius) { - in_contact = false; - } else { - in_contact = true; - } - } - return in_contact; -} - -/** -/brief TRIANGLE FACE--SPHERE COLLISION DETECTION (DIRECTIONAL) - -The triangular face is defined by points A, B, C. The sequence is important as it defines the positive face via a -right-hand rule. -The sphere is centered at sphere_pos and has radius. -The index "1" is associated with the triangle. The index "2" is associated with the sphere. -The coordinates of the face and sphere are assumed to be provided in the same reference frame. -This flavor is directional, meaning if a sphere is not in (geometric) contact with a triangle, it may still be -considered in contact, if it is submerged in the (closed) object that this mesh is representing, or say the penetration -is too deep, large than 2 * sphere rad. - -Output: - - pt1: contact point on triangle - - depth: penetration distance (a negative value means that overlap exists) - - normal: contact normal, from pt2 to pt1 -A return value of "true" signals collision. -*/ -template -__device__ bool triangle_sphere_CD_directional(const T1& A, ///< First vertex of the triangle - const T1& B, ///< Second vertex of the triangle - const T1& C, ///< Third vertex of the triangle - const T1& sphere_pos, ///< Location of the center of the sphere - const T2 radius, ///< Sphere radius - T1& normal, ///< contact normal - T2& depth, ///< penetration (negative if in contact) - T1& pt1 ///< contact point on triangle -) { - // Calculate face normal using RHR - T1 face_n = normalize(cross(B - A, C - A)); - - // Calculate signed height of sphere center above face plane - T2 h = dot(sphere_pos - A, face_n); - - // Find the closest point on the face to the sphere center and determine - // whether or not this location is inside the face or on an edge. - T1 faceLoc; - - // Triangle in contact with sphere or not - bool in_contact; - - // Still do the following since we need depth - if (!snap_to_face(A, B, C, sphere_pos, faceLoc)) { - // Nearest point on the triangle is on its face - // printf("FACE CONTACT\n"); - depth = h - radius; - normal.x = face_n.x; - normal.y = face_n.y; - normal.z = face_n.z; - pt1 = faceLoc; - if (depth >= 0.) { - in_contact = false; - } else { - in_contact = true; - } - } else { - // printf("EDGE CONTACT\n"); - // Nearest point on the triangle is on an edge - { - T1 normal_d = sphere_pos - faceLoc; - normal.x = normal_d.x; - normal.y = normal_d.y; - normal.z = normal_d.z; - } - T2 dist = length(normal); - depth = dist - radius; - - normal = (1.0 / dist) * normal; - pt1 = faceLoc; - if (depth >= 0. || h >= radius) { - in_contact = false; - } else { - in_contact = true; - } - } - return in_contact; -} - -#endif diff --git a/src/kernel/DEMCollisionKernels_SphSph.cuh b/src/kernel/DEMCollisionKernels_SphSph.cuh new file mode 100644 index 00000000..c224ba8f --- /dev/null +++ b/src/kernel/DEMCollisionKernels_SphSph.cuh @@ -0,0 +1,228 @@ +// DEM collision-related kernel collection + +#ifndef DEME_COLLI_KERNELS_SS_CUH +#define DEME_COLLI_KERNELS_SS_CUH + +#include +#include + +template +inline __device__ void equipOwnerPosRot(deme::DEMSimParams* simParams, + deme::DEMDataDT* granData, + const deme::bodyID_t& myOwner, + T1& relPos, + double3& ownerPos, + double3& bodyPos, + float4& oriQ) { + voxelIDToPosition( + ownerPos.x, ownerPos.y, ownerPos.z, granData->voxelID[myOwner], granData->locX[myOwner], + granData->locY[myOwner], granData->locZ[myOwner], simParams->nvXp2, simParams->nvYp2, simParams->voxelSize, + simParams->l); + // Do this and we get the `true' pos... + ownerPos.x += simParams->LBFX; + ownerPos.y += simParams->LBFY; + ownerPos.z += simParams->LBFZ; + oriQ.w = granData->oriQw[myOwner]; + oriQ.x = granData->oriQx[myOwner]; + oriQ.y = granData->oriQy[myOwner]; + oriQ.z = granData->oriQz[myOwner]; + applyOriQToVector3(relPos, oriQ); + bodyPos.x = ownerPos.x + (double)relPos.x; + bodyPos.y = ownerPos.y + (double)relPos.y; + bodyPos.z = ownerPos.z + (double)relPos.z; +} + +/** + * Template arguments: + * - T1: the floating point accuracy level for contact point location/penetration depth + * - T2: the floating point accuracy level for the relative position of 2 bodies involved + * + * Basic idea: determines whether 2 spheres intersect and the intersection point coordinates which also gives the + * penetration length and bodyB's outward contact normal. + * + */ +template +__host__ __device__ deme::contact_t checkSpheresOverlap(const T1& XA, + const T1& YA, + const T1& ZA, + const T1& radA, + const T1& XB, + const T1& YB, + const T1& ZB, + const T1& radB, + T1& CPX, + T1& CPY, + T1& CPZ, + T2& normalX, + T2& normalY, + T2& normalZ, + T1& overlapDepth, + T1& overlapArea) { + T1 centerDist2 = distSquared(XA, YA, ZA, XB, YB, ZB); + deme::contact_t contactTypePrimitive; + if (centerDist2 > (radA + radB) * (radA + radB)) { + contactTypePrimitive = deme::NOT_A_CONTACT; + } else { + contactTypePrimitive = deme::SPHERE_SPHERE_CONTACT; + } + // If getting this far, then 2 spheres have an intersection, let's calculate the intersection point + normalX = XA - XB; + normalY = YA - YB; + normalZ = ZA - ZB; + normalizeVector3(normalX, normalY, normalZ); + overlapDepth = radA + radB - sqrt(centerDist2); + // overlap area in the sph-sph case will involve only half the overlap depth + // overlapArea = deme::PI * (radA * radA - (radA - overlapDepth / (T1)2) * (radA - overlapDepth / (T1)2)); + // Simplify it... + overlapArea = deme::PI * (radA * overlapDepth - overlapDepth * overlapDepth / (T1)4); + // From center of B, towards center of A, move a distance of radB, then backtrack a bit, for half the overlap depth + CPX = XB + (radB - overlapDepth / (T1)2) * normalX; + CPY = YB + (radB - overlapDepth / (T1)2) * normalY; + CPZ = ZB + (radB - overlapDepth / (T1)2) * normalZ; + return contactTypePrimitive; +} + +// Return a deterministic radial direction for points on a cone axis, where the closest direction is non-unique. +inline __host__ __device__ float3 anyPerpendicularUnitVector(const float3& axis) { + float3 reference = (fabsf(axis.x) < 0.5f) ? make_float3(1, 0, 0) : make_float3(0, 1, 0); + float3 perpendicular = cross(axis, reference); + if (length(perpendicular) < DEME_TINY_FLOAT) { + reference = make_float3(0, 0, 1); + perpendicular = cross(axis, reference); + } + return normalize(perpendicular); +} + +// Check whether a sphere and an analytical boundary are in contact, and gives overlap depth, contact point and contact +// normal. Returned contact type is only useful for kT to sort contact types, as for dT's force calculation, the flavor +// used is determined by type B's actual objType. +template +__host__ __device__ deme::contact_t checkSphereEntityOverlap(const T1& A, + const T2& radA, + const deme::objType_t& typeB, + const T1& B, + const float3& dirB, + const float& size1B, + const float& size2B, + const float& size3B, + const float& normal_sign, + const float& beta4Entity, + T1& CP, + float3& cntNormal, + T3& overlapDepth, + T3& overlapArea) { + deme::contact_t contactTypePrimitive; + switch (typeB) { + case (deme::ANAL_OBJ_TYPE_PLANE): { + // Plane is directional, and the direction is given by plane rotation + const T3 dist = planeSignedDistance(A, B, dirB); + overlapDepth = (radA + beta4Entity - dist); + if (overlapDepth < 0.0) { + contactTypePrimitive = deme::NOT_A_CONTACT; + overlapArea = 0.0; + } else { + contactTypePrimitive = deme::SPHERE_ANALYTICAL_CONTACT; + // Approximate overlap area as circle area + overlapArea = deme::PI * (radA * radA - (dist - beta4Entity) * (dist - beta4Entity)); + } + // From sphere center, go along reverse plane normal for (dist + overlapDepth / 2) + CP = A - to_real3(dirB * (dist + overlapDepth / 2.0)); + // Contact normal (B to A) is the same as plane normal + cntNormal = dirB; + return contactTypePrimitive; + } + case (deme::ANAL_OBJ_TYPE_CYL_INF): { + T1 cyl2sph = cylRadialDistanceVec(A, B, dirB); + const T3 dist_delta_r = length(cyl2sph); + // The margin expands an exterior cylinder and shrinks an interior one in the direction of its normal. + const float cyl_rad = size1B - normal_sign * beta4Entity; + overlapDepth = radA - normal_sign * (cyl_rad - dist_delta_r); + if (overlapDepth <= DEME_TINY_FLOAT) { + contactTypePrimitive = deme::NOT_A_CONTACT; + overlapArea = 0.0; + } else { + contactTypePrimitive = deme::SPHERE_ANALYTICAL_CONTACT; + // Approximate overlap area as circle area + // overlapArea = deme::PI * (radA * radA - (radA - overlapDepth) * (radA - overlapDepth)); + // Simplify it... + overlapArea = deme::PI * (2.0 * radA * overlapDepth - overlapDepth * overlapDepth); + } + // Inward normal is 1, outward is -1, so flip normal_sign for B2A vector + // A sphere exactly on the cylinder axis has no radial direction. Use the cylinder axis as a deterministic + // finite fallback instead of dividing by zero. + if (dist_delta_r >= (T3)DEME_TINY_FLOAT) { + cntNormal = to_real3(-normal_sign / dist_delta_r * cyl2sph); + CP = A - to_real3(cntNormal * (radA - overlapDepth / 2.0)); + } else { + cntNormal = dirB; + CP = A; + } + return contactTypePrimitive; + } + case (deme::ANAL_OBJ_TYPE_CONE_INF): + case (deme::ANAL_OBJ_TYPE_CONE): { + const T3 cone_slope = (T3)size1B; + const T3 min_h = (T3)size2B; + const T3 max_h = (T3)size3B; + const T1 tip2sph = A - B; + const T1 cone_axis = to_real3(dirB); + const T3 axial_dist = dot(tip2sph, cone_axis); + const T1 radial_vec = tip2sph - cone_axis * axial_dist; + const T3 radial_dist = length(radial_vec); + const float3 radial_dir = (radial_dist >= (T3)DEME_TINY_FLOAT) + ? to_real3(radial_vec / radial_dist) + : anyPerpendicularUnitVector(dirB); + + // Project the sphere center onto the cone generator in (axial distance, radius) space. Clamping this + // projection supplies exact apex/rim contact for bounded cone segments without adding mesh facets. + const T3 inv_slope_metric = (T3)1.0 / ((T3)1.0 + cone_slope * cone_slope); + T3 closest_h = (axial_dist + cone_slope * radial_dist) * inv_slope_metric; + bool closest_is_edge = false; + if (closest_h < min_h) { + closest_h = min_h; + closest_is_edge = true; + } else if (closest_h > max_h) { + closest_h = max_h; + closest_is_edge = true; + } + + if (closest_is_edge) { + const T1 closest = + B + cone_axis * closest_h + to_real3(radial_dir) * (cone_slope * closest_h); + const T1 feature2sph = A - closest; + const T3 dist_to_feature = length(feature2sph); + overlapDepth = (T3)radA + (T3)beta4Entity - dist_to_feature; + contactTypePrimitive = (overlapDepth < (T3)0.0) ? deme::NOT_A_CONTACT : deme::SPHERE_ANALYTICAL_CONTACT; + overlapArea = (contactTypePrimitive == deme::NOT_A_CONTACT) + ? (T3)0.0 + : deme::PI * ((T3)2.0 * (T3)radA * overlapDepth - overlapDepth * overlapDepth); + + if (dist_to_feature >= (T3)DEME_TINY_FLOAT) { + cntNormal = to_real3(feature2sph / dist_to_feature); + } else { + const T3 side_normal_len = sqrt((T3)1.0 + cone_slope * cone_slope); + cntNormal = normal_sign * ((float)cone_slope * dirB - radial_dir) / side_normal_len; + } + CP = A - to_real3(cntNormal * ((T3)radA - overlapDepth / (T3)2.0)); + return contactTypePrimitive; + } + + // Directional side contact follows the analytical-cylinder convention: inward normals retain particles + // inside the cone, while outward normals model the exterior of a solid cone. + const T3 side_normal_len = sqrt((T3)1.0 + cone_slope * cone_slope); + const T3 signed_gap = (T3)normal_sign * (cone_slope * axial_dist - radial_dist) / side_normal_len; + overlapDepth = (T3)radA + (T3)beta4Entity - signed_gap; + contactTypePrimitive = (overlapDepth < (T3)0.0) ? deme::NOT_A_CONTACT : deme::SPHERE_ANALYTICAL_CONTACT; + overlapArea = (contactTypePrimitive == deme::NOT_A_CONTACT) + ? (T3)0.0 + : deme::PI * ((T3)2.0 * (T3)radA * overlapDepth - overlapDepth * overlapDepth); + cntNormal = normal_sign * ((float)cone_slope * dirB - radial_dir) / side_normal_len; + CP = A - to_real3(cntNormal * ((T3)radA - overlapDepth / (T3)2.0)); + return contactTypePrimitive; + } + default: + return deme::NOT_A_CONTACT; + } +} + +#endif diff --git a/src/kernel/DEMCollisionKernels_SphTri_TriTri.cuh b/src/kernel/DEMCollisionKernels_SphTri_TriTri.cuh new file mode 100644 index 00000000..6499b13e --- /dev/null +++ b/src/kernel/DEMCollisionKernels_SphTri_TriTri.cuh @@ -0,0 +1,1600 @@ +// DEM collision-related kernel collection + +#ifndef DEME_COLLI_KERNELS_ST_TT_CUH +#define DEME_COLLI_KERNELS_ST_TT_CUH + +#include +#include + +// ------------------------------------------------------------------ +// Triangle-analytical object collision detection utilities +// ------------------------------------------------------------------ + +template +bool __device__ tri_plane_penetration(const T1** tri, + const T1& entityLoc, + const float3& entityDir, + T2& overlapDepth, + T2& overlapArea, + T1& contactPnt) { + // signed distances + T2 d[3]; + // penetration depth: deepest point in triangle + T2 dmin = DEME_HUGE_FLOAT; +#pragma unroll + for (int i = 0; i < 3; ++i) { + d[i] = planeSignedDistance(*tri[i], entityLoc, entityDir); + if (d[i] < dmin) + dmin = d[i]; + } + // build clipped polygon + T1 poly[4]; + int nNode = 0; // max 4 poly nodes + bool hasIntersection = false; // one edge intersecting the plane +#pragma unroll + for (int i = 0; i < 3; ++i) { + int j = (i + 1) % 3; // compare with next vertex + bool in_i = (d[i] < 0.0); + bool in_j = (d[j] < 0.0); + + // ^ means one is in, the other is out + if (in_i ^ in_j) { + T2 t = d[i] / (d[i] - d[j]); // between 0 and 1 + T1 inter = *tri[i] + (*tri[j] - *tri[i]) * t; + // Only register inside points once - project them onto the plane + if (in_i) { + // Project the submerging node onto the plane + T1 projectedNode = *tri[i] - d[i] * to_real3(entityDir); + poly[nNode++] = projectedNode; + } + poly[nNode++] = inter; + hasIntersection = true; + } + } + + // Handle the case where all three vertices are submerged (no edge crosses the plane) + if (!hasIntersection) { + // Check if all vertices are below the plane + bool allBelow = true; +#pragma unroll + for (int i = 0; i < 3; ++i) { + if (d[i] >= 0.0) { + allBelow = false; + break; + } + } + if (allBelow) { + // All vertices are below the plane - project all three onto the plane +#pragma unroll + for (int i = 0; i < 3; ++i) { + T1 projectedNode = *tri[i] - d[i] * to_real3(entityDir); + poly[nNode++] = projectedNode; + } + hasIntersection = true; // We now have a valid polygon + } + } + + // centroid of contact + T1 centroid; + centroid.x = 0.; + centroid.y = 0.; + centroid.z = 0.; + if (hasIntersection) { // If has intersection, centroid of the (inside) polygon + for (int i = 0; i < nNode; i++) + centroid = centroid + poly[i]; + centroid = centroid / T2(nNode); + } else { // If no intersection, centroid is just average of all tri verts +#pragma unroll + for (int i = 0; i < 3; i++) + centroid = centroid + *tri[i]; + centroid = centroid / 3.0; + } + + // We use the convention that if in contact, overlapDepth is positive + overlapDepth = -dmin; + bool in_contact = (overlapDepth >= 0.); + // The centroid's projection to the plane + T1 projection = + centroid - planeSignedDistance(centroid, entityLoc, entityDir) * to_real3(entityDir); + + // Calculate the area of the clipping polygon using fan triangulation from centroid + overlapArea = 0.0; + if (hasIntersection && nNode >= 3) { + for (int i = 0; i < nNode; ++i) { + T1 v1 = poly[i] - centroid; + T1 v2 = poly[(i + 1) % nNode] - centroid; + T1 crossProd = cross(v1, v2); + overlapArea += sqrt(dot(crossProd, crossProd)); + } + overlapArea *= static_cast(0.5); + } + + // cntPnt is from the projection point, go half penetration depth. + // Note this penetration depth is signed, so if no contact, we go positive plane normal; if in contact, we go + // negative plane normal. As such, cntPnt always exists and this is important for the cases with extraMargin. + contactPnt = projection - (overlapDepth * 0.5) * to_real3(entityDir); + return in_contact; +} + +// Used to approximate tri--cyl contact +template +inline __host__ __device__ bool planar_cyl_plane_from_ref(const T1& ref, + const T1& entityLoc, + const float3& entityDir, + const float& radius, + const float& normal_sign, + T1& plane_point, + float3& plane_normal) { + T1 radial_vec = cylRadialDistanceVec(ref, entityLoc, entityDir); + const auto dist = length(radial_vec); + if (dist <= (decltype(dist))DEME_TINY_FLOAT) { + return false; + } + const T1 radial_dir = radial_vec / dist; + const float dist_plane = normal_sign * (radius - (float)dist); + if (dist_plane < 0) { + return false; + } + plane_normal = to_real3(-normal_sign * radial_dir); + const T1 axis_point = ref - radial_vec; + plane_point = axis_point + radial_dir * radius; + return true; +} + +// Check only, no contact point, depth, area output +template +__host__ __device__ deme::contact_t checkTriEntityOverlap(const T1& A, + const T1& B, + const T1& C, + const deme::objType_t& typeB, + const T1& entityLoc, + const float3& entityDir, + const float& entitySize1, + const float& entitySize2, + const float& entitySize3, + const float& normal_sign, + const float& beta4Entity) { + const T1* tri[] = {&A, &B, &C}; + switch (typeB) { + case (deme::ANAL_OBJ_TYPE_PLANE): { + for (const T1*& v : tri) { + // Always cast to double + double d = planeSignedDistance(*v, entityLoc, entityDir); + double overlapDepth = beta4Entity - d; + // printf("v point %f %f %f, entityLoc %f %f %f\n", v->x, v->y, v->z, entityLoc.x, entityLoc.y, + // entityLoc.z); + if (overlapDepth >= 0.0) + return deme::TRIANGLE_ANALYTICAL_CONTACT; + } + return deme::NOT_A_CONTACT; + } + case (deme::ANAL_OBJ_TYPE_CYL_INF): { + // Uses a plane-based approximation + T1 centroid = (A + B + C) / 3.0; + T1 plane_point; + float3 plane_normal; + if (!planar_cyl_plane_from_ref(centroid, entityLoc, entityDir, entitySize1, normal_sign, plane_point, + plane_normal)) { + return deme::NOT_A_CONTACT; + } + for (const T1*& v : tri) { + double d = planeSignedDistance(*v, plane_point, plane_normal); + double overlapDepth = beta4Entity - d; + if (overlapDepth >= 0.0) + return deme::TRIANGLE_ANALYTICAL_CONTACT; + } + return deme::NOT_A_CONTACT; + } + default: + return deme::NOT_A_CONTACT; + } +} + +// Fast FP32-only overlap check for kT contact detection (no penetration/area outputs). +template +__host__ __device__ deme::contact_t checkTriEntityOverlapFP32(const T1& A, + const T1& B, + const T1& C, + const deme::objType_t& typeB, + const T1& entityLoc, + const float3& entityDir, + const float& entitySize1, + const float& entitySize2, + const float& entitySize3, + const float& normal_sign, + const float& beta4Entity) { + const T1* tri[] = {&A, &B, &C}; + switch (typeB) { + case (deme::ANAL_OBJ_TYPE_PLANE): { + for (const T1*& v : tri) { + const float d = planeSignedDistance(*v, entityLoc, entityDir); + const float overlapDepth = beta4Entity - d; + if (overlapDepth >= 0.0f) + return deme::TRIANGLE_ANALYTICAL_CONTACT; + } + return deme::NOT_A_CONTACT; + } + case (deme::ANAL_OBJ_TYPE_CYL_INF): { + // Uses a plane-based approximation + T1 centroid = (A + B + C) / 3.0f; + T1 plane_point; + float3 plane_normal; + if (!planar_cyl_plane_from_ref(centroid, entityLoc, entityDir, entitySize1, normal_sign, plane_point, + plane_normal)) { + return deme::NOT_A_CONTACT; + } + for (const T1*& v : tri) { + const float d = planeSignedDistance(*v, plane_point, plane_normal); + const float overlapDepth = beta4Entity - d; + if (overlapDepth >= 0.0f) + return deme::TRIANGLE_ANALYTICAL_CONTACT; + } + return deme::NOT_A_CONTACT; + } + default: + return deme::NOT_A_CONTACT; + } +} + +// NOTE: Due to our algorithm needs a overlapDepth even in the case of no contact (because of extraMargin; and with +// extraMargin, negative overlapDepth can be considered in-contact), our tri-anal CD algorithm needs to always return a +// overlapDepth, even in the case of no contact. This is different from the usual CD algorithms which only return a +// overlapDepth. Also unlike tri-sph contact which has a unified util function, calcTriEntityOverlap is different from +// checkTriEntityOverlap. +template +bool __device__ calcTriEntityOverlap(const T1& A, + const T1& B, + const T1& C, + const deme::objType_t& entityType, + const T1& entityLoc, + const float3& entityDir, + const float& entitySize1, + const float& entitySize2, + const float& entitySize3, + const float& normal_sign, + T1& contactPnt, + float3& contact_normal, + T2& overlapDepth, + T2& overlapArea) { + const T1* tri[] = {&A, &B, &C}; + bool in_contact; + switch (entityType) { + case deme::ANAL_OBJ_TYPE_PLANE: { + in_contact = + tri_plane_penetration(tri, entityLoc, entityDir, overlapDepth, overlapArea, contactPnt); + // Plane contact's normal is always the plane's normal + contact_normal = entityDir; + return in_contact; + } + case deme::ANAL_OBJ_TYPE_CYL_INF: { + // Uses a plane-based approximation + T1 centroid = (A + B + C) / 3.0; + T1 plane_point; + float3 plane_normal; + if (!planar_cyl_plane_from_ref(centroid, entityLoc, entityDir, entitySize1, normal_sign, plane_point, + plane_normal)) { + return false; + } + in_contact = + tri_plane_penetration(tri, plane_point, plane_normal, overlapDepth, overlapArea, contactPnt); + contact_normal = plane_normal; + return in_contact; + } + default: + return false; + } +} + +// ----------------------------------------------------------------- +// Triangle-sphere collision detection utilities +// ----------------------------------------------------------------- + +/// This utility function takes the location 'P' and snaps it to the closest +/// point on the triangular face with given vertices (A, B, and C). The result +/// is returned in 'res'. Both 'P' and 'res' are assumed to be specified in +/// the same frame as the face vertices. This function returns 'true' if the +/// result is on an edge of this face and 'false' if the result is inside the +/// triangle. +/// Code from Ericson, "real-time collision detection", 2005, pp. 141 +template +__device__ bool snap_to_face(const T1& A, const T1& B, const T1& C, const T1& P, T1& res) { + T1 AB = B - A; + T1 AC = C - A; + + // Check if P in vertex region outside A + T1 AP = P - A; + T2 d1 = dot(AB, AP); + T2 d2 = dot(AC, AP); + if (d1 <= 0 && d2 <= 0) { + res = A; // barycentric coordinates (1,0,0) + return true; + } + + // Check if P in vertex region outside B + T1 BP = P - B; + T2 d3 = dot(AB, BP); + T2 d4 = dot(AC, BP); + if (d3 >= 0 && d4 <= d3) { + res = B; // barycentric coordinates (0,1,0) + return true; + } + + // Check if P in edge region of AB + T2 vc = d1 * d4 - d3 * d2; + if (vc <= 0 && d1 >= 0 && d3 <= 0) { + // Return projection of P onto AB + T2 v = d1 / (d1 - d3); + res = A + v * AB; // barycentric coordinates (1-v,v,0) + return true; + } + + // Check if P in vertex region outside C + T1 CP = P - C; + T2 d5 = dot(AB, CP); + T2 d6 = dot(AC, CP); + if (d6 >= 0 && d5 <= d6) { + res = C; // barycentric coordinates (0,0,1) + return true; + } + + // Check if P in edge region of AC + T2 vb = d5 * d2 - d1 * d6; + if (vb <= 0 && d2 >= 0 && d6 <= 0) { + // Return projection of P onto AC + T2 w = d2 / (d2 - d6); + res = A + w * AC; // barycentric coordinates (1-w,0,w) + return true; + } + + // Check if P in edge region of BC + T2 va = d3 * d6 - d5 * d4; + if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) { + // Return projection of P onto BC + T2 w = (d4 - d3) / ((d4 - d3) + (d5 - d6)); + res = B + w * (C - B); // barycentric coordinates (0,1-w,w) + return true; + } + + // P inside face region. Return projection of P onto face + // barycentric coordinates (u,v,w) + T2 denom = __drcp_ru(va + vb + vc); + T2 v = __dmul_ru(vb, denom); + T2 w = __dmul_ru(vc, denom); + res = A + v * AB + w * AC; // = u*A + v*B + w*C where (u = 1 - v - w) + return false; +} + +template +__device__ __forceinline__ T2 clamp01(const T2& x) { + return x < (T2)0 ? (T2)0 : (x > (T2)1 ? (T2)1 : x); +} + +template +__device__ __forceinline__ T2 clampRange(const T2& x, const T2& lo, const T2& hi) { + return x < lo ? lo : (x > hi ? hi : x); +} + +template +__device__ bool checkSphereTriPrismCandidate(const T1& outerA, + const T1& outerB, + const T1& outerC, + const T1& innerA, + const T1& sphere_pos, + const T2 radius, + T2& depth, + T1& closest) { + const T1 n_out = normalize(cross(outerB - outerA, outerC - outerA)); + const T1 shift = innerA - outerA; + const T2 thick = fabs(dot(shift, n_out)); + + T1 tri_closest; + snap_to_face(outerA, outerB, outerC, sphere_pos, tri_closest); + + const T2 z = dot(sphere_pos - outerA, n_out); + const T2 zc = clampRange(z, (T2)(-thick), (T2)0); + closest = tri_closest + zc * n_out; + + const T1 d = sphere_pos - closest; + const T2 dist = length(d); + depth = radius - dist; + return depth > (T2)0; +} + +template +__device__ bool checkSphereOuterTriPrismBarrier(const T1& A, + const T1& B, + const T1& C, + const T1& sphere_pos, + const T2 radius, + const T2 shell_half, + T1& normal, + T2& depth, + T2& overlapArea, + T1& pt1) { + const T1 n_out = normalize(cross(B - A, C - A)); + const T1 outerA = A + shell_half * n_out; + const T1 outerB = B + shell_half * n_out; + const T1 outerC = C + shell_half * n_out; + const T1 inward = ((T2)-2 * shell_half) * n_out; + const T2 inward_len2 = dot(inward, inward); + + bool have_candidate = false; + bool best_is_barrier = false; + T2 best_metric = DEME_HUGE_FLOAT; + T2 best_depth = (T2)(-DEME_HUGE_FLOAT); + T1 best_normal = n_out; + T1 best_pt = outerA; + + // Outer-face barrier: one-sided, no normal flip. If the sphere center is already inside, + // this remains active and pushes it back out. + T1 face_loc; + const bool face_is_edge = snap_to_face(outerA, outerB, outerC, sphere_pos, face_loc); + if (!face_is_edge) { + const T2 h = dot(sphere_pos - outerA, n_out); + const T2 cand_depth = radius - h; + if (cand_depth > (T2)0) { + have_candidate = true; + best_is_barrier = true; + best_metric = h; + best_depth = cand_depth; + best_normal = n_out; + best_pt = face_loc; + } + } + + const T1 verts[3] = {outerA, outerB, outerC}; + for (int i = 0; i < 3; i++) { + const T1 base = verts[i]; + const T1 next = verts[(i + 1) % 3]; + const T1 edge = next - base; + const T2 edge_len2 = dot(edge, edge); + if (edge_len2 <= (T2)DEME_TINY_FLOAT) { + continue; + } + + const T1 rel = sphere_pos - base; + const T2 u_raw = dot(rel, edge) / edge_len2; + const T2 w_raw = inward_len2 > (T2)DEME_TINY_FLOAT ? dot(rel, inward) / inward_len2 : (T2)0; + + // Side-wall barrier when the orthogonal projection lands inside the rectangle extents. + if (u_raw >= (T2)0 && u_raw <= (T2)1 && w_raw >= (T2)0 && w_raw <= (T2)1) { + T1 s_out = normalize(cross(edge, n_out)); + const T2 g = dot(rel, s_out); + const T2 cand_depth = radius - g; + if (cand_depth > (T2)0) { + if (!have_candidate || g < best_metric) { + have_candidate = true; + best_is_barrier = true; + best_metric = g; + best_depth = cand_depth; + best_normal = s_out; + best_pt = base + clampRange(u_raw, (T2)0, (T2)1) * edge + + clampRange(w_raw, (T2)0, (T2)1) * inward; + } + } + } + + // Rectangle closest-point candidate for edge/vertex grazing contacts. + const T2 u = clampRange(u_raw, (T2)0, (T2)1); + const T2 w = clampRange(w_raw, (T2)0, (T2)1); + const T1 rect_q = base + u * edge + w * inward; + const T1 d = sphere_pos - rect_q; + const T2 dist = length(d); + const T2 cand_depth = radius - dist; + if (cand_depth > (T2)0) { + const T2 metric = dist; + if (!have_candidate || (!best_is_barrier && metric < best_metric)) { + have_candidate = true; + best_is_barrier = false; + best_metric = metric; + best_depth = cand_depth; + if (dist > (T2)DEME_TINY_FLOAT) { + best_normal = ((T2)1 / dist) * d; + } else { + best_normal = normalize(cross(edge, n_out)); + } + best_pt = rect_q; + } + } + } + + if (!have_candidate) { + depth = (T2)(-DEME_HUGE_FLOAT); + overlapArea = (T2)0; + return false; + } + + // Cap recovery depth to avoid explosive impulses if a particle already ended up deep inside. + // Keep this intentionally conservative so first-time recovery contacts do not inject excessive energy. + const T2 max_recovery_depth = (T2)0.02 * radius; + const T2 capped_depth = best_depth > max_recovery_depth ? max_recovery_depth : best_depth; + depth = capped_depth; + normal = best_normal; + // Keep the contact point on/near the sphere-side interface midpoint so downstream + // lever-arm logic stays bounded even for deep barrier recoveries. + const T2 sphere_to_cp = radius - (T2)0.5 * capped_depth; + pt1 = sphere_pos - sphere_to_cp * normal; + + const T2 overlap_area = static_cast(deme::PI * (2.0 * radius * depth - depth * depth)); + overlapArea = overlap_area > static_cast(0.0) ? overlap_area : static_cast(0.0); + return true; +} + +template +__device__ __forceinline__ T2 +closestPtSegmentSegment(const T1& p1, const T1& q1, const T1& p2, const T1& q2, T1& c1, T1& c2) { + constexpr T2 EPS = (T2)1e-20; + const T1 d1 = q1 - p1; + const T1 d2 = q2 - p2; + const T1 r = p1 - p2; + const T2 a = dot(d1, d1); + const T2 e = dot(d2, d2); + const T2 f = dot(d2, r); + + T2 s = (T2)0; + T2 t = (T2)0; + + if (a <= EPS && e <= EPS) { + c1 = p1; + c2 = p2; + return dot(c1 - c2, c1 - c2); + } + if (a <= EPS) { + s = (T2)0; + t = clamp01(f / e); + } else { + const T2 c = dot(d1, r); + if (e <= EPS) { + t = (T2)0; + s = clamp01(-c / a); + } else { + const T2 b = dot(d1, d2); + const T2 denom = a * e - b * b; + if (denom > EPS) { + s = clamp01((b * f - c * e) / denom); + } else { + s = (T2)0; + } + t = (b * s + f) / e; + if (t < (T2)0) { + t = (T2)0; + s = clamp01(-c / a); + } else if (t > (T2)1) { + t = (T2)1; + s = clamp01((b - c) / a); + } + } + } + + c1 = p1 + d1 * s; + c2 = p2 + d2 * t; + return dot(c1 - c2, c1 - c2); +} + +template +__device__ __forceinline__ T2 closestPtTriTriDistance(const T1& A1, + const T1& B1, + const T1& C1, + const T1& A2, + const T1& B2, + const T1& C2, + T1& outA, + T1& outB) { + const T1 triA[3] = {A1, B1, C1}; + const T1 triB[3] = {A2, B2, C2}; + + T2 best2 = DEME_HUGE_FLOAT; + outA = A1; + outB = A2; + + // Edge-edge pairs (9) + for (int i = 0; i < 3; i++) { + const T1 pA = triA[i]; + const T1 qA = triA[(i + 1) % 3]; + for (int j = 0; j < 3; j++) { + const T1 pB = triB[j]; + const T1 qB = triB[(j + 1) % 3]; + T1 cA, cB; + const T2 d2 = closestPtSegmentSegment(pA, qA, pB, qB, cA, cB); + if (d2 < best2) { + best2 = d2; + outA = cA; + outB = cB; + } + } + } + + // Vertices of A against face B + for (int i = 0; i < 3; i++) { + T1 q; + snap_to_face(A2, B2, C2, triA[i], q); + const T2 d2 = dot(triA[i] - q, triA[i] - q); + if (d2 < best2) { + best2 = d2; + outA = triA[i]; + outB = q; + } + } + + // Vertices of B against face A + for (int i = 0; i < 3; i++) { + T1 q; + snap_to_face(A1, B1, C1, triB[i], q); + const T2 d2 = dot(q - triB[i], q - triB[i]); + if (d2 < best2) { + best2 = d2; + outA = q; + outB = triB[i]; + } + } + + if (best2 < (T2)0) { + best2 = (T2)0; + } + return sqrt(best2); +} + +/** +/brief TRIANGLE FACE--SPHERE COLLISION DETECTION + +The triangular face is defined by points A, B, C. The sequence is important as it defines the positive face via a +right-hand rule. +The sphere is centered at sphere_pos and has radius. +The index "1" is associated with the triangle. The index "2" is associated with the sphere. +The coordinates of the face and sphere are assumed to be provided in the same reference frame. + +Output: + - pt1: contact point on triangle + - depth: penetration distance (a positive value means that overlap exists) + - normal: contact normal, from pt2 to pt1 +A return value of "true" signals collision. +*/ +template +__device__ bool checkTriSphereOverlap(const T1& A, ///< First vertex of the triangle + const T1& B, ///< Second vertex of the triangle + const T1& C, ///< Third vertex of the triangle + const T1& sphere_pos, ///< Location of the center of the sphere + const T2 radius, ///< Sphere radius + T1& normal, ///< contact normal + T2& depth, ///< penetration (positive if in contact) + T2& overlapArea, ///< overlap area + T1& pt1 ///< contact point on triangle +) { + // Calculate face normal using RHR + T1 face_n = normalize(cross(B - A, C - A)); + + // Calculate signed height of sphere center above face plane + T2 h = dot(sphere_pos - A, face_n); + + // Find the closest point on the face to the sphere center and determine + // whether or not this location is inside the face or on an edge. + T1 faceLoc; + + // Triangle in contact with sphere or not + bool in_contact; + + // Still do the following since we need depth + if (!snap_to_face(A, B, C, sphere_pos, faceLoc)) { + // Nearest point on the triangle is on its face + // printf("FACE CONTACT\n"); + depth = radius - h; // Positive for contact + normal.x = face_n.x; + normal.y = face_n.y; + normal.z = face_n.z; + // The contact point is somewhere in the midpoint of the deepest penetration line segment. Go from faceLoc, + // backwards wrt normal, half the penetration depth. + pt1 = faceLoc - (depth * 0.5) * normal; + if (h >= radius || h <= -radius) { + in_contact = false; + } else { + in_contact = true; + } + // overlapArea = deme::PI * (radius * radius - (radius - depth) * (radius - depth)); + // Simplify it and assign it at the end of this call + } else { + // printf("EDGE CONTACT\n"); + // Nearest point on the triangle is on an edge + { + T1 normal_d = sphere_pos - faceLoc; + normal.x = normal_d.x; + normal.y = normal_d.y; + normal.z = normal_d.z; + } + T2 dist = length(normal); + depth = radius - dist; // Positive for contact + + normal = (1.0 / dist) * normal; + // Go from faceLoc, backwards wrt normal, half the penetration depth + pt1 = faceLoc - (depth * 0.5) * normal; + if (depth < 0. || h >= radius || h <= -radius) { + in_contact = false; + } else { + in_contact = true; + } + // In the edge case, overlapArea is a bit tricky to define accurately. + // Here we still just approximate it as a circle area. + } + overlapArea = static_cast(deme::PI * (2.0 * radius * depth - depth * depth)); + return in_contact; +} + +/** +/brief TRIANGLE FACE--SPHERE COLLISION DETECTION (DIRECTIONAL) + +The triangular face is defined by points A, B, C. The sequence is important as it defines the positive face via a +right-hand rule. +The sphere is centered at sphere_pos and has radius. +The index "1" is associated with the triangle. The index "2" is associated with the sphere. +The coordinates of the face and sphere are assumed to be provided in the same reference frame. +This flavor is directional, meaning if a sphere is not in (geometric) contact with a triangle, it may still be +considered in contact, if it is submerged in the (closed) object that this mesh is representing, or say the penetration +is too deep, large than 2 * sphere rad. + +Output: + - pt1: contact point on triangle + - depth: penetration distance (a positive value means that overlap exists) + - normal: contact normal, from pt2 to pt1 +A return value of "true" signals collision. +*/ +template +__device__ bool checkTriSphereOverlap_directional(const T1& A, ///< First vertex of the triangle + const T1& B, ///< Second vertex of the triangle + const T1& C, ///< Third vertex of the triangle + const T1& sphere_pos, ///< Location of the center of the sphere + const T2 radius, ///< Sphere radius + T1& normal, ///< contact normal + T2& depth, ///< penetration (positive if in contact) + T1& pt1 ///< contact point on triangle +) { + // Calculate face normal using RHR + T1 face_n = normalize(cross(B - A, C - A)); + + // Calculate signed height of sphere center above face plane + T2 h = dot(sphere_pos - A, face_n); + + // Find the closest point on the face to the sphere center and determine + // whether or not this location is inside the face or on an edge. + T1 faceLoc; + + // Triangle in contact with sphere or not + bool in_contact; + + // Still do the following since we need depth + if (!snap_to_face(A, B, C, sphere_pos, faceLoc)) { + // Nearest point on the triangle is on its face + // printf("FACE CONTACT\n"); + depth = radius - h; // Positive for contact + normal.x = face_n.x; + normal.y = face_n.y; + normal.z = face_n.z; + // The contact point is somewhere in the midpoint of the deepest penetration line segment. Go from faceLoc, + // backwards wrt normal, half the penetration depth. + pt1 = faceLoc - (depth * 0.5) * normal; + if (depth < 0.) { + in_contact = false; + } else { + in_contact = true; + } + } else { + // printf("EDGE CONTACT\n"); + // Nearest point on the triangle is on an edge + { + T1 normal_d = sphere_pos - faceLoc; + normal.x = normal_d.x; + normal.y = normal_d.y; + normal.z = normal_d.z; + } + T2 dist = length(normal); + depth = radius - dist; // Positive for contact + + normal = (1.0 / dist) * normal; + // Go from faceLoc, backwards wrt normal, half the penetration depth + pt1 = faceLoc - (depth * 0.5) * normal; + if (depth < 0. || h >= radius) { + in_contact = false; + } else { + in_contact = true; + } + } + return in_contact; +} + +// ----------------------------------------------------------------------------- +// Triangle-triangle collision detection utilities +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +// Prism contact detection using the Separating Axis Theorem (SAT) +// +// For the extruded triangle "sandwich" prisms we only have 4 unique edge +// directions (3 base edges + extrusion). This yields: +// - 8 face normals (base + 3 side faces per prism) +// - 16 edge-edge axes +// Total: 24 axes, evaluated on the fly without normalization. +//////////////////////////////////////////////////////////////////////////////// + +__device__ __forceinline__ float invSqrt(float x) { + return rsqrtf(x); +} + +__device__ __forceinline__ double invSqrt(double x) { + return 1.0 / sqrt(x); +} + +#ifndef DEME_SAT_ENABLE_MIXED_PRECISION + #define DEME_SAT_ENABLE_MIXED_PRECISION 0 +#endif + +// minimal arithmetic for extruded-triangle prism projection. +// Key identity: prism projection = [minTri, maxTri] union [minTri+shift, maxTri+shift] +// => outMin = minTri + min(0,shift), outMax = maxTri + max(0,shift) +template +__device__ __forceinline__ void projectExtrudedTriPrism(const Vec& v0, + const Vec& v1, + const Vec& v2, + const Vec& d, + const Vec& axis, + Scalar& outMin, + Scalar& outMax) { + const Scalar p0 = dot(v0, axis); + const Scalar p1 = dot(v1, axis); + const Scalar p2 = dot(v2, axis); + + const Scalar triMin = fmin(p0, fmin(p1, p2)); + const Scalar triMax = fmax(p0, fmax(p1, p2)); + + const Scalar shift = dot(d, axis); + const Scalar z = Scalar(0); + + outMin = triMin + fmin(z, shift); + outMax = triMax + fmax(z, shift); +} + +template +__device__ __forceinline__ Scalar satSeparationOnAxis(const Vec& axis, + const Vec& A0, + const Vec& A1, + const Vec& A2, + const Vec& dA, + const Vec& B0, + const Vec& B1, + const Vec& B2, + const Vec& dB) { + Scalar len2 = dot(axis, axis); + if (len2 < Scalar(DEME_TINY_FLOAT)) + return -Scalar(DEME_HUGE_FLOAT); + + Scalar minA, maxA, minB, maxB; + projectExtrudedTriPrism(A0, A1, A2, dA, axis, minA, maxA); + projectExtrudedTriPrism(B0, B1, B2, dB, axis, minB, maxB); + + Scalar sep1 = minB - maxA; + Scalar sep2 = minA - maxB; + Scalar sepProj = (sep1 > sep2) ? sep1 : sep2; + Scalar invLen = invSqrt(len2); + return sepProj * invLen; +} + +/** + * @brief Helper function to project one triangle onto another triangle's plane and clip using Sutherland-Hodgman + * + * @param incTri The incident triangle vertices to be projected + * @param refTri The reference triangle vertices (defines the plane and clipping region) + * @param refNormal The normal of the reference triangle's plane + * @param depth Output: penetration depth (max distance of submerged vertices) + * @param area Output: area of the clipping polygon + * @param centroid Output: centroid of the clipping polygon + * @param minPenetrationFullySubmerged Output: shallowest vertex penetration depth, but ONLY non-zero when all 3 + * incident vertices are submerged (i.e., the incident triangle is completely below the reference plane). When + * fewer than 3 vertices are submerged this is set to 0. This value is used by kT margin sizing: a triangle + * that is fully submerged in the reference plane direction cannot be detected by SAT, so its shallowest + * vertex depth tells us the minimum extra margin we must add to keep the contact traceable. + * @return true if there is contact (at least one vertex submerged), false otherwise + */ +template +__device__ bool projectTriangleOntoTriangle(const T1* incTri, + const T1* refTri, + const T1& refNormal, + T2& depth, + T2& area, + T1& centroid, + T2& minPenetrationFullySubmerged) { + // Compute signed distances of incident triangle vertices to reference plane + area = T2(0.0); + minPenetrationFullySubmerged = T2(0.0); + T2 incDists[3]; + T2 maxPenetration = 0.0; + T2 minPenetration = T2(DEME_HUGE_FLOAT); + int8_t numSubmerged = 0; +#pragma unroll + for (int8_t i = 0; i < 3; ++i) { + incDists[i] = dot(incTri[i] - refTri[0], refNormal); + if (incDists[i] < 0.0) { + numSubmerged++; + T2 pen = -incDists[i]; + if (pen > maxPenetration) + maxPenetration = pen; + if (pen < minPenetration) + minPenetration = pen; + } + } + // The shallowest penetration is only meaningful when ALL vertices are submerged: that is the only case where the + // incident triangle is completely buried and cannot be found by SAT on its own. + if (numSubmerged == 3) { + minPenetrationFullySubmerged = minPenetration; + } + + // If no vertices are submerged, no contact + if (numSubmerged == 0) { + // Non-contact placeholder contact info is set outside this function + // depth = T2(0.0); + // area = T2(0.0); + // centroid.x = T2(0.0); + // centroid.y = T2(0.0); + // centroid.z = T2(0.0); + return false; + } + + // Maximum vertices in a triangle-triangle clipping polygon + // Sutherland-Hodgman clipping can produce up to (n+m) vertices where n and m are + // the number of vertices in the input polygons. For triangle-triangle clipping, + // we conservatively use 9 (more than the theoretical max of 6) for safety. + constexpr int8_t SH_MAX_CLIPPING_VERTICES = 9; + + // Build polygon from projected submerged vertices and edge-plane intersections + T1 projectedPoly[SH_MAX_CLIPPING_VERTICES]; + int8_t nPoly = 0; + + // Process each edge of the incident triangle +#pragma unroll + for (int8_t i = 0; i < 3; ++i) { + int8_t j = (i + 1) % 3; + bool in_i = (incDists[i] < 0.0); + bool in_j = (incDists[j] < 0.0); + + // Add submerged vertex (projected onto plane) + if (in_i) { + projectedPoly[nPoly++] = incTri[i] - refNormal * incDists[i]; + } + + // Add edge-plane intersection if edge crosses the plane + if (in_i != in_j) { + T2 denom = incDists[i] - incDists[j]; + if (denom != 0.0) { // Avoid division by zero + T2 t = incDists[i] / denom; + T1 inter = incTri[i] + (incTri[j] - incTri[i]) * t; + projectedPoly[nPoly++] = inter; + } + } + } + + // If we don't have at least 3 vertices, no valid polygon + if (nPoly < 3) { + // Non-contact placeholder contact info is set outside this function + // depth = maxPenetration; + // area = T2(0.0); + // centroid = (incTri[0] + incTri[1] + incTri[2]) / T2(3.0); + return false; + } + + // Now compute the intersection polygon of the projected triangle and reference triangle + // We need bidirectional clipping: clip projectedPoly against refTri, then add refTri vertices inside projectedPoly + + // Step 1: Clip projected polygon against reference triangle (Sutherland-Hodgman) + T1 resultPoly[SH_MAX_CLIPPING_VERTICES]; + for (int8_t i = 0; i < nPoly; ++i) { + resultPoly[i] = projectedPoly[i]; + } + int8_t numInputVerts = nPoly; + + T1 intermediatePoly[SH_MAX_CLIPPING_VERTICES]; + for (int8_t edge = 0; edge < 3; ++edge) { + int8_t numOutputVerts = 0; + T1 edgeStart = refTri[edge]; + T1 edgeEnd = refTri[(edge + 1) % 3]; + T1 edgeDir = edgeEnd - edgeStart; + T1 edgeNormal = cross(refNormal, edgeDir); + edgeNormal = normalize(edgeNormal); + + // Clip input polygon against this edge + for (int8_t i = 0; i < numInputVerts; ++i) { + T1 v1 = resultPoly[i]; + T1 v2 = resultPoly[(i + 1) % numInputVerts]; + T2 d1 = dot(v1 - edgeStart, edgeNormal); + T2 d2 = dot(v2 - edgeStart, edgeNormal); + bool in1 = (d1 >= -DEME_TINY_FLOAT); + bool in2 = (d2 >= -DEME_TINY_FLOAT); + + if (in1) { + intermediatePoly[numOutputVerts++] = v1; + } + if (in1 != in2) { + T2 denom = d1 - d2; + if (denom != 0.0) { // Avoid division by zero + T2 t = d1 / denom; + T1 inter = v1 + (v2 - v1) * t; + intermediatePoly[numOutputVerts++] = inter; + } + } + } + + // Copy output to input for next iteration + for (int8_t i = 0; i < numOutputVerts; ++i) { + resultPoly[i] = intermediatePoly[i]; + } + numInputVerts = numOutputVerts; + + if (numInputVerts == 0) { + break; // No intersection + } + } + + // Step 2: Check if any reference triangle vertices are inside the projected polygon + // and add them to the intersection polygon if they are + int8_t numFinalVerts = numInputVerts; + + // For each reference triangle vertex, check if it's inside the original projected polygon + for (int8_t refIdx = 0; refIdx < 3; ++refIdx) { + T1 refVertex = refTri[refIdx]; + + // Check if refVertex is inside the projected polygon using winding number + bool inside = true; + for (int8_t i = 0; i < nPoly; ++i) { + T1 edgeStart = projectedPoly[i]; + T1 edgeEnd = projectedPoly[(i + 1) % nPoly]; + T1 edgeDir = edgeEnd - edgeStart; + T1 edgeNormal = cross(refNormal, edgeDir); + T2 dist = dot(refVertex - edgeStart, edgeNormal); + if (dist < -DEME_TINY_FLOAT) { + inside = false; + break; + } + } + + if (inside) { + // Check if this vertex is not already in the polygon (avoid duplicates) + bool isDuplicate = false; + for (int8_t j = 0; j < numFinalVerts; ++j) { + T1 diff = resultPoly[j] - refVertex; + if (dot(diff, diff) < DEME_TINY_FLOAT * DEME_TINY_FLOAT) { + isDuplicate = true; + break; + } + } + if (!isDuplicate) { + resultPoly[numFinalVerts++] = refVertex; + } + } + } + + // If we added reference vertices, we need to reorder the polygon to maintain proper winding + if (numFinalVerts > numInputVerts && numFinalVerts >= 3) { + // Compute centroid of all vertices + T1 tempCentroid; + tempCentroid.x = 0.0; + tempCentroid.y = 0.0; + tempCentroid.z = 0.0; + for (int8_t i = 0; i < numFinalVerts; ++i) { + tempCentroid = tempCentroid + resultPoly[i]; + } + tempCentroid = tempCentroid / T2(numFinalVerts); + + // Sort vertices by angle around centroid to ensure proper winding order + // Use simple bubble sort for small number of vertices + for (int8_t i = 0; i < numFinalVerts - 1; ++i) { + for (int8_t j = i + 1; j < numFinalVerts; ++j) { + T1 vi = resultPoly[i] - tempCentroid; + T1 vj = resultPoly[j] - tempCentroid; + // Use reference normal to determine consistent orientation + T1 cross_ij = cross(vi, vj); + if (dot(cross_ij, refNormal) < 0.0) { + // Swap + T1 temp = resultPoly[i]; + resultPoly[i] = resultPoly[j]; + resultPoly[j] = temp; + } + } + } + } + + numInputVerts = numFinalVerts; + + // Compute centroid and area of the intersection polygon + centroid.x = 0.0; + centroid.y = 0.0; + centroid.z = 0.0; + + depth = maxPenetration; + if (numInputVerts >= 3) { + for (int8_t i = 0; i < numInputVerts; ++i) { + centroid = centroid + resultPoly[i]; + } + centroid = centroid / T2(numInputVerts); + + // Calculate area using fan triangulation from centroid + area = 0.0; + for (int8_t i = 0; i < numInputVerts; ++i) { + T1 v1 = resultPoly[i] - centroid; + T1 v2 = resultPoly[(i + 1) % numInputVerts] - centroid; + T1 crossProd = cross(v1, v2); + area += sqrt(dot(crossProd, crossProd)); + } + area *= static_cast(0.5); + return true; + } else { + // Degenerate intersection polygon + // centroid = (incTri[0] + incTri[1] + incTri[2]) / T2(3.0); + // Non-contact placeholder contact info is set outside this function + return false; + } +} + +/** + * @brief Fast SAT contact check between two triangular prisms (triangle sandwiches). + * + * Evaluates 24 axes (8 face normals + 16 edge-edge) without normalization. Uses FP32 by + * default with a narrow mixed-precision recheck near zero overlap to avoid false positives. + * + * OPTIMIZED VERSION: + * - Uses fused operations to reduce register pressure + * - Inline axis separation test to avoid lambda overhead + * - Early termination structure optimized for GPU SIMT execution + * + * @return true if prisms are in contact (no separating axis found), false otherwise + */ +template +__device__ __forceinline__ bool calc_prism_contact(const T1& prismAFaceANode1, + const T1& prismAFaceANode2, + const T1& prismAFaceANode3, + const T1& prismAFaceBNode1, + const T1& prismAFaceBNode2, + const T1& prismAFaceBNode3, + const T1& prismBFaceANode1, + const T1& prismBFaceANode2, + const T1& prismBFaceANode3, + const T1& prismBFaceBNode1, + const T1& prismBFaceBNode2, + const T1& prismBFaceBNode3) { + // Relative coordinates centered at prismAFaceANode1 reduce FP32 dynamic range issues. + const float3 origin = prismAFaceANode1; + + // Prism A base triangle relative to origin (A0 is exactly zero) + const float3 A0 = make_float3(0.0f, 0.0f, 0.0f); + const float3 A1 = prismAFaceANode2 - origin; + const float3 A2 = prismAFaceANode3 - origin; + + // Prism B base triangle relative to origin + const float3 B0 = prismBFaceANode1 - origin; + const float3 B1 = prismBFaceANode2 - origin; + const float3 B2 = prismBFaceANode3 - origin; + + // Extrusion vectors (world differences; origin cancels) + const float3 dA = prismAFaceBNode1 - prismAFaceANode1; + const float3 dB = prismBFaceBNode1 - prismBFaceANode1; + + // ------------------------------------------------------------------ + // Cheap AABB overlap test on world axes (X/Y/Z) for the full prisms. + // This rejects many non-contacts before the expensive SAT axes. + // Prism extents: triMin + min(0,d), triMax + max(0,d). + // ------------------------------------------------------------------ + { + const float triMinAx = fminf(0.0f, fminf(A1.x, A2.x)); + const float triMaxAx = fmaxf(0.0f, fmaxf(A1.x, A2.x)); + const float triMinAy = fminf(0.0f, fminf(A1.y, A2.y)); + const float triMaxAy = fmaxf(0.0f, fmaxf(A1.y, A2.y)); + const float triMinAz = fminf(0.0f, fminf(A1.z, A2.z)); + const float triMaxAz = fmaxf(0.0f, fmaxf(A1.z, A2.z)); + + const float minAx = triMinAx + fminf(0.0f, dA.x); + const float maxAx = triMaxAx + fmaxf(0.0f, dA.x); + const float minAy = triMinAy + fminf(0.0f, dA.y); + const float maxAy = triMaxAy + fmaxf(0.0f, dA.y); + const float minAz = triMinAz + fminf(0.0f, dA.z); + const float maxAz = triMaxAz + fmaxf(0.0f, dA.z); + + const float triMinBx = fminf(B0.x, fminf(B1.x, B2.x)); + const float triMaxBx = fmaxf(B0.x, fmaxf(B1.x, B2.x)); + const float triMinBy = fminf(B0.y, fminf(B1.y, B2.y)); + const float triMaxBy = fmaxf(B0.y, fmaxf(B1.y, B2.y)); + const float triMinBz = fminf(B0.z, fminf(B1.z, B2.z)); + const float triMaxBz = fmaxf(B0.z, fmaxf(B1.z, B2.z)); + + const float minBx = triMinBx + fminf(0.0f, dB.x); + const float maxBx = triMaxBx + fmaxf(0.0f, dB.x); + const float minBy = triMinBy + fminf(0.0f, dB.y); + const float maxBy = triMaxBy + fmaxf(0.0f, dB.y); + const float minBz = triMinBz + fminf(0.0f, dB.z); + const float maxBz = triMaxBz + fmaxf(0.0f, dB.z); + + if (maxAx < minBx || maxBx < minAx) + return false; + if (maxAy < minBy || maxBy < minAy) + return false; + if (maxAz < minBz || maxBz < minAz) + return false; + } + + // Edge vectors (triangle edges only; extrusion edges handled via dA/dB) + const float3 eA0 = A1 - A0; + const float3 eA1 = A2 - A1; + const float3 eA2 = A0 - A2; + + const float3 eB0 = B1 - B0; + const float3 eB1 = B2 - B1; + const float3 eB2 = B0 - B2; + +// Project an extruded triangle prism where the shift along axis is provided (can be forced to 0 cheaply). +// NOTE: We avoid any rsqrt normalization. Separation sign is invariant to axis scale. +#define PROJECT_PRISM_WITH_SHIFT(v0, v1, v2, shift, axis, outMin, outMax) \ + do { \ + const float p0 = dot((v0), (axis)); \ + const float p1 = dot((v1), (axis)); \ + const float p2 = dot((v2), (axis)); \ + const float triMin = fminf(p0, fminf(p1, p2)); \ + const float triMax = fmaxf(p0, fmaxf(p1, p2)); \ + (outMin) = triMin + fminf(0.0f, (shift)); \ + (outMax) = triMax + fmaxf(0.0f, (shift)); \ + } while (0) + +// Test axis: caller can declare whether shiftA and/or shiftB are guaranteed zero for this axis. +// This saves a dot(d,axis) on the relevant prism. +#define TEST_AXIS(axis_expr, shiftA_zero, shiftB_zero) \ + do { \ + const float3 axis = (axis_expr); \ + const float len2 = dot(axis, axis); \ + if (len2 > DEME_TINY_FLOAT) { \ + float minA, maxA, minB, maxB; \ + const float shiftA = (shiftA_zero) ? 0.0f : dot(dA, axis); \ + const float shiftB = (shiftB_zero) ? 0.0f : dot(dB, axis); \ + PROJECT_PRISM_WITH_SHIFT(A0, A1, A2, shiftA, axis, minA, maxA); \ + PROJECT_PRISM_WITH_SHIFT(B0, B1, B2, shiftB, axis, minB, maxB); \ + if (maxA < minB || maxB < minA) \ + return false; \ + } \ + } while (0) + + // Face normals (2 axes): shifts generally non-zero for both prisms + TEST_AXIS(cross(eA0, A2 - A0), false, false); + TEST_AXIS(cross(eB0, B2 - B0), false, false); + + // Side normals (6 axes): + // For axis = cross(eA?, dA): dot(dA, axis) == 0 in exact arithmetic -> shiftA_zero = true. + // For axis = cross(eB?, dB): dot(dB, axis) == 0 -> shiftB_zero = true. + TEST_AXIS(cross(eA0, dA), true, false); + TEST_AXIS(cross(eA1, dA), true, false); + TEST_AXIS(cross(eA2, dA), true, false); + TEST_AXIS(cross(eB0, dB), false, true); + TEST_AXIS(cross(eB1, dB), false, true); + TEST_AXIS(cross(eB2, dB), false, true); + + // Edge-edge axes (9 axes): shifts generally non-zero for both prisms + TEST_AXIS(cross(eA0, eB0), false, false); + TEST_AXIS(cross(eA0, eB1), false, false); + TEST_AXIS(cross(eA0, eB2), false, false); + TEST_AXIS(cross(eA1, eB0), false, false); + TEST_AXIS(cross(eA1, eB1), false, false); + TEST_AXIS(cross(eA1, eB2), false, false); + TEST_AXIS(cross(eA2, eB0), false, false); + TEST_AXIS(cross(eA2, eB1), false, false); + TEST_AXIS(cross(eA2, eB2), false, false); + + // Edge-extrusion cross products (6 axes): + // axis = cross(eA?, dB) => dot(dB,axis) == 0 -> shiftB_zero = true + // axis = cross(dA, eB?) => dot(dA,axis) == 0 -> shiftA_zero = true + TEST_AXIS(cross(eA0, dB), false, true); + TEST_AXIS(cross(eA1, dB), false, true); + TEST_AXIS(cross(eA2, dB), false, true); + TEST_AXIS(cross(dA, eB0), true, false); + TEST_AXIS(cross(dA, eB1), true, false); + TEST_AXIS(cross(dA, eB2), true, false); + + // Extrusion-extrusion (1 axis): dot(dA,cross(dA,dB)) == dot(dB,cross(dA,dB)) == 0 + TEST_AXIS(cross(dA, dB), true, true); + +#undef TEST_AXIS +#undef PROJECT_PRISM_WITH_SHIFT + + return true; +} + +/// Lightweight SAT check for triangle-triangle contact (physical contact only) +/// Returns true if triangles are in physical contact (no separating axis found), false otherwise +/// This is a simplified version that only performs the SAT test without computing contact details + +// ---------- helpers +template +__device__ __forceinline__ T tmin2(T a, T b) { + return a < b ? a : b; +} + +template +__device__ __forceinline__ T tmax2(T a, T b) { + return a > b ? a : b; +} + +template +__device__ __forceinline__ T tmin3(T a, T b, T c) { + return tmin2(a, tmin2(b, c)); +} + +template +__device__ __forceinline__ T tmax3(T a, T b, T c) { + return tmax2(a, tmax2(b, c)); +} + +__device__ __forceinline__ float3 make_zero3_float() { + return make_float3(0.f, 0.f, 0.f); +} +__device__ __forceinline__ double3 make_zero3_double() { + return make_double3(0.0, 0.0, 0.0); +} + +template +__device__ __forceinline__ T1 make_zero3(); +template <> +__device__ __forceinline__ float3 make_zero3() { + return make_zero3_float(); +} +template <> +__device__ __forceinline__ double3 make_zero3() { + return make_zero3_double(); +} + +// axis separation test (no normalization) +template +__device__ __forceinline__ bool axis_separates_skin(const T1& axis, + const T1& a0, + const T1& a1, + const T1& a2, + const T1& b0, + const T1& b1, + const T1& b2, + const T2 skin, + const T2 tiny_axis2, + const T2 num_eps) { + const T2 len2 = (T2)dot(axis, axis); + if (len2 <= tiny_axis2) + return false; // ignore degenerate axis + + const T2 pa0 = (T2)dot(a0, axis); + const T2 pa1 = (T2)dot(a1, axis); + const T2 pa2 = (T2)dot(a2, axis); + const T2 minA = tmin3(pa0, pa1, pa2); + const T2 maxA = tmax3(pa0, pa1, pa2); + + const T2 pb0 = (T2)dot(b0, axis); + const T2 pb1 = (T2)dot(b1, axis); + const T2 pb2 = (T2)dot(b2, axis); + const T2 minB = tmin3(pb0, pb1, pb2); + const T2 maxB = tmax3(pb0, pb1, pb2); + + const T2 sep1 = minB - maxA; + const T2 sep2 = minA - maxB; + const T2 sep = (sep1 > sep2) ? sep1 : sep2; + + // Separation only if gap is strictly larger than skin (+ numeric cushion) + return sep > (skin + num_eps); +} + +template +__device__ __forceinline__ bool checkTriangleTriangleSAT(const T1& A1, + const T1& B1, + const T1& C1, + const T1& A2, + const T1& B2, + const T1& C2) { + // Contact skin in your length unit (e.g., mm) + constexpr T2 CONTACT_SKIN = (T2)0.05; // adjust + + // Degeneracy gate for axes (len^2). Keep very small. + //// TODO: Beyond this threshol the presence of degenerated tris should be warned! + constexpr T2 TINY_AXIS2 = (T2)1e-20; + + // Small numerical cushion (should be << skin) + constexpr T2 NUM_EPS = (T2)1e-12; + + // --------------------------------------------------------- + // 1) Pair-local frame: shift by origin to shrink dynamic range + // --------------------------------------------------------- + const T1 O = A1; + const T1 a0 = make_zero3(); + const T1 a1 = B1 - O; + const T1 a2 = C1 - O; + + const T1 b0 = A2 - O; + const T1 b1 = B2 - O; + const T1 b2 = C2 - O; + + // --------------------------------------------------------- + // 2) AABB early-out on X/Y/Z with skin + // --------------------------------------------------------- + { + const T2 minAx = tmin3((T2)a0.x, (T2)a1.x, (T2)a2.x); + const T2 maxAx = tmax3((T2)a0.x, (T2)a1.x, (T2)a2.x); + const T2 minBx = tmin3((T2)b0.x, (T2)b1.x, (T2)b2.x); + const T2 maxBx = tmax3((T2)b0.x, (T2)b1.x, (T2)b2.x); + if (maxAx < (minBx - CONTACT_SKIN) || maxBx < (minAx - CONTACT_SKIN)) + return false; + + const T2 minAy = tmin3((T2)a0.y, (T2)a1.y, (T2)a2.y); + const T2 maxAy = tmax3((T2)a0.y, (T2)a1.y, (T2)a2.y); + const T2 minBy = tmin3((T2)b0.y, (T2)b1.y, (T2)b2.y); + const T2 maxBy = tmax3((T2)b0.y, (T2)b1.y, (T2)b2.y); + if (maxAy < (minBy - CONTACT_SKIN) || maxBy < (minAy - CONTACT_SKIN)) + return false; + + const T2 minAz = tmin3((T2)a0.z, (T2)a1.z, (T2)a2.z); + const T2 maxAz = tmax3((T2)a0.z, (T2)a1.z, (T2)a2.z); + const T2 minBz = tmin3((T2)b0.z, (T2)b1.z, (T2)b2.z); + const T2 maxBz = tmax3((T2)b0.z, (T2)b1.z, (T2)b2.z); + if (maxAz < (minBz - CONTACT_SKIN) || maxBz < (minAz - CONTACT_SKIN)) + return false; + } + + // --------------------------------------------------------- + // 3) SAT axes: 2 face normals + 9 edge×edge (no normalization) + // --------------------------------------------------------- + const T1 eA0 = a1 - a0; + const T1 eA1 = a2 - a1; + const T1 eA2 = a0 - a2; + + const T1 eB0 = b1 - b0; + const T1 eB1 = b2 - b1; + const T1 eB2 = b0 - b2; + + // Face normals (unnormalized) + const T1 nA = cross(eA0, a2 - a0); + if (axis_separates_skin(nA, a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + + const T1 nB = cross(eB0, b2 - b0); + if (axis_separates_skin(nB, a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + + // Edge×Edge (9) + if (axis_separates_skin(cross(eA0, eB0), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA0, eB1), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA0, eB2), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + + if (axis_separates_skin(cross(eA1, eB0), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA1, eB1), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA1, eB2), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + + if (axis_separates_skin(cross(eA2, eB0), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA2, eB1), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + if (axis_separates_skin(cross(eA2, eB2), a0, a1, a2, b0, b1, b2, CONTACT_SKIN, TINY_AXIS2, NUM_EPS)) + return false; + + return true; // no axis with gap > skin => treat as contact candidate +} + +/// Triangle-triangle contact detection using projection-based approach: +/// 1. Project triangle A onto triangle B's plane and clip against B's edges +/// 2. Project triangle B onto triangle A's plane and clip against A's edges +/// 3. Average the results for final contact info +/// This approach uses Sutherland-Hodgman algorithm for clipping and does not require SAT +template +__device__ bool checkTriangleTriangleOverlap( + const T1& A1, + const T1& B1, + const T1& C1, + const T1& A2, + const T1& B2, + const T1& C2, + T1& normal, ///< contact normal (B2A direction) + T2& depth, ///< penetration (positive if in contact) + T2& projectedArea, ///< projected area of clipping polygon (optional output) + T1& point, ///< contact point + T2& minDepthA, ///< shallowest penetration of triA into triB's plane; non-zero only when triA is fully + ///< submerged below triB's plane. Used only for triA's future contact-detection margin. + T2& minDepthB) { ///< shallowest penetration of triB into triA's plane; non-zero only when triB is fully + ///< submerged below triA's plane. Used only for triB's future contact-detection margin. + // Triangle A vertices (tri1) + const T1 triA[3] = {A1, B1, C1}; + // Triangle B vertices (tri2) + const T1 triB[3] = {A2, B2, C2}; + + // Margin sizing is per triangle and independent of which projection direction supplies the contact force geometry. + minDepthA = T2(0.0); + minDepthB = T2(0.0); + + // Compute face normal for triangle A first; triangle B normal is only needed if B->A projection hits. + T1 nA = normalize(cross(B1 - A1, C1 - A1)); + + //// TODO: And degenerated triangles? + + // ======================================================================== + // Projection-based approach: project each triangle onto the other's plane + // and clip using Sutherland-Hodgman algorithm + // ======================================================================== + + // Project triangle B onto triangle A's plane and clip against A. + // minDepthB is non-zero only if all of triB is fully submerged below triA's plane. + T2 depthBA, areaBA; + T1 centroidBA; + const bool contactBA = projectTriangleOntoTriangle(triB, triA, nA, depthBA, areaBA, centroidBA, minDepthB); + + if (!contactBA) { + // No contact detected, Provide separation info + T1 centA = (triA[0] + triA[1] + triA[2]) / 3.0; + T1 centB = (triB[0] + triB[1] + triB[2]) / 3.0; + T1 sep = centA - centB; + T2 sepLen2 = dot(sep, sep); + + if (sepLen2 > (DEME_TINY_FLOAT * DEME_TINY_FLOAT)) { + T2 sepLen = sqrt(sepLen2); + normal = sep / sepLen; + depth = -sepLen; // Negative for separation + point = (centA + centB) * 0.5; + } else { + normal = nA; + depth = -DEME_TINY_FLOAT; + point = centA; + } + projectedArea = 0.0; + return false; + } + + // Project triangle A onto triangle B's plane and clip against B. + // minDepthA is non-zero only if all of triA is fully submerged below triB's plane. + T1 nB = normalize(cross(B2 - A2, C2 - A2)); + T2 depthAB, areaAB; + T1 centroidAB; + const bool contactAB = projectTriangleOntoTriangle(triA, triB, nB, depthAB, areaAB, centroidAB, minDepthA); + + if (!contactAB) { + // No contact detected, Provide separation info + T1 centA = (triA[0] + triA[1] + triA[2]) / 3.0; + T1 centB = (triB[0] + triB[1] + triB[2]) / 3.0; + T1 sep = centA - centB; + T2 sepLen2 = dot(sep, sep); + + if (sepLen2 > (DEME_TINY_FLOAT * DEME_TINY_FLOAT)) { + T2 sepLen = sqrt(sepLen2); + normal = sep / sepLen; + depth = -sepLen; // Negative for separation + point = (centA + centB) * 0.5; + } else { + normal = nA; + depth = -DEME_TINY_FLOAT; + point = centA; + } + projectedArea = 0.0; + return false; + } + + // If both projection yields results, we select the one with less projection distance. + // This is important. For example, consider a small surface intersecting with a large surface nearly vertically. The + // smaller one projected onto the larger one: nearly 0 area (depending on numerical stability, may actually be 0); + // Larger one projected onto the smaller one: almost covers the entire smaller surface. We always want them both + // have non-0 projection area, and then select the shorter projection distance one. This is good for stability. + if (depthBA < depthAB) { + // Use B->A projection results. + depth = depthBA; + projectedArea = areaBA; + normal = -1.0 * nA; // Pay attention to direction + + // Contact point: centroid on A's plane, moved back by half depth + point = centroidBA - nA * (depth * 0.5); + } else { + // Use A->B projection results. + depth = depthAB; + projectedArea = areaAB; + normal = nB; + + // Contact point: centroid on B's plane, moved back by half depth + point = centroidAB - nB * (depth * 0.5); + } + + return true; +} + +#endif diff --git a/src/kernel/DEMContactKernels_SphTri_TriTri.cu b/src/kernel/DEMContactKernels_SphTri_TriTri.cu new file mode 100644 index 00000000..4635e787 --- /dev/null +++ b/src/kernel/DEMContactKernels_SphTri_TriTri.cu @@ -0,0 +1,710 @@ +// DEM contact detection-related custom kernels +#include +#include +_kernelIncludes_; + +inline __device__ float triRadiusFromNodes(const float3& center, + const float3& a1, + const float3& a2, + const float3& a3, + const float3& b1, + const float3& b2, + const float3& b3) { + float r2 = dot(a1 - center, a1 - center); + r2 = fmaxf(r2, dot(a2 - center, a2 - center)); + r2 = fmaxf(r2, dot(a3 - center, a3 - center)); + r2 = fmaxf(r2, dot(b1 - center, b1 - center)); + r2 = fmaxf(r2, dot(b2 - center, b2 - center)); + r2 = fmaxf(r2, dot(b3 - center, b3 - center)); + return sqrtf(r2); +} + +// #include +// #include +// #include +// #include +// #include + +// If clump templates are jitified, they will be below +_clumpTemplateDefs_; + +inline __device__ void fillSharedMemTriangles(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + const deme::spheresBinTouches_t& myThreadID, + const deme::bodyID_t& triID_in, + deme::bodyID_t* triOwnerIDs, + deme::bodyID_t* triIDs, + deme::family_t* triOwnerFamilies, + const float3* tri_vA1_all, + const float3* tri_vB1_all, + const float3* tri_vC1_all, + const float3* tri_shift_all, + float3* triANode1, + float3* triANode2, + float3* triANode3, + float3* triBNode1, + float3* triBNode2, + float3* triBNode3, + float3* triCenters) { + deme::bodyID_t tri_id = triID_in; + + const deme::bodyID_t ownerID = granData->ownerTriMesh[tri_id]; + triIDs[myThreadID] = tri_id; + triOwnerIDs[myThreadID] = ownerID; + triOwnerFamilies[myThreadID] = granData->familyID[ownerID]; + + // Load precomputed world-space A-face and shift. + float3 A1 = tri_vA1_all[tri_id]; + float3 A2 = tri_vB1_all[tri_id]; + float3 A3 = tri_vC1_all[tri_id]; + float3 sh = tri_shift_all[tri_id]; + + triANode1[myThreadID] = A1; + triANode2[myThreadID] = A2; + triANode3[myThreadID] = A3; + + // Reconstruct B-face (opposite normal) from A-face + shift; swap nodes 2<->3. + triBNode1[myThreadID] = A1 + sh; + triBNode2[myThreadID] = A3 + sh; + triBNode3[myThreadID] = A2 + sh; + + triCenters[myThreadID] = + triangleCentroid(triANode1[myThreadID], triANode2[myThreadID], triANode3[myThreadID]); +} + +template +inline __device__ void fillSharedMemSpheres(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + const deme::spheresBinTouches_t& myThreadID, + const deme::bodyID_t& sphereID_in, + deme::bodyID_t* ownerIDs, + deme::bodyID_t* bodyIDs, + deme::family_t* ownerFamilies, + T1* radii, + T2* bodyX, + T2* bodyY, + T2* bodyZ) { + // Keep a local `sphereID` for component acquisition macros. + deme::bodyID_t sphereID = sphereID_in; + + deme::bodyID_t ownerID = granData->ownerClumpBody[sphereID]; + bodyIDs[myThreadID] = sphereID; + ownerIDs[myThreadID] = ownerID; + ownerFamilies[myThreadID] = granData->familyID[ownerID]; + float ownerX, ownerY, ownerZ; + float myRadius; + float3 myRelPos; + + // Get my component offset info from either jitified arrays or global memory + // Outputs myRelPos, myRadius (in CD kernels, radius needs to be expanded) + // Use an input named exactly `sphereID' which is the id of this sphere component + { + _componentAcqStrat_; + myRadius += granData->marginSizeSphere[sphereID]; + } + + // These locations does not include the LBF offset + voxelIDToPosition( + ownerX, ownerY, ownerZ, granData->voxelID[ownerID], granData->locX[ownerID], granData->locY[ownerID], + granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); + float4 myOriQ; + myOriQ.w = granData->oriQw[ownerID]; + myOriQ.x = granData->oriQx[ownerID]; + myOriQ.y = granData->oriQy[ownerID]; + myOriQ.z = granData->oriQz[ownerID]; + applyOriQToVector3(myRelPos, myOriQ); + bodyX[myThreadID] = ownerX + myRelPos.x; + bodyY[myThreadID] = ownerY + myRelPos.y; + bodyZ[myThreadID] = ownerZ + myRelPos.z; + radii[myThreadID] = myRadius; +} + +inline __device__ bool checkPrismPrismContact(deme::DEMSimParams* simParams, + const float3& triANode1, + const float3& triANode2, + const float3& triANode3, + const float3& triBNode1, + const float3& triBNode2, + const float3& triBNode3, + const float3& triANode1_other, + const float3& triANode2_other, + const float3& triANode3_other, + const float3& triBNode1_other, + const float3& triBNode2_other, + const float3& triBNode3_other) { + // Calculate the contact point between 2 prisms, and return whether they are in contact + bool in_contact = + calc_prism_contact(triANode1, triANode2, triANode3, triBNode1, triBNode2, triBNode3, triANode1_other, + triANode2_other, triANode3_other, triBNode1_other, triBNode2_other, triBNode3_other); + return in_contact; +} + +DEME_KERNEL void getNumberOfTriangleContactsEachBin(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::bodyID_t* sphereIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDs, + deme::spheresBinTouches_t* numSpheresBinTouches, + deme::binSphereTouchPairs_t* sphereIDsLookUpTable, + deme::binID_t* mapTriActBinToSphActBin, + deme::bodyID_t* triIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDsForTri, + deme::trianglesBinTouches_t* numTrianglesBinTouches, + deme::binsTriangleTouchPairs_t* triIDsLookUpTable, + deme::binContactPairs_t* numTriSphContactsInEachBin, + deme::binContactPairs_t* numTriTriContactsInEachBin, + const float3* tri_vA1_all, + const float3* tri_vB1_all, + const float3* tri_vC1_all, + const float3* tri_shift_all, + size_t nActiveBinsForTri, + bool meshUniversalContact) { + // Shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. + __shared__ deme::bodyID_t triOwnerIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::bodyID_t triIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triCenters[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::family_t triOwnerFamilies[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::binContactPairs_t blockSphTriPairCnt, blockTriTriPairCnt; + + // typedef cub::BlockReduce BlockReduceT; + // __shared__ typename BlockReduceT::TempStorage temp_storage; + + const deme::trianglesBinTouches_t nTriInBin = numTrianglesBinTouches[blockIdx.x]; + const deme::binID_t binID = activeBinIDsForTri[blockIdx.x]; + if (threadIdx.x == 0 && nTriInBin > simParams->errOutBinTriNum) { + DEME_ABORT_KERNEL( + "Bin %u contains %u triangular mesh facets, exceeding maximum allowance (%u).\nIf you want the solver to " + "run despite this, set allowance higher via SetMaxTriangleInBin before simulation starts.", + blockIdx.x, nTriInBin, simParams->errOutBinTriNum); + } + const deme::spheresBinTouches_t myThreadID = threadIdx.x; + // But what is the index of the same binID in array activeBinIDs? Well, mapTriActBinToSphActBin comes to rescure. + const deme::binID_t indForAcqSphInfo = mapTriActBinToSphActBin[blockIdx.x]; + // If it is not an active bin from the perspective of the spheres, then we can move on, unless mesh universal + // contact + if (indForAcqSphInfo == deme::NULL_BINID && !meshUniversalContact) { + if (threadIdx.x == 0) { + numTriSphContactsInEachBin[blockIdx.x] = 0; + } + return; + } + const deme::binsTriangleTouchPairs_t thisTriTableEntry = triIDsLookUpTable[blockIdx.x]; + // Have to allocate my sphere info first (each thread handles a sphere) + const deme::spheresBinTouches_t nSphInBin = + (indForAcqSphInfo == deme::NULL_BINID) ? 0 : numSpheresBinTouches[indForAcqSphInfo]; + const deme::binSphereTouchPairs_t thisSphereTableEntry = + (nSphInBin == 0) ? 0 : sphereIDsLookUpTable[indForAcqSphInfo]; + + // No need to check if there are too many spheres, since we did in another kernel + if (myThreadID == 0) { + blockSphTriPairCnt = 0; + blockTriTriPairCnt = 0; + } + __syncthreads(); + + // This bin may have more than 128 (default) triangles, so we process it by 128-triangle batch + for (deme::trianglesBinTouches_t processed_count = 0; processed_count < nTriInBin; + processed_count += DEME_NUM_TRIANGLES_PER_CD_BATCH) { + // After this batch is processed, how many tris are still left to go in this bin? + const deme::trianglesBinTouches_t leftover_count = + (nTriInBin - processed_count > DEME_NUM_TRIANGLES_PER_CD_BATCH) + ? nTriInBin - processed_count - DEME_NUM_TRIANGLES_PER_CD_BATCH + : 0; + // How many threads are still active in this batch + const deme::trianglesBinTouches_t this_batch_active_count = + (leftover_count > 0) ? DEME_NUM_TRIANGLES_PER_CD_BATCH : nTriInBin - processed_count; + // If I need to work on shared memory allocation + if (myThreadID < this_batch_active_count) { + deme::bodyID_t triID = triIDsEachBinTouches_sorted[thisTriTableEntry + processed_count + myThreadID]; + fillSharedMemTriangles(simParams, granData, myThreadID, triID, triOwnerIDs, triIDs, triOwnerFamilies, + tri_vA1_all, tri_vB1_all, tri_vC1_all, tri_shift_all, triANode1, triANode2, + triANode3, triBNode1, triBNode2, triBNode3, triCenters); + } + __syncthreads(); + + // We may have more spheres than threads, so we have to process by batch too + for (deme::spheresBinTouches_t processed_sph = 0; processed_sph < nSphInBin; + processed_sph += DEME_KT_CD_NTHREADS_PER_BLOCK) { + // Thread will go idle if no more sphere is for it + if (processed_sph + myThreadID < nSphInBin) { + deme::bodyID_t sphereID = + sphereIDsEachBinTouches_sorted[thisSphereTableEntry + processed_sph + myThreadID]; + deme::bodyID_t ownerID; + deme::family_t ownerFamily; + float myRadius; + float3 sphXYZ; + + // Borrow it from another kernel file... + fillSharedMemSpheres(simParams, granData, 0, sphereID, &ownerID, &sphereID, &ownerFamily, + &myRadius, &sphXYZ.x, &sphXYZ.y, &sphXYZ.z); + // Test contact with each triangle in shared memory + for (deme::trianglesBinTouches_t ind = 0; ind < this_batch_active_count; ind++) { + // A mesh facet and a sphere may have the same owner... although it is not possible with the current + // implementation... + if (ownerID == triOwnerIDs[ind]) + continue; + + // Grab family number from memory + unsigned int maskMatID = locateMaskPair(ownerFamily, triOwnerFamilies[ind]); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + // The smaller of the two added margins is recorded... + float artificialMargin = (granData->familyExtraMarginSize[ownerFamily] < + granData->familyExtraMarginSize[triOwnerFamilies[ind]]) + ? granData->familyExtraMarginSize[ownerFamily] + : granData->familyExtraMarginSize[triOwnerFamilies[ind]]; + + float3 cntPnt, normal; + float depth; + bool in_contact_A, in_contact_B; + // NOTE: checkTriSphereOverlap_directional, instead of checkTriSphereOverlap, is in use here. This + // is because if the later is in use, then if a sphere is between 2 sandwiching triangles, then its + // potential contact with the original triangle will not be registered. + in_contact_A = checkTriSphereOverlap_directional( + triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); + // If the contact is too shallow (smaller than the smaller of artificial margin, then it can be + // dropped to reduce the overall number of contact pairs, as when making sandwich, the added safety + // margin already includes familyExtra). Note checkTriSphereOverlap_directional gives positive + // number for contacts. + in_contact_A = in_contact_A && (depth > artificialMargin); + + // And triangle B... + in_contact_B = checkTriSphereOverlap_directional( + triBNode1[ind], triBNode2[ind], triBNode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); + // Same treatment for B... + in_contact_B = in_contact_B && (depth > artificialMargin); + + // Note the contact point must be calculated through one triangle, not the 2 phantom + // triangles; or we will have double count problems. Use the first triangle as standard. + if (in_contact_A || in_contact_B) { + snap_to_face(triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, cntPnt); + deme::binID_t contactPntBin = getPointBinID( + cntPnt.x, cntPnt.y, cntPnt.z, simParams->dyn.inv_binSize, simParams->nbX, simParams->nbY); + if (contactPntBin == binID) { + atomicAdd(&blockSphTriPairCnt, 1); + } + } + } // End of a 256-sphere sweep + } + } // End of sweeping through all spheres in bin + + // Now all sph--tri pairs are processed, so we move on to potential tri--tri pairs + if (meshUniversalContact) { + // We have n * (n - 1) / 2 pairs to compare. To ensure even workload, these pairs are distributed to all + // threads. + const unsigned int nPairsNeedHandling = + (unsigned int)this_batch_active_count * ((unsigned int)this_batch_active_count - 1) / 2; + // Note this distribution is not even, but we need all active threads to process the same amount of pairs, + // so that each thread can easily know its offset + const unsigned int nPairsEachHandles = + (nPairsNeedHandling + DEME_KT_CD_NTHREADS_PER_BLOCK - 1) / DEME_KT_CD_NTHREADS_PER_BLOCK; + + // i, j are local tri number in bin + unsigned int bodyA, bodyB; + // We can stop if this thread reaches the end of all potential pairs, nPairsNeedHandling + for (unsigned int ind = nPairsEachHandles * myThreadID; + ind < nPairsNeedHandling && ind < nPairsEachHandles * (myThreadID + 1); ind++) { + recoverCntPair(bodyA, bodyB, ind, this_batch_active_count); + // For 2 bodies to be considered in contact, the contact point must be in this bin (to avoid + // double-counting), and they do not belong to the same clump + if (triOwnerIDs[bodyA] == triOwnerIDs[bodyB]) + continue; + // Grab family number from memory (not jitified: b/c family number can change frequently in a sim) + unsigned int bodyAFamily = triOwnerFamilies[bodyA]; + unsigned int bodyBFamily = triOwnerFamilies[bodyB]; + unsigned int maskMatID = locateMaskPair(bodyAFamily, bodyBFamily); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + // Tri--tri contact does not take into account bins, as duplicates will be removed in the end + bool in_contact = checkPrismPrismContact( + simParams, triANode1[bodyA], triANode2[bodyA], triANode3[bodyA], triBNode1[bodyA], triBNode2[bodyA], + triBNode3[bodyA], triANode1[bodyB], triANode2[bodyB], triANode3[bodyB], triBNode1[bodyB], + triBNode2[bodyB], triBNode3[bodyB]); + + /* + if (in_contact && (contactPntBin != binID)) { + unsigned int ZZ = binID/(simParams->nbX*simParams->nbY); + unsigned int YY = binID%(simParams->nbX*simParams->nbY)/simParams->nbX; + unsigned int XX = binID%(simParams->nbX*simParams->nbY)%simParams->nbX; + double binLocX = (XX + 0.5) * simParams->binSize; + double binLocY = (YY + 0.5) * simParams->binSize; + double binLocZ = (ZZ + 0.5) * simParams->binSize; + printf("binLoc: %f, %f, %f\n", binLocX, binLocY, binLocZ); + printf("triANode1A: %f, %f, %f\n", triANode1[bodyA].x, triANode1[bodyA].y, triANode1[bodyA].z); + } + */ + + if (in_contact) { + atomicAdd(&blockTriTriPairCnt, 1); + } + } + + // Take care of the left-over triangles + if (myThreadID < this_batch_active_count) { + for (deme::trianglesBinTouches_t i = 0; i < leftover_count; i++) { + deme::bodyID_t cur_ownerID, cur_bodyID; + float3 cur_triANode1, cur_triANode2, cur_triANode3, cur_triBNode1, cur_triBNode2, cur_triBNode3; + float3 cur_triCenter; + deme::family_t cur_ownerFamily; + { + const deme::trianglesBinTouches_t cur_ind = + processed_count + DEME_NUM_TRIANGLES_PER_CD_BATCH + i; + + // Get the info of this tri in question here. Note this is a broadcast so should be relatively + // fast. And it's not really shared mem filling, just using that function to get the info. + deme::bodyID_t cur_triID = triIDsEachBinTouches_sorted[thisTriTableEntry + cur_ind]; + fillSharedMemTriangles(simParams, granData, 0, cur_triID, &cur_ownerID, &cur_bodyID, + &cur_ownerFamily, tri_vA1_all, tri_vB1_all, tri_vC1_all, tri_shift_all, + &cur_triANode1, &cur_triANode2, &cur_triANode3, &cur_triBNode1, + &cur_triBNode2, &cur_triBNode3, &cur_triCenter); + } + // Then each in-shared-mem sphere compares against it. But first, check if same owner... + if (triOwnerIDs[myThreadID] == cur_ownerID) + continue; + // Grab family number from memory (not jitified: b/c family number can change frequently in a sim) + unsigned int bodyAFamily = triOwnerFamilies[myThreadID]; + unsigned int maskMatID = locateMaskPair(bodyAFamily, cur_ownerFamily); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + // Tri--tri contact does not take into account bins, as duplicates will be removed in the end + bool in_contact = checkPrismPrismContact( + simParams, triANode1[myThreadID], triANode2[myThreadID], triANode3[myThreadID], + triBNode1[myThreadID], triBNode2[myThreadID], triBNode3[myThreadID], cur_triANode1, + cur_triANode2, cur_triANode3, cur_triBNode1, cur_triBNode2, cur_triBNode3); + + if (in_contact) { + atomicAdd(&blockTriTriPairCnt, 1); + } + } + } + } + __syncthreads(); + } // End of batch-wise triangle processing + + // deme::binContactPairs_t total_count = BlockReduceT(temp_storage).Sum(contact_count); + if (myThreadID == 0) { + numTriSphContactsInEachBin[blockIdx.x] = blockSphTriPairCnt; + if (meshUniversalContact) { + numTriTriContactsInEachBin[blockIdx.x] = blockTriTriPairCnt; + } + } +} + +DEME_KERNEL void populateTriangleContactsEachBin(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::bodyID_t* sphereIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDs, + deme::spheresBinTouches_t* numSpheresBinTouches, + deme::binSphereTouchPairs_t* sphereIDsLookUpTable, + deme::binID_t* mapTriActBinToSphActBin, + deme::bodyID_t* triIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDsForTri, + deme::trianglesBinTouches_t* numTrianglesBinTouches, + deme::binsTriangleTouchPairs_t* triIDsLookUpTable, + deme::contactPairs_t* triSphContactReportOffsets, + deme::contactPairs_t* triTriContactReportOffsets, + deme::bodyID_t* idSphA_sm, + deme::bodyID_t* idTriB_sm, + deme::contact_t* dType_sm, + deme::bodyID_t* idTriA_mm, + deme::bodyID_t* idTriB_mm, + deme::contact_t* dType_mm, + const float3* tri_vA1_all, + const float3* tri_vB1_all, + const float3* tri_vC1_all, + const float3* tri_shift_all, + size_t nActiveBinsForTri, + bool meshUniversalContact) { + // Shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. + __shared__ deme::bodyID_t triOwnerIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::bodyID_t triIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triANode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triBNode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ float3 triCenters[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::family_t triOwnerFamilies[DEME_NUM_TRIANGLES_PER_CD_BATCH]; + __shared__ deme::binContactPairs_t blockSphTriPairCnt, blockTriTriPairCnt; + + const deme::trianglesBinTouches_t nTriInBin = numTrianglesBinTouches[blockIdx.x]; + const deme::spheresBinTouches_t myThreadID = threadIdx.x; + const deme::binID_t binID = activeBinIDsForTri[blockIdx.x]; + // But what is the index of the same binID in array activeBinIDs? Well, mapTriActBinToSphActBin comes to rescure. + const deme::binID_t indForAcqSphInfo = mapTriActBinToSphActBin[blockIdx.x]; + // If it is not an active bin from the perspective of the spheres, then we can move on, unless mesh universal + // contact + if (indForAcqSphInfo == deme::NULL_BINID && !meshUniversalContact) { + return; + } + const deme::binsTriangleTouchPairs_t thisTriTableEntry = triIDsLookUpTable[blockIdx.x]; + // Have to allocate my sphere info first (each thread handles a sphere) + const deme::spheresBinTouches_t nSphInBin = + (indForAcqSphInfo == deme::NULL_BINID) ? 0 : numSpheresBinTouches[indForAcqSphInfo]; + const deme::binSphereTouchPairs_t thisSphereTableEntry = + (nSphInBin == 0) ? 0 : sphereIDsLookUpTable[indForAcqSphInfo]; + + // No need to check if there are too many spheres, since we did in another kernel + if (myThreadID == 0) { + blockSphTriPairCnt = 0; + blockTriTriPairCnt = 0; + } + const deme::contactPairs_t smReportOffset = triSphContactReportOffsets[blockIdx.x]; + const deme::contactPairs_t smReportOffset_end = triSphContactReportOffsets[blockIdx.x + 1]; + deme::contactPairs_t mmReportOffset, mmReportOffset_end; + if (meshUniversalContact) { + mmReportOffset = triTriContactReportOffsets[blockIdx.x]; + mmReportOffset_end = triTriContactReportOffsets[blockIdx.x + 1]; + } + __syncthreads(); + + // This bin may have more than 128 (default) triangles, so we process it by 128-triangle batch + for (deme::trianglesBinTouches_t processed_count = 0; processed_count < nTriInBin; + processed_count += DEME_NUM_TRIANGLES_PER_CD_BATCH) { + // After this batch is processed, how many tris are still left to go in this bin? + const deme::trianglesBinTouches_t leftover_count = + (nTriInBin - processed_count > DEME_NUM_TRIANGLES_PER_CD_BATCH) + ? nTriInBin - processed_count - DEME_NUM_TRIANGLES_PER_CD_BATCH + : 0; + // How many threads are still active in this batch + const deme::trianglesBinTouches_t this_batch_active_count = + (leftover_count > 0) ? DEME_NUM_TRIANGLES_PER_CD_BATCH : nTriInBin - processed_count; + + // If I need to work on shared memory allocation + if (myThreadID < this_batch_active_count) { + deme::bodyID_t triID = triIDsEachBinTouches_sorted[thisTriTableEntry + processed_count + myThreadID]; + fillSharedMemTriangles(simParams, granData, myThreadID, triID, triOwnerIDs, triIDs, triOwnerFamilies, + tri_vA1_all, tri_vB1_all, tri_vC1_all, tri_shift_all, triANode1, triANode2, + triANode3, triBNode1, triBNode2, triBNode3, triCenters); + } + __syncthreads(); + + // We may have more spheres than threads, so we have to process by batch too + for (deme::spheresBinTouches_t processed_sph = 0; processed_sph < nSphInBin; + processed_sph += DEME_KT_CD_NTHREADS_PER_BLOCK) { + // Thread will go idle if no more sphere is for it + if (processed_sph + myThreadID < nSphInBin) { + deme::bodyID_t sphereID = + sphereIDsEachBinTouches_sorted[thisSphereTableEntry + processed_sph + myThreadID]; + deme::bodyID_t ownerID; + deme::family_t ownerFamily; + float myRadius; + float3 sphXYZ; + + // Borrow it from another kernel file... + fillSharedMemSpheres(simParams, granData, 0, sphereID, &ownerID, &sphereID, &ownerFamily, + &myRadius, &sphXYZ.x, &sphXYZ.y, &sphXYZ.z); + // Test contact with each triangle in shared memory + for (deme::trianglesBinTouches_t ind = 0; ind < this_batch_active_count; ind++) { + // A mesh facet and a sphere may have the same owner... although it is not possible with the current + // implementation... + if (ownerID == triOwnerIDs[ind]) + continue; + + // Grab family number from memory + unsigned int maskMatID = locateMaskPair(ownerFamily, triOwnerFamilies[ind]); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + // The smaller of the two added margins is recorded... + float artificialMargin = (granData->familyExtraMarginSize[ownerFamily] < + granData->familyExtraMarginSize[triOwnerFamilies[ind]]) + ? granData->familyExtraMarginSize[ownerFamily] + : granData->familyExtraMarginSize[triOwnerFamilies[ind]]; + + float3 cntPnt, normal; + float depth; + bool in_contact_A, in_contact_B; + // NOTE: checkTriSphereOverlap_directional, instead of checkTriSphereOverlap, is in use here. This + // is because if the later is in use, then if a sphere is between 2 sandwiching triangles, then its + // potential contact with the original triangle will not be registered. + in_contact_A = checkTriSphereOverlap_directional( + triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); + // If the contact is too shallow (smaller than the smaller of artificial margin, then it can be + // dropped to reduce the overall number of contact pairs, as when making sandwich, the added safety + // margin already includes familyExtra). Note checkTriSphereOverlap_directional gives positive + // number for contacts. + in_contact_A = in_contact_A && (depth > artificialMargin); + + // And triangle B... + in_contact_B = checkTriSphereOverlap_directional( + triBNode1[ind], triBNode2[ind], triBNode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); + // Same treatment for B... + in_contact_B = in_contact_B && (depth > artificialMargin); + + // Note the contact point must be calculated through one triangle, not the 2 phantom + // triangles; or we will have double count problems. Use the first triangle as standard. + if (in_contact_A || in_contact_B) { + snap_to_face(triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, cntPnt); + deme::binID_t contactPntBin = getPointBinID( + cntPnt.x, cntPnt.y, cntPnt.z, simParams->dyn.inv_binSize, simParams->nbX, simParams->nbY); + if (contactPntBin == binID) { + deme::contactPairs_t inBlockOffset = smReportOffset + atomicAdd(&blockSphTriPairCnt, 1); + if (inBlockOffset < smReportOffset_end) { + idSphA_sm[inBlockOffset] = sphereID; + idTriB_sm[inBlockOffset] = triIDs[ind]; + dType_sm[inBlockOffset] = deme::SPHERE_TRIANGLE_CONTACT; + } + } + } + } // End of a 256-sphere sweep + } + } // End of sweeping through all spheres in bin + + // Now all sph--tri pairs are processed, so we move on to potential tri--tri pairs + if (meshUniversalContact) { + // We have n * (n - 1) / 2 pairs to compare. To ensure even workload, these pairs are distributed to all + // threads. + const unsigned int nPairsNeedHandling = + (unsigned int)this_batch_active_count * ((unsigned int)this_batch_active_count - 1) / 2; + // Note this distribution is not even, but we need all active threads to process the same amount of pairs, + // so that each thread can easily know its offset + const unsigned int nPairsEachHandles = + (nPairsNeedHandling + DEME_KT_CD_NTHREADS_PER_BLOCK - 1) / DEME_KT_CD_NTHREADS_PER_BLOCK; + + // i, j are local tri number in bin + unsigned int bodyA, bodyB; + // We can stop if this thread reaches the end of all potential pairs, nPairsNeedHandling + for (unsigned int ind = nPairsEachHandles * myThreadID; + ind < nPairsNeedHandling && ind < nPairsEachHandles * (myThreadID + 1); ind++) { + recoverCntPair(bodyA, bodyB, ind, this_batch_active_count); + // For 2 bodies to be considered in contact, the contact point must be in this bin (to avoid + // double-counting), and they do not belong to the same clump + if (triOwnerIDs[bodyA] == triOwnerIDs[bodyB]) + continue; + // Grab family number from memory (not jitified: b/c family number can change frequently in a sim) + unsigned int bodyAFamily = triOwnerFamilies[bodyA]; + unsigned int bodyBFamily = triOwnerFamilies[bodyB]; + unsigned int maskMatID = locateMaskPair(bodyAFamily, bodyBFamily); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + // Tri--tri contact does not take into account bins, as duplicates will be removed in the end + bool in_contact = checkPrismPrismContact( + simParams, triANode1[bodyA], triANode2[bodyA], triANode3[bodyA], triBNode1[bodyA], triBNode2[bodyA], + triBNode3[bodyA], triANode1[bodyB], triANode2[bodyB], triANode3[bodyB], triBNode1[bodyB], + triBNode2[bodyB], triBNode3[bodyB]); + + if (in_contact) { + deme::contactPairs_t inBlockOffset = mmReportOffset + atomicAdd(&blockTriTriPairCnt, 1); + // The chance of offset going out-of-bound is very low, lower than sph--bin CD step, but I put it + // here anyway + if (inBlockOffset < mmReportOffset_end) { + // ---------------------------------------------------------------------------- + // IMPORTANT NOTE: Here, we don't need to adjust A and B ids to ensure A < B, and it's + // automatically ensured due to 1) The binID--triID pairs were generated with an inherent order + // of triID, then processed through (stable) radix sort, which preserved the blockwise order of + // triID; 2) Then the ordered triIDs are loaded to shared mem, and the in-kernel contact + // detection had threads reconstruct shared mem offsets from a recoverCntPair process, which + // also ensures i < j. Therefore, the generated tri contact pair has A < B. However, a change + // in these processes could affect the ordering, so I added this superfluous check to be + // future-proof. + // ---------------------------------------------------------------------------- + const deme::bodyID_t triA_ID = triIDs[bodyA]; + const deme::bodyID_t triB_ID = triIDs[bodyB]; + deme::bodyID_t outA = triA_ID; + deme::bodyID_t outB = triB_ID; + if (triA_ID > triB_ID) { + outA = triB_ID; + outB = triA_ID; + } + idTriA_mm[inBlockOffset] = outA; + idTriB_mm[inBlockOffset] = outB; + dType_mm[inBlockOffset] = deme::TRIANGLE_TRIANGLE_CONTACT; + } + } + } + + // Take care of the left-over triangles + if (myThreadID < this_batch_active_count) { + for (deme::trianglesBinTouches_t i = 0; i < leftover_count; i++) { + deme::bodyID_t cur_ownerID, cur_bodyID; + float3 cur_triANode1, cur_triANode2, cur_triANode3, cur_triBNode1, cur_triBNode2, cur_triBNode3; + float3 cur_triCenter; + deme::family_t cur_ownerFamily; + { + const deme::trianglesBinTouches_t cur_ind = + processed_count + DEME_NUM_TRIANGLES_PER_CD_BATCH + i; + + // Get the info of this tri in question here. Note this is a broadcast so should be relatively + // fast. And it's not really shared mem filling, just using that function to get the info. + deme::bodyID_t cur_triID = triIDsEachBinTouches_sorted[thisTriTableEntry + cur_ind]; + fillSharedMemTriangles(simParams, granData, 0, cur_triID, &cur_ownerID, &cur_bodyID, + &cur_ownerFamily, tri_vA1_all, tri_vB1_all, tri_vC1_all, tri_shift_all, + &cur_triANode1, &cur_triANode2, &cur_triANode3, &cur_triBNode1, + &cur_triBNode2, &cur_triBNode3, &cur_triCenter); + } + // Then each in-shared-mem sphere compares against it. But first, check if same owner... + if (triOwnerIDs[myThreadID] == cur_ownerID) + continue; + // Grab family number from memory (not jitified: b/c family number can change frequently in a sim) + unsigned int bodyAFamily = triOwnerFamilies[myThreadID]; + unsigned int maskMatID = locateMaskPair(bodyAFamily, cur_ownerFamily); + // If marked no contact, skip ths iteration + if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { + continue; + } + + // Tri--tri contact does not take into account bins, as duplicates will be removed in the end + bool in_contact = checkPrismPrismContact( + simParams, triANode1[myThreadID], triANode2[myThreadID], triANode3[myThreadID], + triBNode1[myThreadID], triBNode2[myThreadID], triBNode3[myThreadID], cur_triANode1, + cur_triANode2, cur_triANode3, cur_triBNode1, cur_triBNode2, cur_triBNode3); + + if (in_contact) { + deme::contactPairs_t inBlockOffset = mmReportOffset + atomicAdd(&blockTriTriPairCnt, 1); + // The chance of offset going out-of-bound is very low, lower than sph--bin CD step, but I put + // it here anyway + if (inBlockOffset < mmReportOffset_end) { + const deme::bodyID_t triA_ID = triIDs[myThreadID]; + const deme::bodyID_t triB_ID = cur_bodyID; + deme::bodyID_t outA = triA_ID; + deme::bodyID_t outB = triB_ID; + if (triA_ID > triB_ID) { + outA = triB_ID; + outB = triA_ID; + } + idTriA_mm[inBlockOffset] = outA; + idTriB_mm[inBlockOffset] = outB; + dType_mm[inBlockOffset] = deme::TRIANGLE_TRIANGLE_CONTACT; + } + } + } + } + } + __syncthreads(); + } // End of batch-wise triangle processing + // __syncthreads(); + + // In practice, I've never seen non-illed contact slots that need to be resolved this way. It's purely for ultra + // safety. + if (threadIdx.x == 0) { + for (deme::contactPairs_t inBlockOffset = smReportOffset + blockSphTriPairCnt; + inBlockOffset < smReportOffset_end; inBlockOffset++) { + dType_sm[inBlockOffset] = deme::NOT_A_CONTACT; + } + if (meshUniversalContact) { + for (deme::contactPairs_t inBlockOffset = mmReportOffset + blockTriTriPairCnt; + inBlockOffset < mmReportOffset_end; inBlockOffset++) { + dType_mm[inBlockOffset] = deme::NOT_A_CONTACT; + } + } + } +} diff --git a/src/kernel/DEMContactKernels_SphereSphere.cu b/src/kernel/DEMContactKernels_SphereSphere.cu index 945179dc..90544f3a 100644 --- a/src/kernel/DEMContactKernels_SphereSphere.cu +++ b/src/kernel/DEMContactKernels_SphereSphere.cu @@ -1,6 +1,6 @@ // DEM contact detection-related custom kernels #include -#include +#include _kernelIncludes_; // #include @@ -16,7 +16,7 @@ template inline __device__ void fillSharedMemSpheres(deme::DEMSimParams* simParams, deme::DEMDataKT* granData, const deme::spheresBinTouches_t& myThreadID, - const deme::bodyID_t& sphereID, + const deme::bodyID_t& sphereID_in, deme::bodyID_t* ownerIDs, deme::bodyID_t* bodyIDs, deme::family_t* ownerFamilies, @@ -24,6 +24,8 @@ inline __device__ void fillSharedMemSpheres(deme::DEMSimParams* simParams, T2* bodyX, T2* bodyY, T2* bodyZ) { + deme::bodyID_t sphereID = sphereID_in; + deme::bodyID_t ownerID = granData->ownerClumpBody[sphereID]; bodyIDs[myThreadID] = sphereID; ownerIDs[myThreadID] = ownerID; @@ -37,17 +39,19 @@ inline __device__ void fillSharedMemSpheres(deme::DEMSimParams* simParams, // Use an input named exactly `sphereID' which is the id of this sphere component { _componentAcqStrat_; - myRadius += granData->marginSize[ownerID]; + myRadius += granData->marginSizeSphere[sphereID]; } + // These locations does not include the LBF offset voxelIDToPosition( ownerX, ownerY, ownerZ, granData->voxelID[ownerID], granData->locX[ownerID], granData->locY[ownerID], granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - float myOriQw = granData->oriQw[ownerID]; - float myOriQx = granData->oriQx[ownerID]; - float myOriQy = granData->oriQy[ownerID]; - float myOriQz = granData->oriQz[ownerID]; - applyOriQToVector3(myRelPos.x, myRelPos.y, myRelPos.z, myOriQw, myOriQx, myOriQy, myOriQz); + float4 myOriQ; + myOriQ.w = granData->oriQw[ownerID]; + myOriQ.x = granData->oriQx[ownerID]; + myOriQ.y = granData->oriQy[ownerID]; + myOriQ.z = granData->oriQz[ownerID]; + applyOriQToVector3(myRelPos, myOriQ); bodyX[myThreadID] = ownerX + (double)myRelPos.x; bodyY[myThreadID] = ownerY + (double)myRelPos.y; bodyZ[myThreadID] = ownerZ + (double)myRelPos.z; @@ -66,36 +70,27 @@ inline __device__ bool calcContactPoint(deme::DEMSimParams* simParams, deme::binID_t& binID, float artificialMarginA, float artificialMarginB) { - double contactPntX; - double contactPntY; - double contactPntZ; bool in_contact; - float normX; // Normal directions are placeholders here - float normY; - float normZ; double overlapDepth; // overlapDepth is needed for making artificial contacts not too loose. - //// TODO: I guess is fine too. - in_contact = checkSpheresOverlap(XA, YA, ZA, rA, XB, YB, ZB, rB, contactPntX, contactPntY, - contactPntZ, normX, normY, normZ, overlapDepth); + in_contact = checkSphereContactOverlapAndBin(XA, YA, ZA, rA, XB, YB, ZB, rB, simParams->dyn.inv_binSize, + simParams->nbX, simParams->nbY, overlapDepth, binID); // The contact needs to be larger than the smaller articifical margin so that we don't double count the artificially // added margin. This is a design choice, to avoid having too many contact pairs when adding artificial margins. float artificialMargin = (artificialMarginA < artificialMarginB) ? artificialMarginA : artificialMarginB; in_contact = in_contact && (overlapDepth > (double)artificialMargin); - binID = getPointBinID(contactPntX, contactPntY, contactPntZ, simParams->binSize, simParams->nbX, - simParams->nbY); return in_contact; } -__global__ void getNumberOfSphereContactsEachBin(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::bodyID_t* sphereIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDs, - deme::spheresBinTouches_t* numSpheresBinTouches, - deme::binSphereTouchPairs_t* sphereIDsLookUpTable, - deme::binContactPairs_t* numContactsInEachBin, - size_t nActiveBins) { +DEME_KERNEL void getNumberOfSphereContactsEachBin(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::bodyID_t* sphereIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDs, + deme::spheresBinTouches_t* numSpheresBinTouches, + deme::binSphereTouchPairs_t* sphereIDsLookUpTable, + deme::binContactPairs_t* numContactsInEachBin, + size_t nActiveBins) { // shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. __shared__ deme::bodyID_t ownerIDs[DEME_NUM_SPHERES_PER_CD_BATCH]; __shared__ deme::bodyID_t bodyIDs[DEME_NUM_SPHERES_PER_CD_BATCH]; // In this kernel, this is not used @@ -214,7 +209,6 @@ __global__ void getNumberOfSphereContactsEachBin(deme::DEMSimParams* simParams, } } } - __syncthreads(); // Take care of the left-overs. If there are left-overs, then this is a full block. But we still need to do the // check, because we could have more threads in a block than max_sphere_num. @@ -229,7 +223,7 @@ __global__ void getNumberOfSphereContactsEachBin(deme::DEMSimParams* simParams, deme::bodyID_t cur_sphereID = sphereIDsEachBinTouches_sorted[thisBodiesTableEntry + cur_ind]; // Get the info of this sphere in question here. Note this is a broadcast so should be relatively - // fast. + // fast. And it's not really shared mem filling, just using that function to get the info. fillSharedMemSpheres(simParams, granData, 0, cur_sphereID, &cur_ownerID, &cur_bodyID, &cur_ownerFamily, &cur_radii, &cur_bodyX, &cur_bodyY, &cur_bodyZ); @@ -265,17 +259,17 @@ __global__ void getNumberOfSphereContactsEachBin(deme::DEMSimParams* simParams, } } -__global__ void populateSphSphContactPairsEachBin(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::bodyID_t* sphereIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDs, - deme::spheresBinTouches_t* numSpheresBinTouches, - deme::binSphereTouchPairs_t* sphereIDsLookUpTable, - deme::contactPairs_t* contactReportOffsets, - deme::bodyID_t* idSphA, - deme::bodyID_t* idSphB, - deme::contact_t* dType, - size_t nActiveBins) { +DEME_KERNEL void populateSphereContactPairsEachBin(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + deme::bodyID_t* sphereIDsEachBinTouches_sorted, + deme::binID_t* activeBinIDs, + deme::spheresBinTouches_t* numSpheresBinTouches, + deme::binSphereTouchPairs_t* sphereIDsLookUpTable, + deme::contactPairs_t* contactReportOffsets, + deme::bodyID_t* idSphA, + deme::bodyID_t* idSphB, + deme::contact_t* dType, + size_t nActiveBins) { // shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. __shared__ deme::bodyID_t ownerIDs[DEME_NUM_SPHERES_PER_CD_BATCH]; __shared__ deme::bodyID_t bodyIDs[DEME_NUM_SPHERES_PER_CD_BATCH]; @@ -348,7 +342,7 @@ __global__ void populateSphSphContactPairsEachBin(deme::DEMSimParams* simParams, unsigned int bodyAFamily = ownerFamilies[bodyA]; unsigned int bodyBFamily = ownerFamilies[bodyB]; unsigned int maskMatID = locateMaskPair(bodyAFamily, bodyBFamily); - // If marked no contact, skip ths iteration + // If marked no contact, skip this iteration if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { continue; } @@ -364,8 +358,26 @@ __global__ void populateSphSphContactPairsEachBin(deme::DEMSimParams* simParams, // The chance of offset going out-of-bound is very low, lower than sph--bin CD step, but I put it // here anyway if (inBlockOffset < myReportOffset_end) { - idSphA[inBlockOffset] = bodyIDs[bodyA]; - idSphB[inBlockOffset] = bodyIDs[bodyB]; + // ---------------------------------------------------------------------------- + // IMPORTANT NOTE: Here, we don't need to adjust A and B ids to ensure A < B, and it's + // automatically ensured due to 1) The binID--sphereID pairs were generated with an inherent + // order of sphereID, then processed through (stable) radix sort, which preserved the blockwise + // order of sphereID; 2) Then the ordered sphereIDs are loaded to shared mem, and the in-kernel + // contact detection had threads reconstruct shared mem offsets from a recoverCntPair process, + // which also ensures i < j. Therefore, the generated sphere contact pair has A < B. However, a + // change in these processes could affect the ordering, so I added this superfluous check to be + // future-proof. + // ---------------------------------------------------------------------------- + const deme::bodyID_t idA = bodyIDs[bodyA]; + const deme::bodyID_t idB = bodyIDs[bodyB]; + if (idA <= idB) { + // This branch will be reached, always + idSphA[inBlockOffset] = idA; + idSphB[inBlockOffset] = idB; + } else { + idSphA[inBlockOffset] = idB; + idSphB[inBlockOffset] = idA; + } dType[inBlockOffset] = deme::SPHERE_SPHERE_CONTACT; } } @@ -414,8 +426,16 @@ __global__ void populateSphSphContactPairsEachBin(deme::DEMSimParams* simParams, // The chance of offset going out-of-bound is very low, lower than sph--bin CD step, but I put it // here anyway if (inBlockOffset < myReportOffset_end) { - idSphA[inBlockOffset] = bodyIDs[myThreadID]; - idSphB[inBlockOffset] = cur_bodyID; + const deme::bodyID_t idA = bodyIDs[myThreadID]; + const deme::bodyID_t idB = cur_bodyID; + if (idA <= idB) { + // This branch will be reached, always + idSphA[inBlockOffset] = idA; + idSphB[inBlockOffset] = idB; + } else { + idSphA[inBlockOffset] = idB; + idSphB[inBlockOffset] = idA; + } dType[inBlockOffset] = deme::SPHERE_SPHERE_CONTACT; } } diff --git a/src/kernel/DEMContactKernels_SphereTriangle.cu b/src/kernel/DEMContactKernels_SphereTriangle.cu deleted file mode 100644 index efbce705..00000000 --- a/src/kernel/DEMContactKernels_SphereTriangle.cu +++ /dev/null @@ -1,427 +0,0 @@ -// DEM contact detection-related custom kernels -#include -#include -#include -_kernelIncludes_; - -// #include -// #include -// #include -// #include -// #include - -// If clump templates are jitified, they will be below -_clumpTemplateDefs_; - -inline __device__ void fillSharedMemTriangles(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - const deme::spheresBinTouches_t& myThreadID, - const deme::bodyID_t& triID, - deme::bodyID_t* triOwnerIDs, - deme::bodyID_t* triIDs, - deme::family_t* triOwnerFamilies, - float3* sandwichANode1, - float3* sandwichANode2, - float3* sandwichANode3, - float3* sandwichBNode1, - float3* sandwichBNode2, - float3* sandwichBNode3, - float3* triANode1, - float3* triANode2, - float3* triANode3, - float3* triBNode1, - float3* triBNode2, - float3* triBNode3) { - deme::bodyID_t ownerID = granData->ownerMesh[triID]; - triIDs[myThreadID] = triID; - triOwnerIDs[myThreadID] = ownerID; - triOwnerFamilies[myThreadID] = granData->familyID[ownerID]; - double3 ownerXYZ; - float3 node1, node2, node3; - - voxelIDToPosition( - ownerXYZ.x, ownerXYZ.y, ownerXYZ.z, granData->voxelID[ownerID], granData->locX[ownerID], - granData->locY[ownerID], granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - float myOriQw = granData->oriQw[ownerID]; - float myOriQx = granData->oriQx[ownerID]; - float myOriQy = granData->oriQy[ownerID]; - float myOriQz = granData->oriQz[ownerID]; - - { - node1 = sandwichANode1[triID]; - node2 = sandwichANode2[triID]; - node3 = sandwichANode3[triID]; - applyOriQToVector3(node1.x, node1.y, node1.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(node2.x, node2.y, node2.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(node3.x, node3.y, node3.z, myOriQw, myOriQx, myOriQy, myOriQz); - triANode1[myThreadID] = ownerXYZ + node1; - triANode2[myThreadID] = ownerXYZ + node2; - triANode3[myThreadID] = ownerXYZ + node3; - } - { - node1 = sandwichBNode1[triID]; - node2 = sandwichBNode2[triID]; - node3 = sandwichBNode3[triID]; - applyOriQToVector3(node1.x, node1.y, node1.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(node2.x, node2.y, node2.z, myOriQw, myOriQx, myOriQy, myOriQz); - applyOriQToVector3(node3.x, node3.y, node3.z, myOriQw, myOriQx, myOriQy, myOriQz); - triBNode1[myThreadID] = ownerXYZ + node1; - triBNode2[myThreadID] = ownerXYZ + node2; - triBNode3[myThreadID] = ownerXYZ + node3; - } -} - -template -inline __device__ void fillSharedMemSpheres(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - const deme::spheresBinTouches_t& myThreadID, - const deme::bodyID_t& sphereID, - deme::bodyID_t* ownerIDs, - deme::bodyID_t* bodyIDs, - deme::family_t* ownerFamilies, - T1* radii, - T2* bodyX, - T2* bodyY, - T2* bodyZ) { - deme::bodyID_t ownerID = granData->ownerClumpBody[sphereID]; - bodyIDs[myThreadID] = sphereID; - ownerIDs[myThreadID] = ownerID; - ownerFamilies[myThreadID] = granData->familyID[ownerID]; - double ownerX, ownerY, ownerZ; - float myRadius; - float3 myRelPos; - - // Get my component offset info from either jitified arrays or global memory - // Outputs myRelPos, myRadius (in CD kernels, radius needs to be expanded) - // Use an input named exactly `sphereID' which is the id of this sphere component - { - _componentAcqStrat_; - myRadius += granData->marginSize[ownerID]; - } - - voxelIDToPosition( - ownerX, ownerY, ownerZ, granData->voxelID[ownerID], granData->locX[ownerID], granData->locY[ownerID], - granData->locZ[ownerID], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - float myOriQw = granData->oriQw[ownerID]; - float myOriQx = granData->oriQx[ownerID]; - float myOriQy = granData->oriQy[ownerID]; - float myOriQz = granData->oriQz[ownerID]; - applyOriQToVector3(myRelPos.x, myRelPos.y, myRelPos.z, myOriQw, myOriQx, myOriQy, myOriQz); - bodyX[myThreadID] = ownerX + (double)myRelPos.x; - bodyY[myThreadID] = ownerY + (double)myRelPos.y; - bodyZ[myThreadID] = ownerZ + (double)myRelPos.z; - radii[myThreadID] = myRadius; -} - -__global__ void getNumberOfSphTriContactsEachBin(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::bodyID_t* sphereIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDs, - deme::spheresBinTouches_t* numSpheresBinTouches, - deme::binSphereTouchPairs_t* sphereIDsLookUpTable, - deme::binID_t* mapTriActBinToSphActBin, - deme::bodyID_t* triIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDsForTri, - deme::trianglesBinTouches_t* numTrianglesBinTouches, - deme::binsTriangleTouchPairs_t* triIDsLookUpTable, - deme::binContactPairs_t* numTriSphContactsInEachBin, - float3* sandwichANode1, - float3* sandwichANode2, - float3* sandwichANode3, - float3* sandwichBNode1, - float3* sandwichBNode2, - float3* sandwichBNode3, - size_t nActiveBinsForTri) { - // Shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. - __shared__ deme::bodyID_t triOwnerIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::bodyID_t triIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::family_t triOwnerFamilies[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::binContactPairs_t blockPairCnt; - - // typedef cub::BlockReduce BlockReduceT; - // __shared__ typename BlockReduceT::TempStorage temp_storage; - - const deme::trianglesBinTouches_t nTriInBin = numTrianglesBinTouches[blockIdx.x]; - const deme::binID_t binID = activeBinIDsForTri[blockIdx.x]; - if (threadIdx.x == 0 && nTriInBin > simParams->errOutBinTriNum) { - DEME_ABORT_KERNEL( - "Bin %u contains %u triangular mesh facets, exceeding maximum allowance (%u).\nIf you want the solver to " - "run despite this, set allowance higher via SetMaxTriangleInBin before simulation starts.", - blockIdx.x, nTriInBin, simParams->errOutBinTriNum); - } - const deme::spheresBinTouches_t myThreadID = threadIdx.x; - // But what is the index of the same binID in array activeBinIDs? Well, mapTriActBinToSphActBin comes to rescure. - const deme::binID_t indForAcqSphInfo = mapTriActBinToSphActBin[blockIdx.x]; - // If it is not an active bin from the perspective of the spheres, then we can move on - if (indForAcqSphInfo == deme::NULL_BINID) { - if (threadIdx.x == 0) { - numTriSphContactsInEachBin[blockIdx.x] = 0; - } - return; - } - // Have to allocate my sphere info first (each thread handles a sphere) - const deme::binSphereTouchPairs_t thisSphereTableEntry = sphereIDsLookUpTable[indForAcqSphInfo]; - const deme::binsTriangleTouchPairs_t thisTriTableEntry = triIDsLookUpTable[blockIdx.x]; - // No need to check if there are too many spheres, since we did in another kernel - const deme::spheresBinTouches_t nSphInBin = numSpheresBinTouches[indForAcqSphInfo]; - if (myThreadID == 0) - blockPairCnt = 0; - __syncthreads(); - - // This bin may have more than 128 (default) triangles, so we process it by 128-triangle batch - for (deme::trianglesBinTouches_t processed_count = 0; processed_count < nTriInBin; - processed_count += DEME_NUM_TRIANGLES_PER_CD_BATCH) { - // How many threads are still active in this batch - const deme::trianglesBinTouches_t this_batch_active_count = - (nTriInBin - processed_count > DEME_NUM_TRIANGLES_PER_CD_BATCH) ? DEME_NUM_TRIANGLES_PER_CD_BATCH - : nTriInBin - processed_count; - // If I need to work on shared memory allocation - if (myThreadID < this_batch_active_count) { - deme::bodyID_t triID = triIDsEachBinTouches_sorted[thisTriTableEntry + processed_count + myThreadID]; - fillSharedMemTriangles(simParams, granData, myThreadID, triID, triOwnerIDs, triIDs, triOwnerFamilies, - sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, sandwichBNode2, - sandwichBNode3, triANode1, triANode2, triANode3, triBNode1, triBNode2, triBNode3); - } - __syncthreads(); - - // We may have more spheres than threads, so we have to process by batch too - for (deme::spheresBinTouches_t proessed_sph = 0; proessed_sph < nSphInBin; - proessed_sph += DEME_KT_CD_NTHREADS_PER_BLOCK) { - // Thread will go idle if no more sphere is for it - if (proessed_sph + myThreadID < nSphInBin) { - deme::bodyID_t sphereID = - sphereIDsEachBinTouches_sorted[thisSphereTableEntry + proessed_sph + myThreadID]; - deme::bodyID_t ownerID; - deme::family_t ownerFamily; - float myRadius; - float3 sphXYZ; - - // Borrow it from another kernel file... - fillSharedMemSpheres(simParams, granData, 0, sphereID, &ownerID, &sphereID, &ownerFamily, - &myRadius, &sphXYZ.x, &sphXYZ.y, &sphXYZ.z); - - // Test contact with each triangle in shared memory - for (deme::trianglesBinTouches_t ind = 0; ind < this_batch_active_count; ind++) { - // A mesh facet and a sphere may have the same owner... although it is not possible with the current - // implementation... - if (ownerID == triOwnerIDs[ind]) - continue; - - // Grab family number from memory - unsigned int maskMatID = locateMaskPair(ownerFamily, triOwnerFamilies[ind]); - // If marked no contact, skip ths iteration - if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { - continue; - } - // The smaller of the two added margins is recorded... - float artificialMargin = (granData->familyExtraMarginSize[ownerFamily] < - granData->familyExtraMarginSize[triOwnerFamilies[ind]]) - ? granData->familyExtraMarginSize[ownerFamily] - : granData->familyExtraMarginSize[triOwnerFamilies[ind]]; - - float3 cntPnt, normal; - float depth; - bool in_contact_A, in_contact_B; - // NOTE: triangle_sphere_CD_directional, instead of triangle_sphere_CD, is in use here. This is - // because if the later is in use, then if a sphere is between 2 sandwiching triangles, then its - // potential contact with the original triangle will not be registered. At the same time, we don't - // want to use triangle_sphere_CD_directional for the real force calculation, and the concern is - // mainly "sphere near needle tip" scenario. Think about it. - in_contact_A = triangle_sphere_CD_directional( - triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); - // If the contact is too shallow (smaller than the smaller of artificial margin, then it can be - // dropped to reduce the overall number of contact pairs). Note triangle_sphere_CD_directional gives - // negative for contacts. - in_contact_A = in_contact_A && (-depth > artificialMargin); - - // And triangle B... - in_contact_B = triangle_sphere_CD_directional( - triBNode1[ind], triBNode2[ind], triBNode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); - // Same treatment for B... - in_contact_B = in_contact_B && (-depth > artificialMargin); - - // Note the contact point must be calculated through the original triangle, not the 2 phantom - // triangles; or we will have double count problems. Use the first triangle as standard. - if (in_contact_A || in_contact_B) { - snap_to_face(triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, cntPnt); - deme::binID_t contactPntBin = getPointBinID( - cntPnt.x, cntPnt.y, cntPnt.z, simParams->binSize, simParams->nbX, simParams->nbY); - if (contactPntBin == binID) { - atomicAdd(&blockPairCnt, 1); - } - } - } // End of a 256-sphere sweep - } - - } // End of sweeping through all spheres in bin - __syncthreads(); - } // End of batch-wise triangle processing - - // deme::binContactPairs_t total_count = BlockReduceT(temp_storage).Sum(contact_count); - if (myThreadID == 0) { - numTriSphContactsInEachBin[blockIdx.x] = blockPairCnt; - } -} - -__global__ void populateTriSphContactsEachBin(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - deme::bodyID_t* sphereIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDs, - deme::spheresBinTouches_t* numSpheresBinTouches, - deme::binSphereTouchPairs_t* sphereIDsLookUpTable, - deme::binID_t* mapTriActBinToSphActBin, - deme::bodyID_t* triIDsEachBinTouches_sorted, - deme::binID_t* activeBinIDsForTri, - deme::trianglesBinTouches_t* numTrianglesBinTouches, - deme::binsTriangleTouchPairs_t* triIDsLookUpTable, - deme::contactPairs_t* triSphContactReportOffsets, - deme::bodyID_t* idSphA, - deme::bodyID_t* idTriB, - deme::contact_t* dType, - float3* sandwichANode1, - float3* sandwichANode2, - float3* sandwichANode3, - float3* sandwichBNode1, - float3* sandwichBNode2, - float3* sandwichBNode3, - size_t nActiveBinsForTri) { - // Shared storage for bodies involved in this bin. Pre-allocated so that each threads can easily use. - __shared__ deme::bodyID_t triOwnerIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::bodyID_t triIDs[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triANode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode1[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode2[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ float3 triBNode3[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::family_t triOwnerFamilies[DEME_NUM_TRIANGLES_PER_CD_BATCH]; - __shared__ deme::binContactPairs_t blockPairCnt; - - const deme::trianglesBinTouches_t nTriInBin = numTrianglesBinTouches[blockIdx.x]; - const deme::spheresBinTouches_t myThreadID = threadIdx.x; - const deme::binID_t binID = activeBinIDsForTri[blockIdx.x]; - // But what is the index of the same binID in array activeBinIDs? Well, mapTriActBinToSphActBin comes to rescure. - const deme::binID_t indForAcqSphInfo = mapTriActBinToSphActBin[blockIdx.x]; - // If it is not an active bin from the perspective of the spheres, then we can move on - if (indForAcqSphInfo == deme::NULL_BINID) { - return; - } - const deme::binSphereTouchPairs_t thisSphereTableEntry = sphereIDsLookUpTable[indForAcqSphInfo]; - const deme::spheresBinTouches_t nSphInBin = numSpheresBinTouches[indForAcqSphInfo]; - const deme::binsTriangleTouchPairs_t thisTriTableEntry = triIDsLookUpTable[blockIdx.x]; - // Get my offset for writing back to the global arrays that contain contact pair info - const deme::contactPairs_t myReportOffset = triSphContactReportOffsets[blockIdx.x]; - const deme::contactPairs_t myReportOffset_end = triSphContactReportOffsets[blockIdx.x + 1]; - if (myThreadID == 0) - blockPairCnt = 0; - __syncthreads(); - - // This bin may have more than 128 (default) triangles, so we process it by 128-triangle batch - for (deme::trianglesBinTouches_t processed_count = 0; processed_count < nTriInBin; - processed_count += DEME_NUM_TRIANGLES_PER_CD_BATCH) { - // How many threads are still active in this batch - const deme::trianglesBinTouches_t this_batch_active_count = - (nTriInBin - processed_count > DEME_NUM_TRIANGLES_PER_CD_BATCH) ? DEME_NUM_TRIANGLES_PER_CD_BATCH - : nTriInBin - processed_count; - // If I need to work on shared memory allocation - if (myThreadID < this_batch_active_count) { - deme::bodyID_t triID = triIDsEachBinTouches_sorted[thisTriTableEntry + processed_count + myThreadID]; - fillSharedMemTriangles(simParams, granData, myThreadID, triID, triOwnerIDs, triIDs, triOwnerFamilies, - sandwichANode1, sandwichANode2, sandwichANode3, sandwichBNode1, sandwichBNode2, - sandwichBNode3, triANode1, triANode2, triANode3, triBNode1, triBNode2, triBNode3); - } - __syncthreads(); - - // We may have more spheres than threads, so we have to process by batch too - for (deme::spheresBinTouches_t proessed_sph = 0; proessed_sph < nSphInBin; - proessed_sph += DEME_KT_CD_NTHREADS_PER_BLOCK) { - // Thread will go idle if no more sphere is for it - if (proessed_sph + myThreadID < nSphInBin) { - deme::bodyID_t sphereID = - sphereIDsEachBinTouches_sorted[thisSphereTableEntry + proessed_sph + myThreadID]; - deme::bodyID_t ownerID; - deme::family_t ownerFamily; - float myRadius; - float3 sphXYZ; - - // Borrow it from another kernel file... - fillSharedMemSpheres(simParams, granData, 0, sphereID, &ownerID, &sphereID, &ownerFamily, - &myRadius, &sphXYZ.x, &sphXYZ.y, &sphXYZ.z); - - // Test contact with each triangle in shared memory - for (deme::trianglesBinTouches_t ind = 0; ind < this_batch_active_count; ind++) { - // A mesh facet and a sphere may have the same owner... although it is not possible with the current - // implementation... - if (ownerID == triOwnerIDs[ind]) - continue; - - // Grab family number from memory - unsigned int maskMatID = locateMaskPair(ownerFamily, triOwnerFamilies[ind]); - // If marked no contact, skip ths iteration - if (granData->familyMasks[maskMatID] != deme::DONT_PREVENT_CONTACT) { - continue; - } - // The smaller of the two added margins is recorded... - float artificialMargin = (granData->familyExtraMarginSize[ownerFamily] < - granData->familyExtraMarginSize[triOwnerFamilies[ind]]) - ? granData->familyExtraMarginSize[ownerFamily] - : granData->familyExtraMarginSize[triOwnerFamilies[ind]]; - - float3 cntPnt, normal; - float depth; - bool in_contact_A, in_contact_B; - // NOTE: triangle_sphere_CD_directional, instead of triangle_sphere_CD, is in use here. This is - // because if the later is in use, then if a sphere is between 2 sandwiching triangles, then its - // potential contact with the original triangle will not be registered. At the same time, we don't - // want to use triangle_sphere_CD_directional for the real force calculation, and the concern is - // mainly "sphere near needle tip" scenario. Think about it. - in_contact_A = triangle_sphere_CD_directional( - triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); - // If the contact is too shallow (smaller than the smaller of artificial margin, then it can be - // dropped to reduce the overall number of contact pairs). Note triangle_sphere_CD_directional gives - // negative for contacts. - in_contact_A = in_contact_A && (-depth > artificialMargin); - - // And triangle B... - in_contact_B = triangle_sphere_CD_directional( - triBNode1[ind], triBNode2[ind], triBNode3[ind], sphXYZ, myRadius, normal, depth, cntPnt); - // Same treatment for B... - in_contact_B = in_contact_B && (-depth > artificialMargin); - - // Note the contact point must be calculated through the original triangle, not the 2 phantom - // triangles; or we will have double count problems. Use the first triangle as standard. - if (in_contact_A || in_contact_B) { - snap_to_face(triANode1[ind], triANode2[ind], triANode3[ind], sphXYZ, cntPnt); - deme::binID_t contactPntBin = getPointBinID( - cntPnt.x, cntPnt.y, cntPnt.z, simParams->binSize, simParams->nbX, simParams->nbY); - if (contactPntBin == binID) { - deme::contactPairs_t inBlockOffset = myReportOffset + atomicAdd(&blockPairCnt, 1); - if (inBlockOffset < myReportOffset_end) { - idSphA[inBlockOffset] = sphereID; - idTriB[inBlockOffset] = triIDs[ind]; - dType[inBlockOffset] = deme::SPHERE_MESH_CONTACT; - } - } - } - } // End of a 256-sphere sweep - } - } // End of sweeping through all spheres in bin - __syncthreads(); - } // End of batch-wise triangle processing - // __syncthreads(); - - // In practice, I've never seen non-illed contact slots that need to be resolved this way. It's purely for ultra - // safety. - if (threadIdx.x == 0) { - for (deme::contactPairs_t inBlockOffset = myReportOffset + blockPairCnt; inBlockOffset < myReportOffset_end; - inBlockOffset++) { - dType[inBlockOffset] = deme::NOT_A_CONTACT; - } - } -} diff --git a/src/kernel/DEMCustomizablePolicies/ContactInfoWriteBack.cu b/src/kernel/DEMCustomizablePolicies/ContactInfoWriteBack.cu index bf3a038b..bf472b56 100644 --- a/src/kernel/DEMCustomizablePolicies/ContactInfoWriteBack.cu +++ b/src/kernel/DEMCustomizablePolicies/ContactInfoWriteBack.cu @@ -1,4 +1,7 @@ -granData->contactPointGeometryA[myContactID] = locCPA; -granData->contactPointGeometryB[myContactID] = locCPB; -granData->contactForces[myContactID] = force; -granData->contactTorque_convToForce[myContactID] = torque_only_force; +granData->contactPointGeometryA[myPatchContactID] = locCPA; +granData->contactPointGeometryB[myPatchContactID] = locCPB; +granData->contactForces[myPatchContactID] = force; +granData->contactTorque_convToForce[myPatchContactID] = torque_only_force; +if (simParams->storeNormal) { + granData->contactNormals[myPatchContactID] = B2A; +} diff --git a/src/kernel/DEMCustomizablePolicies/ForceInKernelReductionStrat.cu b/src/kernel/DEMCustomizablePolicies/ForceInKernelReductionStrat.cu index 619241b7..35a9541a 100644 --- a/src/kernel/DEMCustomizablePolicies/ForceInKernelReductionStrat.cu +++ b/src/kernel/DEMCustomizablePolicies/ForceInKernelReductionStrat.cu @@ -1,14 +1,33 @@ +// Unlike the dedicated collection kernel, this policy consumes locCPA/locCPB directly from the force-calculation +// kernel. Those values are computed locally for active contacts and explicitly zeroed with the forces for inactive +// margin candidates, so this route cannot read an unwritten global contact-point slot. A separate zero-resultant guard +// is therefore unnecessary for the stochastic contact-ramp fix. +float3 forceA = force; +float3 forceB = make_float3(-force.x, -force.y, -force.z); +float3 torqueA = torque_only_force; +float3 torqueB = make_float3(-torque_only_force.x, -torque_only_force.y, -torque_only_force.z); + // Take care of A { - atomicAdd(granData->aX + AOwner, force.x / AOwnerMass); - atomicAdd(granData->aY + AOwner, force.y / AOwnerMass); - atomicAdd(granData->aZ + AOwner, force.z / AOwnerMass); + const bool bad_cp = !isfinite3(locCPA); + const bool bad_vec = !isfinite3(forceA) || !isfinite3(torqueA); + if (bad_cp || bad_vec) { + DEME_ABORT_KERNEL( + "In-place force reduction found invalid force/torque/contact point for A: contact owner %llu, type " + "%u, bad_vec=%d, bad_cp=%d.\n", + static_cast(AOwner), static_cast(ContactType), static_cast(bad_vec), + static_cast(bad_cp)); + } + + atomicAdd(granData->aX + AOwner, forceA.x / AOwnerMass); + atomicAdd(granData->aY + AOwner, forceA.y / AOwnerMass); + atomicAdd(granData->aZ + AOwner, forceA.z / AOwnerMass); // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque // only, not linear velocity - float3 myF = (force + torque_only_force); + float3 myF = (forceA + torqueA); // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt - applyOriQToVector3(myF.x, myF.y, myF.z, AOriQ.w, -AOriQ.x, -AOriQ.y, -AOriQ.z); + applyOriQToVector3(myF, make_float4(-AOriQ.x, -AOriQ.y, -AOriQ.z, AOriQ.w)); const float3 angAcc = cross(locCPA, myF) / AOwnerMOI; atomicAdd(granData->alphaX + AOwner, angAcc.x); atomicAdd(granData->alphaY + AOwner, angAcc.y); @@ -17,15 +36,25 @@ // Take care of B { - atomicAdd(granData->aX + BOwner, -force.x / BOwnerMass); - atomicAdd(granData->aY + BOwner, -force.y / BOwnerMass); - atomicAdd(granData->aZ + BOwner, -force.z / BOwnerMass); + const bool bad_cp = !isfinite3(locCPB); + const bool bad_vec = !isfinite3(forceB) || !isfinite3(torqueB); + if (bad_cp || bad_vec) { + DEME_ABORT_KERNEL( + "In-place force reduction found invalid force/torque/contact point for B: contact owner %llu, type " + "%u, bad_vec=%d, bad_cp=%d.\n", + static_cast(BOwner), static_cast(ContactType), static_cast(bad_vec), + static_cast(bad_cp)); + } + + atomicAdd(granData->aX + BOwner, forceB.x / BOwnerMass); + atomicAdd(granData->aY + BOwner, forceB.y / BOwnerMass); + atomicAdd(granData->aZ + BOwner, forceB.z / BOwnerMass); // torque_inForceForm is usually the contribution of rolling resistance and it contributes to torque // only, not linear velocity - float3 myF = -1.f * (force + torque_only_force); + float3 myF = (forceB + torqueB); // F is in global frame, but it needs to be in local to coordinate with moi and cntPnt - applyOriQToVector3(myF.x, myF.y, myF.z, BOriQ.w, -BOriQ.x, -BOriQ.y, -BOriQ.z); + applyOriQToVector3(myF, make_float4(-BOriQ.x, -BOriQ.y, -BOriQ.z, BOriQ.w)); const float3 angAcc = cross(locCPB, myF) / BOwnerMOI; atomicAdd(granData->alphaX + BOwner, angAcc.x); atomicAdd(granData->alphaY + BOwner, angAcc.y); diff --git a/src/kernel/DEMCustomizablePolicies/FrictionlessHertzianForceModel.cu b/src/kernel/DEMCustomizablePolicies/FrictionlessHertzianForceModel.cu index 9ac098a7..7a7d6e4e 100644 --- a/src/kernel/DEMCustomizablePolicies/FrictionlessHertzianForceModel.cu +++ b/src/kernel/DEMCustomizablePolicies/FrictionlessHertzianForceModel.cu @@ -9,34 +9,30 @@ if (overlapDepth > 0) { CoR_cnt = CoR[bodyAMatType][bodyBMatType]; } - float3 rotVelCPA, rotVelCPB; - { - // We also need the relative velocity between A and B in global frame to use in the damping terms - // To get that, we need contact points' rotational velocity in GLOBAL frame - // This is local rotational velocity (the portion of linear vel contributed by rotation) - rotVelCPA = cross(ARotVel, locCPA); - rotVelCPB = cross(BRotVel, locCPB); - // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); - } + // We also need the relative velocity between A and B in global frame to use in the damping terms + // To get that, we need contact points' rotational velocity in GLOBAL frame + // This is local rotational velocity (the portion of linear vel contributed by rotation) + float3 rotVelCPA = cross(ARotVel, locCPA); + float3 rotVelCPB = cross(BRotVel, locCPB); + // This is mapping from local rotational velocity to global + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); // The (total) relative linear velocity of A relative to B const float3 velB2A = (ALinVel + rotVelCPA) - (BLinVel + rotVelCPB); const float projection = dot(velB2A, B2A); const float mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - float sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + + // Contact radius (radial distance from contact center axis) called cnt_rad, computed from area + const float cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2.f * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); - float beta = loge / sqrt(loge * loge + deme::PI_SQUARED); + const float beta = loge / sqrt(loge * loge + deme::PI_SQUARED); - const float k_n = deme::TWO_OVER_THREE * Sn; - const float gamma_n = deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(Sn * mass_eff); + const float k_n = (2.f / 3.f) * Sn; + const float gamma_n = (2.f * sqrtf(5.f / 6.f)) * beta * sqrtf(Sn * mass_eff); - // normal force (that A feels) force += (k_n * overlapDepth + gamma_n * projection) * B2A; } diff --git a/src/kernel/DEMCustomizablePolicies/FullHertzianForceModel.cu b/src/kernel/DEMCustomizablePolicies/FullHertzianForceModel.cu index 836034b8..a7cd698c 100644 --- a/src/kernel/DEMCustomizablePolicies/FullHertzianForceModel.cu +++ b/src/kernel/DEMCustomizablePolicies/FullHertzianForceModel.cu @@ -18,22 +18,16 @@ if (overlapDepth > 0) { Crr_cnt = Crr[bodyAMatType][bodyBMatType]; } - float3 rotVelCPA, rotVelCPB; - { - // We also need the relative velocity between A and B in global frame to use in the damping terms - // To get that, we need contact points' rotational velocity in GLOBAL frame - // This is local rotational velocity (the portion of linear vel contributed by rotation) - rotVelCPA = cross(ARotVel, locCPA); - rotVelCPB = cross(BRotVel, locCPB); - // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); - } + // We also need the relative velocity between A and B in global frame to use in the damping terms + // To get that, we need contact points' rotational velocity in GLOBAL frame + // This is local rotational velocity (the portion of linear vel contributed by rotation) + float3 rotVelCPA = cross(ARotVel, locCPA); + float3 rotVelCPB = cross(BRotVel, locCPB); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, beta, cnt_rad; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -53,31 +47,35 @@ if (overlapDepth > 0) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + + // Contact radius (radial distance from contact center axis) called cnt_rad, computed from area + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2.f * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); - const float k_n = deme::TWO_OVER_THREE * Sn; - const float gamma_n = deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(Sn * mass_eff); + const float k_n = (2.f / 3.f) * Sn; + const float gamma_n = (2.f * sqrtf(5.f / 6.f)) * beta * sqrtf(Sn * mass_eff); force += (k_n * overlapDepth + gamma_n * projection) * B2A; // printf("normal force: %f, %f, %f\n", force.x, force.y, force.z); } // Rolling resistance part - if (Crr_cnt > 0.0) { + if (Crr_cnt > 0.f) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); - const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); - const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); + // Use geometric equivalence R_eff ≈ cnt_rad^2 / overlapDepth + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; + + const float kn_simple = (4.f / 3.f) * E_cnt * sqrtf(R_eff); + const float gn_simple = -2.f * sqrtf((5.f / 3.f) * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); const float d_coeff = gn_simple / (2.f * sqrtf(kn_simple * mass_eff)); - if (d_coeff < 1.0) { + if (d_coeff < 1.f) { float t_collision = deme::PI * sqrtf(mass_eff / (kn_simple * (1.f - d_coeff * d_coeff))); if (delta_time <= t_collision) { should_add_rolling_resistance = false; @@ -102,9 +100,9 @@ if (overlapDepth > 0) { } // Tangential force part - if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; - const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); + if (mu_cnt > 0.f) { + const float kt = 8.f * G_cnt * cnt_rad; + const float gt = -2.f * sqrtf(5.f / 3.f) * beta * sqrtf(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); if (ft > DEME_TINY_FLOAT) { diff --git a/src/kernel/DEMHelperKernels.cuh b/src/kernel/DEMHelperKernels.cuh index ff5703c6..581495df 100644 --- a/src/kernel/DEMHelperKernels.cuh +++ b/src/kernel/DEMHelperKernels.cuh @@ -12,7 +12,7 @@ #include #endif -// inline __device__ voxelID_t position2VoxelID +// __device__ voxelID_t position2VoxelID //////////////////////////////////////////////////////////////////////////////// // A few helper functions specific to DEM module @@ -20,18 +20,23 @@ // Sign function template -inline __host__ __device__ int sgn(const T1& val) { +__host__ __device__ int sgn(const T1& val) { return (T1(0) < val) - (val < T1(0)); } template -inline __host__ __device__ T1 dot3(const T1& x1, const T1& x2, const T1& x3, const T1& y1, const T1& y2, const T1& y3) { +__host__ __device__ T1 dot3(const T1& x1, const T1& x2, const T1& x3, const T1& y1, const T1& y2, const T1& y3) { return x1 * y1 + x2 * y2 + x3 * y3; } +template +__host__ __device__ bool isfinite3(const T1& v) { + return isfinite(v.x) && isfinite(v.y) && isfinite(v.z); +} + // Integer division that rounds towards -infty template -inline __host__ __device__ T1 div_floor(const T1& a, const T2& b) { +__host__ __device__ T1 div_floor(const T1& a, const T2& b) { T1 res = a / b; T1 rem = a % b; // Correct division result downwards if up-rounding happened, @@ -42,7 +47,7 @@ inline __host__ __device__ T1 div_floor(const T1& a, const T2& b) { // Modulus that rounds towards -infty template -inline __host__ __device__ T1 mod_floor(const T1& a, const T2& b) { +__host__ __device__ T1 mod_floor(const T1& a, const T2& b) { if (b < 0) // you can check for b == 0 separately and do what you want return -mod_floor(-a, -b); T1 ret = a % b; @@ -55,7 +60,7 @@ inline __host__ __device__ T1 mod_floor(const T1& a, const T2& b) { // corresponding flatten-ed non-zero entries (col-major like in matlab). This function does not assume i <= j. It is // used in locating masks that maps the contact between families. template -inline __host__ __device__ T1 locateMaskPair(const T1& i, const T1& j) { +__host__ __device__ T1 locateMaskPair(const T1& i, const T1& j) { if (i > j) return locateMaskPair(j, i); return (1 + j) * j / 2 + i; @@ -64,14 +69,14 @@ inline __host__ __device__ T1 locateMaskPair(const T1& i, const T1& j) { // Magic function that converts an index of a flatten-ed upper-triangular matrix (EXCLUDING the diagonal) to its // corresponding i and j. It is ROW-major. It is used to map contact pair numbers in a bin. template -inline __host__ __device__ void recoverCntPair(T1& i, T1& j, const T1& ind, const T1& n) { +__host__ __device__ void recoverCntPair(T1& i, T1& j, const T1& ind, const T1& n) { i = n - 2 - (T1)(sqrt((float)(4 * n * (n - 1) - 7 - 8 * ind)) / 2.0 - 0.5); j = ind + i + 1 + (n - i) * ((n - i) - 1) / 2 - n * (n - 1) / 2; } // Make sure a T1 type triplet falls in a range, then output as T2 type template -inline __host__ __device__ T2 clampBetween3Comp(const T1& data, const T2& low, const T2& high) { +__host__ __device__ T2 clampBetween3Comp(const T1& data, const T2& low, const T2& high) { T2 res; res.x = DEME_MIN(DEME_MAX(data.x, low.x), high.x); res.y = DEME_MIN(DEME_MAX(data.y, low.y), high.y); @@ -81,7 +86,7 @@ inline __host__ __device__ T2 clampBetween3Comp(const T1& data, const T2& low, c // Make sure a T1 type triplet falls in a range, then output as T2 type template -inline __host__ __device__ T2 clampBetween(const T1& data, const T2& low, const T2& high) { +__host__ __device__ T2 clampBetween(const T1& data, const T2& low, const T2& high) { T2 res; res = DEME_MIN(DEME_MAX(data, low), high); return res; @@ -89,12 +94,12 @@ inline __host__ __device__ T2 clampBetween(const T1& data, const T2& low, const // Chops a long ID (typically voxelID) into XYZ components template -inline __host__ __device__ void IDChopper(T1& X, - T1& Y, - T1& Z, - const T2& ID, - const unsigned char& nvXp2, - const unsigned char& nvYp2) { +__host__ __device__ void IDChopper(T1& X, + T1& Y, + T1& Z, + const T2& ID, + const unsigned char& nvXp2, + const unsigned char& nvYp2) { X = ID & (((T1)1 << nvXp2) - 1); // & operation here equals modulo Y = (ID >> nvXp2) & (((T1)1 << nvYp2) - 1); Z = (ID) >> (nvXp2 + nvYp2); @@ -102,12 +107,12 @@ inline __host__ __device__ void IDChopper(T1& X, // Packs XYZ components back to a long ID (typically voxelID) template -inline __host__ __device__ void IDPacker(T1& ID, - const T2& X, - const T2& Y, - const T2& Z, - const unsigned char& nvXp2, - const unsigned char& nvYp2) { +__host__ __device__ void IDPacker(T1& ID, + const T2& X, + const T2& Y, + const T2& Z, + const unsigned char& nvXp2, + const unsigned char& nvYp2) { ID = X; ID += Y << nvXp2; ID += Z << (nvXp2 + nvYp2); @@ -115,17 +120,17 @@ inline __host__ __device__ void IDPacker(T1& ID, // From a voxelID to (usually double-precision) xyz coordinate template -inline __host__ __device__ void voxelIDToPosition(T1& X, - T1& Y, - T1& Z, - const T2& ID, - const T3& subPosX, - const T3& subPosY, - const T3& subPosZ, - const unsigned char& nvXp2, - const unsigned char& nvYp2, - const T1& voxelSize, - const T1& l) { +__host__ __device__ void voxelIDToPosition(T1& X, + T1& Y, + T1& Z, + const T2& ID, + const T3& subPosX, + const T3& subPosY, + const T3& subPosZ, + const unsigned char& nvXp2, + const unsigned char& nvYp2, + const T1& voxelSize, + const T1& l) { T2 voxelIDX, voxelIDY, voxelIDZ; IDChopper(voxelIDX, voxelIDY, voxelIDZ, ID, nvXp2, nvYp2); X = (T1)voxelIDX * voxelSize + (T1)subPosX * l; @@ -135,17 +140,17 @@ inline __host__ __device__ void voxelIDToPosition(T1& X, // From xyz coordinate (usually double-precision) to voxelID template -inline __host__ __device__ void positionToVoxelID(T1& ID, - T2& subPosX, - T2& subPosY, - T2& subPosZ, - const T3& X, - const T3& Y, - const T3& Z, - const unsigned char& nvXp2, - const unsigned char& nvYp2, - const T3& voxelSize, - const T3& l) { +__host__ __device__ void positionToVoxelID(T1& ID, + T2& subPosX, + T2& subPosY, + T2& subPosZ, + const T3& X, + const T3& Y, + const T3& Z, + const unsigned char& nvXp2, + const unsigned char& nvYp2, + const T3& voxelSize, + const T3& l) { deme::voxelID_t voxelNumX = X / voxelSize; deme::voxelID_t voxelNumY = Y / voxelSize; deme::voxelID_t voxelNumZ = Z / voxelSize; @@ -159,7 +164,7 @@ inline __host__ __device__ void positionToVoxelID(T1& ID, } template -inline __host__ __device__ void +__host__ __device__ void applyOriQToVector3(T1& X, T1& Y, T1& Z, const T2& Qw, const T2& Qx, const T2& Qy, const T2& Qz) { T1 oldX = X; T1 oldY = Y; @@ -172,29 +177,42 @@ applyOriQToVector3(T1& X, T1& Y, T1& Z, const T2& Qw, const T2& Qx, const T2& Qy ((T2)2.0 * (Qw * Qw + Qz * Qz) - (T2)1.0) * oldZ; } +template +inline __host__ __device__ void applyOriQToVector3(T1& v, const T2& Q) { + auto oldX = v.x; + auto oldY = v.y; + auto oldZ = v.z; + const auto Qw = Q.w; + const auto Qx = Q.x; + const auto Qy = Q.y; + const auto Qz = Q.z; + v.x = (2 * (Qw * Qw + Qx * Qx) - 1) * oldX + (2 * (Qx * Qy - Qw * Qz)) * oldY + (2 * (Qx * Qz + Qw * Qy)) * oldZ; + v.y = (2 * (Qx * Qy + Qw * Qz)) * oldX + (2 * (Qw * Qw + Qy * Qy) - 1) * oldY + (2 * (Qy * Qz - Qw * Qx)) * oldZ; + v.z = (2 * (Qx * Qz - Qw * Qy)) * oldX + (2 * (Qy * Qz + Qw * Qx)) * oldY + (2 * (Qw * Qw + Qz * Qz) - 1) * oldZ; +} + template -inline __host__ __device__ void applyFrameTransformLocalToGlobal(T1& pos, const T2& vec, const T3& rot_Q) { - applyOriQToVector3(pos.x, pos.y, pos.z, rot_Q.w, rot_Q.x, rot_Q.y, rot_Q.z); +__host__ __device__ void applyFrameTransformLocalToGlobal(T1& pos, const T2& vec, const T3& rot_Q) { + applyOriQToVector3(pos, rot_Q); pos.x += vec.x; pos.y += vec.y; pos.z += vec.z; } template -inline __host__ __device__ T1 -distSquared(const T1& x1, const T1& y1, const T1& z1, const T1& x2, const T1& y2, const T1& z2) { +__host__ __device__ T1 distSquared(const T1& x1, const T1& y1, const T1& z1, const T1& x2, const T1& y2, const T1& z2) { return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2); } // Get the maginitude of a 3-component vector template -inline __host__ __device__ T1 magVector3(T1& x, T1& y, T1& z) { +__host__ __device__ T1 magVector3(T1& x, T1& y, T1& z) { return sqrt(x * x + y * y + z * z); } // Normalize a 3-component vector template -inline __host__ __device__ void normalizeVector3(T1& x, T1& y, T1& z) { +__host__ __device__ void normalizeVector3(T1& x, T1& y, T1& z) { T1 magnitude = sqrt(x * x + y * y + z * z); // TODO: Think about whether this is safe // if (magnitude < DEME_TINY_FLOAT) { @@ -207,13 +225,13 @@ inline __host__ __device__ void normalizeVector3(T1& x, T1& y, T1& z) { // Calculate the centroid of a triangle template -inline __host__ __device__ T1 triangleCentroid(const T1& p1, const T1& p2, const T1& p3) { +__host__ __device__ T1 triangleCentroid(const T1& p1, const T1& p2, const T1& p3) { return (p1 + p2 + p3) / 3.; } // Calculate the incenter of a triangle template -inline __host__ __device__ T1 triangleIncenter(const T1& p1, const T1& p2, const T1& p3) { +__host__ __device__ T1 triangleIncenter(const T1& p1, const T1& p2, const T1& p3) { float a = length(p2 - p3); float b = length(p1 - p3); float c = length(p1 - p2); @@ -226,118 +244,41 @@ inline __host__ __device__ T1 triangleIncenter(const T1& p1, const T1& p2, const // Hamilton product of 2 quaternions template -inline __host__ __device__ void HamiltonProduct(T1& A, - T1& B, - T1& C, - T1& D, - const T2 a1, - const T2 b1, - const T2 c1, - const T2 d1, - const T3 a2, - const T3 b2, - const T3 c2, - const T3 d2) { +__host__ __device__ void HamiltonProduct(T1& A, + T1& B, + T1& C, + T1& D, + const T2 a1, + const T2 b1, + const T2 c1, + const T2 d1, + const T3 a2, + const T3 b2, + const T3 c2, + const T3 d2) { A = a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2; B = a1 * b2 + b1 * a2 + c1 * d2 - d1 * c2; C = a1 * c2 - b1 * d2 + c1 * a2 + d1 * b2; D = a1 * d2 + b1 * c2 - c1 * b2 + d1 * a2; } -/* -// This version of checkSpheresOverlap is not used anymore for code clarity, and the fact that every use case now -// requires overlapDepth. -template -inline __device__ deme::contact_t checkSpheresOverlap(const T1& XA, - const T1& YA, - const T1& ZA, - const T1& radA, - const T1& XB, - const T1& YB, - const T1& ZB, - const T1& radB, - T1& CPX, - T1& CPY, - T1& CPZ) { - T1 centerDist2 = distSquared(XA, YA, ZA, XB, YB, ZB); - deme::contact_t contactType; - if (centerDist2 > (radA + radB) * (radA + radB)) { - contactType = deme::NOT_A_CONTACT; - } else { - contactType = deme::SPHERE_SPHERE_CONTACT; - } - // If getting this far, then 2 spheres have an intersection, let's calculate the intersection point - float B2AVecX = XA - XB; - float B2AVecY = YA - YB; - float B2AVecZ = ZA - ZB; - normalizeVector3(B2AVecX, B2AVecY, B2AVecZ); - T1 halfOverlapDepth = (radA + radB - sqrt(centerDist2)) / (T1)2; - // From center of B, towards center of A, move a distance of radB, then backtrack a bit, for half the overlap depth - CPX = XB + (radB - halfOverlapDepth) * B2AVecX; - CPY = YB + (radB - halfOverlapDepth) * B2AVecY; - CPZ = ZB + (radB - halfOverlapDepth) * B2AVecZ; - return contactType; -} -*/ - -/** - * Template arguments: - * - T1: the floating point accuracy level for contact point location/penetration depth - * - T2: the floating point accuracy level for the relative position of 2 bodies involved - * - * Basic idea: determines whether 2 spheres intersect and the intersection point coordinates which also gives the - * penetration length and bodyB's outward contact normal. - * - */ -template -inline __host__ __device__ deme::contact_t checkSpheresOverlap(const T1& XA, - const T1& YA, - const T1& ZA, - const T1& radA, - const T1& XB, - const T1& YB, - const T1& ZB, - const T1& radB, - T1& CPX, - T1& CPY, - T1& CPZ, - T2& normalX, - T2& normalY, - T2& normalZ, - T1& overlapDepth) { - T1 centerDist2 = distSquared(XA, YA, ZA, XB, YB, ZB); - deme::contact_t contactType; - if (centerDist2 > (radA + radB) * (radA + radB)) { - contactType = deme::NOT_A_CONTACT; - } else { - contactType = deme::SPHERE_SPHERE_CONTACT; - } - // If getting this far, then 2 spheres have an intersection, let's calculate the intersection point - normalX = XA - XB; - normalY = YA - YB; - normalZ = ZA - ZB; - normalizeVector3(normalX, normalY, normalZ); - overlapDepth = radA + radB - sqrt(centerDist2); - // From center of B, towards center of A, move a distance of radB, then backtrack a bit, for half the overlap depth - CPX = XB + (radB - overlapDepth / (T1)2) * normalX; - CPY = YB + (radB - overlapDepth / (T1)2) * normalY; - CPZ = ZB + (radB - overlapDepth / (T1)2) * normalZ; - return contactType; -} - // Compute the binID for a point in space template -inline __host__ __device__ T1 -getPointBinID(const double& X, const double& Y, const double& Z, const double& binSize, const T1& nbX, const T1& nbY) { - T1 binIDX = X / binSize; - T1 binIDY = Y / binSize; - T1 binIDZ = Z / binSize; +inline __host__ __device__ T1 getPointBinID(const double& X, + const double& Y, + const double& Z, + const double& inv_binSize, + const T1& nbX, + const T1& nbY) { + T1 binIDX = X * inv_binSize; + T1 binIDY = Y * inv_binSize; + T1 binIDZ = Z * inv_binSize; return binIDX + binIDY * nbX + binIDZ * nbX * nbY; } // Compute the binID using its indices in X, Y and Z directions template -inline __host__ __device__ T1 +__host__ __device__ T1 binIDFrom3Indices(const T1& X, const T1& Y, const T1& Z, const T1& nbX, const T1& nbY, const T1& nbZ) { if ((X < nbX) && (Y < nbY) && (Z < nbZ)) { return X + Y * nbX + Z * nbX * nbY; @@ -346,18 +287,130 @@ binIDFrom3Indices(const T1& X, const T1& Y, const T1& Z, const T1& nbX, const T1 } } +inline __host__ __device__ bool near_edge(float b) { + const float k = floorf(b); + const float d = fminf(b - k, (k + 1.0f) - b); + const float tau = 8.0f * fmaxf(fabsf(b) * 1.1920929e-7f, 1.1920929e-7f); + return d <= tau; +} + +// Computes the inclusive [imin, imax] bin range (clamped to [0, nb - 1]) along one axis with a fast +// FP32 path and an FP64 fallback near bin boundaries. +inline __host__ __device__ deme::AxisBounds axis_bounds(double p, double r, int nb, double invBinSize) { + const float invBinF = (float)invBinSize; + const float pF = (float)p; + const float rF = (float)r; + const float bPlusF = fmaf(rF, invBinF, pF * invBinF); + const float bMinusF = (pF - rF) * invBinF; + if (!(near_edge(bPlusF) || near_edge(bMinusF))) { + int imax = (int)fminf(bPlusF, (float)(nb - 1)); + int imin = (int)fmaxf(bMinusF, 0.0f); + if (imax < imin) { + return {0, -1}; + } + return {imin, imax}; + } + + const double bPlusD = fma(r, invBinSize, p * invBinSize); + const double bMinusD = (p - r) * invBinSize; + const int imax = (int)fmin(bPlusD, (double)(nb - 1)); + const int imin = (int)fmax(bMinusD, 0.0); + if (imax < imin) { + return {0, -1}; + } + return {imin, imax}; +} + +inline __host__ __device__ deme::contact_t checkSphereContactOverlapAndBin(const double& XA, + const double& YA, + const double& ZA, + const double& radA, + const double& XB, + const double& YB, + const double& ZB, + const double& radB, + const double& invBinSize, + const deme::binID_t& nbX, + const deme::binID_t& nbY, + double& overlapDepth, + deme::binID_t& binID) { + const float xA = (float)XA; + const float yA = (float)YA; + const float zA = (float)ZA; + const float xB = (float)XB; + const float yB = (float)YB; + const float zB = (float)ZB; + const float dx = xA - xB; + const float dy = yA - yB; + const float dz = zA - zB; + const float dist2f = fmaf(dz, dz, fmaf(dy, dy, dx * dx)); + const float rsumf = (float)radA + (float)radB; + const float r2f = rsumf * rsumf; + if (dist2f > r2f) { + overlapDepth = 0.0; + binID = (deme::binID_t)0; + return deme::NOT_A_CONTACT; + } + + float nx; + float ny; + float nz; + float distf = 0.0f; + if (dist2f > 0.0f) { + distf = sqrtf(dist2f); + const float invd = 1.0f / distf; + nx = dx * invd; + ny = dy * invd; + nz = dz * invd; + } else { + nx = 1.0f; + ny = 0.0f; + nz = 0.0f; + } + + const float overlapF = rsumf - distf; + const float stepF = (float)radB - 0.5f * overlapF; + const float CPXf = fmaf(stepF, nx, xB); + const float CPYf = fmaf(stepF, ny, yB); + const float CPZf = fmaf(stepF, nz, zB); + + const float invF = (float)invBinSize; + const float bxF = CPXf * invF; + const float byF = CPYf * invF; + const float bzF = CPZf * invF; + if (!(near_edge(bxF) || near_edge(byF) || near_edge(bzF))) { + const deme::binID_t ix = (deme::binID_t)bxF; + const deme::binID_t iy = (deme::binID_t)byF; + const deme::binID_t iz = (deme::binID_t)bzF; + binID = ix + iy * nbX + iz * nbX * nbY; + overlapDepth = (double)overlapF; + return deme::SPHERE_SPHERE_CONTACT; + } + + const double centerDist2 = distSquared(XA, YA, ZA, XB, YB, ZB); + overlapDepth = radA + radB - sqrt(centerDist2); + const double CPXd = XB + (radB - overlapDepth * 0.5) * nx; + const double CPYd = YB + (radB - overlapDepth * 0.5) * ny; + const double CPZd = ZB + (radB - overlapDepth * 0.5) * nz; + const deme::binID_t ix = (deme::binID_t)(CPXd * invBinSize); + const deme::binID_t iy = (deme::binID_t)(CPYd * invBinSize); + const deme::binID_t iz = (deme::binID_t)(CPZd * invBinSize); + binID = ix + iy * nbX + iz * nbX * nbY; + return deme::SPHERE_SPHERE_CONTACT; +} + // This utility function returns the normal to the triangular face defined by // the vertices A, B, and C. The face is assumed to be non-degenerate. // Note that order of vertices is important! template -inline __host__ __device__ T1 face_normal(const T1& A, const T1& B, const T1& C) { +__host__ __device__ T1 face_normal(const T1& A, const T1& B, const T1& C) { return normalize(cross(B - A, C - A)); } // Binary search on GPU, which is probably quite divergent... use if absolutely needs to // T2 must be a singed type template -inline __host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin, T2 imax, T2& res) { +__host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin, T2 imax, T2& res) { while (imax >= imin) { T2 imid = imin + (imax - imin) / 2; if (val == A[imid]) { @@ -373,7 +426,7 @@ inline __host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin return false; } template -inline __host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin, T2 imax) { +__host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin, T2 imax) { while (imax >= imin) { T2 imid = imin + (imax - imin) / 2; if (val == A[imid]) { @@ -396,7 +449,7 @@ inline __host__ __device__ bool cuda_binary_search(T1* A, const T1& val, T2 imin * */ template -inline __host__ __device__ float3 +__host__ __device__ float3 vectorAB(const T1& AX, const T1& AY, const T1& AZ, const T1& BX, const T1& BY, const T1& BZ) { return make_float3(AX - BX, AY - BY, AZ - BZ); } @@ -410,16 +463,16 @@ vectorAB(const T1& AX, const T1& AY, const T1& AZ, const T1& BX, const T1& BY, c * */ template -inline __host__ __device__ float3 findLocalCoord(const T1& X, - const T1& Y, - const T1& Z, - const T1& Ox, - const T1& Oy, - const T1& Oz, - const deme::oriQ_t& oriQw, - const deme::oriQ_t& oriQx, - const deme::oriQ_t& oriQy, - const deme::oriQ_t& oriQz) { +__host__ __device__ float3 findLocalCoord(const T1& X, + const T1& Y, + const T1& Z, + const T1& Ox, + const T1& Oy, + const T1& Oz, + const deme::oriQ_t& oriQw, + const deme::oriQ_t& oriQx, + const deme::oriQ_t& oriQy, + const deme::oriQ_t& oriQz) { float locX, locY, locZ; locX = X - Ox; locY = Y - Oy; @@ -431,12 +484,12 @@ inline __host__ __device__ float3 findLocalCoord(const T1& X, /// Calculate the contact params based on the 2 contact material types given template -inline __host__ __device__ void matProxy2ContactParam(T1& E_eff, - T1& G_eff, - const T1& Y1, - const T1& nu1, - const T1& Y2, - const T1& nu2) { +__host__ __device__ void matProxy2ContactParam(T1& E_eff, + T1& G_eff, + const T1& Y1, + const T1& nu1, + const T1& Y2, + const T1& nu2) { T1 invE = ((T1)1 - nu1 * nu1) / Y1 + ((T1)1 - nu2 * nu2) / Y2; E_eff = (T1)1 / invE; T1 invG = (T1)2 * ((T1)2 - nu1) * ((T1)1 + nu1) / Y1 + (T1)2 * ((T1)2 - nu2) * ((T1)1 + nu2) / Y2; @@ -445,171 +498,47 @@ inline __host__ __device__ void matProxy2ContactParam(T1& E_eff, /// Calculate the contact params based on the 2 contact material types given (no-tangent version) template -inline __host__ __device__ void matProxy2ContactParam(T1& E_eff, - const T1& Y1, - const T1& nu1, - const T1& Y2, - const T1& nu2) { +__host__ __device__ void matProxy2ContactParam(T1& E_eff, const T1& Y1, const T1& nu1, const T1& Y2, const T1& nu2) { T1 invE = ((T1)1 - nu1 * nu1) / Y1 + ((T1)1 - nu2 * nu2) / Y2; E_eff = (T1)1 / invE; } -inline __host__ __device__ float3 anyPerpendicularUnitVector(const float3& axis) { - float3 reference = (fabsf(axis.x) < 0.5f) ? make_float3(1, 0, 0) : make_float3(0, 1, 0); - float3 perpendicular = cross(axis, reference); - if (length(perpendicular) < DEME_TINY_FLOAT) { - reference = make_float3(0, 0, 1); - perpendicular = cross(axis, reference); - } - return normalize(perpendicular); +// Singed distance from a point to a plane defined by a point and a normal vector +template +__host__ __device__ T1 planeSignedDistance(const T2& point, const T2& planePoint, const float3& planeNormal) { + // The distance is positive if the point is above the plane, and negative if below + // The distance is 0 if the point is on the plane + return (T1)(dot(point - planePoint, planeNormal)); } -// Check whether a sphere and an analytical boundary are in contact, and gives overlap depth, contact point and contact -// normal. -template -inline __host__ __device__ deme::contact_t checkSphereEntityOverlap(const T1& A, - const T2& radA, - const deme::objType_t& typeB, - const T1& B, - const float3& dirB, - const float& size1B, - const float& size2B, - const float& size3B, - const float& normal_sign, - const float& beta4Entity, - T1& CP, - float3& cntNormal, - T3& overlapDepth) { - deme::contact_t contactType; - switch (typeB) { - case (deme::ANAL_OBJ_TYPE_PLANE): { - const T1 plane2sph = A - B; - // Plane is directional, and the direction is given by plane rotation - const T3 dist = dot(plane2sph, dirB); - overlapDepth = (radA + beta4Entity - dist); - if (overlapDepth < 0.0) { - contactType = deme::NOT_A_CONTACT; - } else { - contactType = deme::SPHERE_PLANE_CONTACT; - } - // From sphere center, go along negative plane normal for (dist + overlapDepth / 2) - CP = A - to_real3(dirB * (dist + overlapDepth / 2.0)); - // Contact normal (B to A) is the same as plane normal - cntNormal = dirB; - return contactType; - } - case (deme::ANAL_OBJ_TYPE_PLATE): { - return deme::NOT_A_CONTACT; - } - case (deme::ANAL_OBJ_TYPE_CYL_INF): { - T1 sph2cyl = B - A; - // Projection along cyl axis direction - const T3 proj_dist = dot(sph2cyl, dirB); - // Radial vector from cylinder center to sphere center, along inward direction - sph2cyl -= proj_dist * dirB; - const T3 dist_delta_r = length(sph2cyl); - // We can think the existence of beta4Entity simply enlarges or shrinks the rad of cyl - float cyl_rad = size1B - normal_sign * beta4Entity; - overlapDepth = radA - normal_sign * (cyl_rad - dist_delta_r); - if (overlapDepth < (T3)0.0) { - contactType = deme::NOT_A_CONTACT; - } else { - contactType = deme::SPHERE_CYL_CONTACT; - } - if (dist_delta_r >= (T3)DEME_TINY_FLOAT) { - cntNormal = to_real3(normal_sign / dist_delta_r * sph2cyl); - CP = A - to_real3(cntNormal * (radA - overlapDepth / 2.0)); - } else { - cntNormal = dirB; - CP = A; - } - return contactType; - } - case (deme::ANAL_OBJ_TYPE_CONE_INF): - case (deme::ANAL_OBJ_TYPE_CONE): { - const float cone_slope = size1B; - const T3 min_h = (T3)size2B; - const T3 max_h = (T3)size3B; - const T1 tip2sph = A - B; - const T1 cone_axis = to_real3(dirB); - const T3 axial_dist = dot(tip2sph, cone_axis); - const T1 radial_vec = tip2sph - cone_axis * axial_dist; - const T3 radial_dist = length(radial_vec); - const float3 radial_dir = (radial_dist >= (T3)DEME_TINY_FLOAT) - ? to_real3(radial_vec / radial_dist) - : anyPerpendicularUnitVector(dirB); - - // The exact closest point on the conical side is found by projecting the sphere center onto the 2-D - // generator line (h, rho) = (h, slope * h). Clamping that axial coordinate gives the apex/rim feature for - // bounded cone segments without introducing mesh facets. - const T3 inv_slope_metric = (T3)1.0 / ((T3)1.0 + (T3)cone_slope * (T3)cone_slope); - T3 closest_h = (axial_dist + (T3)cone_slope * radial_dist) * inv_slope_metric; - bool closest_is_edge = false; - if (closest_h < min_h) { - closest_h = min_h; - closest_is_edge = true; - } else if (closest_h > max_h) { - closest_h = max_h; - closest_is_edge = true; - } - - if (closest_is_edge) { - const T1 closest = - B + cone_axis * closest_h + to_real3(radial_dir) * ((T3)cone_slope * closest_h); - const T1 feature2sph = A - closest; - const T3 dist_to_feature = length(feature2sph); - overlapDepth = (T3)radA + (T3)beta4Entity - dist_to_feature; - contactType = (overlapDepth < (T3)0.0) ? deme::NOT_A_CONTACT : deme::SPHERE_CONE_CONTACT; - - if (dist_to_feature >= (T3)DEME_TINY_FLOAT) { - cntNormal = to_real3(feature2sph / dist_to_feature); - } else { - const T3 side_normal_len = sqrt((T3)1.0 + (T3)cone_slope * (T3)cone_slope); - cntNormal = normal_sign * (cone_slope * dirB - radial_dir) / side_normal_len; - } - CP = A - to_real3(cntNormal * ((T3)radA - overlapDepth / (T3)2.0)); - return contactType; - } - - // Directional cone contact mirrors the cylinder convention. With inward normals, particles are expected - // where radial_dist <= slope * axial_dist; with outward normals, they are expected outside that side. - const T3 side_normal_len = sqrt((T3)1.0 + (T3)cone_slope * (T3)cone_slope); - const T3 signed_gap = (T3)normal_sign * ((T3)cone_slope * axial_dist - radial_dist) / side_normal_len; - overlapDepth = (T3)radA + (T3)beta4Entity - signed_gap; - contactType = (overlapDepth < (T3)0.0) ? deme::NOT_A_CONTACT : deme::SPHERE_CONE_CONTACT; - - cntNormal = normal_sign * (cone_slope * dirB - radial_dir) / side_normal_len; - CP = A - to_real3(cntNormal * ((T3)radA - overlapDepth / (T3)2.0)); - return contactType; - } - default: - return deme::NOT_A_CONTACT; - } +// Radial distance vector from cylinder axis to a point +template +__host__ __device__ T1 cylRadialDistanceVec(const T1& point, const T1& cylPoint, const float3& cylDir) { + // Projection of the point onto the cylinder axis + float proj_dist = dot(point - cylPoint, cylDir); + // Radial vector from cylinder center to point + T1 radial_vec = point - (cylPoint + to_real3(proj_dist * cylDir)); + return radial_vec; } -//////////////////////////////////////////////////////////////////////////////// -// Triangle-specific helper kernels -//////////////////////////////////////////////////////////////////////////////// - /// Takes in a triangle ID and figures out an SD AABB for broadphase use -__inline__ __device__ void boundingBoxIntersectBin(deme::binID_t* L, - deme::binID_t* U, - const float3& vA, - const float3& vB, - const float3& vC, - deme::DEMSimParams* simParams) { +inline __device__ void boundingBoxIntersectBin(deme::binID_t* L, + deme::binID_t* U, + const float3& vA, + const float3& vB, + const float3& vC, + deme::DEMSimParams* simParams) { float3 min_pt; min_pt.x = DEME_MIN(vA.x, DEME_MIN(vB.x, vC.x)); min_pt.y = DEME_MIN(vA.y, DEME_MIN(vB.y, vC.y)); min_pt.z = DEME_MIN(vA.z, DEME_MIN(vB.z, vC.z)); // Enlarge bounding box, so that no triangle lies right between 2 layers of bins - min_pt -= DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; - // A point on a mesh can be out of the simulation world. In this case, becasue we only need to detect their contact - // with spheres, and spheres are all in the simulation world, so we just clamp out the bins that are outside the - // simulation world. + min_pt -= (float)DEME_BIN_ENLARGE_RATIO_FOR_FACETS * (float)simParams->dyn.binSize; + // A point on a mesh can be out of the simulation world. In this case, becasue we only need to detect contacts + // inside the simulation world, so we just clamp out the bins that are outside the simulation world. int3 min_bin = - clampBetween3Comp(min_pt / simParams->binSize, make_int3(0, 0, 0), + clampBetween3Comp(min_pt * (float)simParams->dyn.inv_binSize, make_int3(0, 0, 0), make_int3(simParams->nbX - 1, simParams->nbY - 1, simParams->nbZ - 1)); float3 max_pt; @@ -617,9 +546,9 @@ __inline__ __device__ void boundingBoxIntersectBin(deme::binID_t* L, max_pt.y = DEME_MAX(vA.y, DEME_MAX(vB.y, vC.y)); max_pt.z = DEME_MAX(vA.z, DEME_MAX(vB.z, vC.z)); - max_pt += DEME_BIN_ENLARGE_RATIO_FOR_FACETS * simParams->binSize; + max_pt += (float)DEME_BIN_ENLARGE_RATIO_FOR_FACETS * (float)simParams->dyn.binSize; int3 max_bin = - clampBetween3Comp(max_pt / simParams->binSize, make_int3(0, 0, 0), + clampBetween3Comp(max_pt * (float)simParams->dyn.inv_binSize, make_int3(0, 0, 0), make_int3(simParams->nbX - 1, simParams->nbY - 1, simParams->nbZ - 1)); L[0] = min_bin.x; @@ -631,4 +560,36 @@ __inline__ __device__ void boundingBoxIntersectBin(deme::binID_t* L, U[2] = max_bin.z; } +// Helper function to convert double to float3 for storage +// Note: This conversion depends on platform endianness. On little-endian systems (standard for x86/x64 and NVIDIA +// GPUs), the least significant bytes of the double are stored in f[0] and most significant in f[1]. This is consistent +// across the same architecture, ensuring proper round-trip conversion when reading back the data in follow-up kernels. +// This assumes sizeof(double) == 2 * sizeof(float), which is verified by static_assert. +inline __device__ float3 doubleToFloat3Storage(double value) { + static_assert( + sizeof(double) == 2 * sizeof(float), + "Double must be exactly twice the size of float on this system for this conversion to work correctly."); + union { + double d; + float f[2]; + } converter; + converter.d = value; + return make_float3(converter.f[0], converter.f[1], 0.0f); +} + +// Helper function to extract double from float3 storage +// This is the reverse operation of doubleToFloat3Storage +inline __device__ double float3StorageToDouble(const float3& storage) { + static_assert( + sizeof(double) == 2 * sizeof(float), + "Double must be exactly twice the size of float on this system for this conversion to work correctly."); + union { + double d; + float f[2]; + } converter; + converter.f[0] = storage.x; + converter.f[1] = storage.y; + return converter.d; +} + #endif diff --git a/src/kernel/DEMHistoryMappingKernels.cu b/src/kernel/DEMHistoryMappingKernels.cu deleted file mode 100644 index 32f72602..00000000 --- a/src/kernel/DEMHistoryMappingKernels.cu +++ /dev/null @@ -1,158 +0,0 @@ -// DEM history mapping related custom kernels -#include -#include -_kernelIncludes_; - -__global__ void fillRunLengthArray(deme::geoSphereTouches_t* runlength_full, - deme::bodyID_t* unique_ids, - deme::geoSphereTouches_t* runlength, - size_t numUnique) { - deme::bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < numUnique) { - deme::bodyID_t i = unique_ids[myID]; - runlength_full[i] = runlength[myID]; - } -} - -__global__ void buildPersistentMap(deme::geoSphereTouches_t* new_idA_runlength_full, - deme::geoSphereTouches_t* old_idA_runlength_full, - deme::contactPairs_t* new_idA_scanned_runlength, - deme::contactPairs_t* old_idA_scanned_runlength, - deme::contactPairs_t* mapping, - deme::DEMDataKT* granData, - size_t nSpheresSafe) { - deme::bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nSpheresSafe) { - deme::geoSphereTouches_t new_cnt_count = new_idA_runlength_full[myID]; - deme::geoSphereTouches_t old_cnt_count = old_idA_runlength_full[myID]; - // If this idA has non-zero runlength in new: a potential enduring sphere - if (new_cnt_count > 0) { - // Where should I start looking? Grab the offset. - deme::contactPairs_t new_cnt_offset = new_idA_scanned_runlength[myID]; - deme::contactPairs_t old_cnt_offset = old_idA_scanned_runlength[myID]; - for (deme::geoSphereTouches_t i = 0; i < new_cnt_count; i++) { - // Current contact number we are inspecting - deme::contactPairs_t this_contact = new_cnt_offset + i; - deme::bodyID_t new_idB = granData->idGeometryB[this_contact]; - deme::contact_t new_cntType = granData->contactType[this_contact]; - // Mark it as no matching pair found, being a new contact; modify it later - deme::contactPairs_t my_partner = deme::NULL_MAPPING_PARTNER; - // If this is a fake contact, we can move on - if (new_cntType == deme::NOT_A_CONTACT) { - mapping[this_contact] = my_partner; - continue; - } - // Loop through the old idB to see if there is a match - for (deme::geoSphereTouches_t j = 0; j < old_cnt_count; j++) { - deme::bodyID_t old_idB = granData->previous_idGeometryB[old_cnt_offset + j]; - deme::contact_t old_cntType = granData->previous_contactType[old_cnt_offset + j]; - // If both idB and contact type match, then it is an enduring contact, write it to the mapping - // array - if (new_idB == old_idB && new_cntType == old_cntType) { - my_partner = old_cnt_offset + j; - break; - } - } - // If old_cnt_count == 0, it is automatically NULL_MAPPING_PARTNER - mapping[this_contact] = my_partner; - } - } - } -} - -__global__ void lineNumbers(deme::contactPairs_t* arr, size_t n) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - arr[myID] = myID; - } -} - -__global__ void convertToAndFrom(deme::contactPairs_t* old_arr_unsort_to_sort_map, - deme::contactPairs_t* converted_map, - size_t n) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::contactPairs_t map_from = old_arr_unsort_to_sort_map[myID]; - converted_map[map_from] = myID; - } -} - -__global__ void rearrangeMapping(deme::contactPairs_t* map_sorted, - deme::contactPairs_t* old_arr_unsort_to_sort_map, - size_t n) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::contactPairs_t map_to = map_sorted[myID]; - if (map_to != deme::NULL_MAPPING_PARTNER) - map_sorted[myID] = old_arr_unsort_to_sort_map[map_to]; - } -} - -__global__ void markBoolIf(deme::notStupidBool_t* bool_arr, - deme::notStupidBool_t* value_arr, - deme::notStupidBool_t val, - size_t n) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::notStupidBool_t my_val = value_arr[myID]; - if (my_val == val) { - bool_arr[myID] = 1; - } else { - bool_arr[myID] = 0; - } - } -} - -__global__ void setArr(deme::notStupidBool_t* arr, size_t n, deme::notStupidBool_t val) { - deme::contactPairs_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - arr[myID] = val; - } -} - -__global__ void markDuplicateContacts(deme::geoSphereTouches_t* idA_runlength, - deme::contactPairs_t* idA_scanned_runlength, - deme::bodyID_t* idB, - deme::contact_t* contactType, - deme::notStupidBool_t* persistency, - deme::notStupidBool_t* retain_list, - size_t n) { - deme::bodyID_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::geoSphereTouches_t cnt_count = idA_runlength[myID]; - // If this idA has non-zero runlength in new: a potential removal needed - if (cnt_count > 0) { - // Where should I start looking? Grab the offset. - deme::contactPairs_t cnt_offset = idA_scanned_runlength[myID]; - for (deme::geoSphereTouches_t i = 0; i < cnt_count - 1; i++) { - for (deme::geoSphereTouches_t j = i + 1; j < cnt_count; j++) { - // Current contact numbers we are inspecting - deme::contactPairs_t contactA = cnt_offset + i; - deme::contactPairs_t contactB = cnt_offset + j; - deme::bodyID_t contactA_idB = idB[contactA]; - deme::contact_t contactA_cntType = contactType[contactA]; - deme::notStupidBool_t contactA_persistency = persistency[contactA]; - deme::bodyID_t contactB_idB = idB[contactB]; - deme::contact_t contactB_cntType = contactType[contactB]; - deme::notStupidBool_t contactB_persistency = persistency[contactB]; - - if (contactA_idB == contactB_idB && contactA_cntType == contactB_cntType) { - // If so, they are the same contact, we have to remove one - if (contactA_persistency && contactB_persistency) { - // This is a weird situation: Both are persistent, it should not exist. But still, we remove - // the first one - retain_list[contactA] = 0; - } else { - // If one is persistent, we remove the non-persistent one - if (contactA_persistency == deme::CONTACT_NOT_PERSISTENT) { - retain_list[contactA] = 0; - } else { - retain_list[contactB] = 0; - } - } - } - } - } - } - } -} diff --git a/src/kernel/DEMIntegrationKernels.cu b/src/kernel/DEMIntegrationKernels.cu index 9d9b6c27..ff2d38ed 100644 --- a/src/kernel/DEMIntegrationKernels.cu +++ b/src/kernel/DEMIntegrationKernels.cu @@ -140,7 +140,6 @@ inline __device__ void integrateVelPos(deme::bodyID_t ownerID, } // Operation phase... - { // User's addition of accelerations won't affect acc arrays in global memory; that is, if the user query the // contact acceleration, still they don't get the part they applied in this acc prescription @@ -253,12 +252,15 @@ inline __device__ void integrateVelPos(deme::bodyID_t ownerID, // IDPacker(voxel, voxelX, voxelY, voxelZ, _nvXp2_, _nvYp2_); // } -__global__ void integrateOwners(deme::DEMSimParams* simParams, deme::DEMDataDT* granData) { +DEME_KERNEL void integrateOwners(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, double timeElapsed) { deme::bodyID_t ownerID = blockIdx.x * blockDim.x + threadIdx.x; if (ownerID < simParams->nOwnerBodies) { // These 2 quantities mean the velocity and ang vel used for updating position/quaternion for this step. // Depending on the integration scheme in use, they can be different. float3 v, omgBar; - integrateVelPos(ownerID, simParams, granData, v, omgBar, simParams->h, simParams->timeElapsed); + integrateVelPos(ownerID, simParams, granData, v, omgBar, simParams->dyn.h, (float)timeElapsed); + } + if (blockIdx.x == 0 && threadIdx.x == 0) { + simParams->dyn.timeElapsed = timeElapsed + (double)simParams->dyn.h; } } diff --git a/src/kernel/DEMKinematicMisc.cu b/src/kernel/DEMKinematicMisc.cu new file mode 100644 index 00000000..e6d95fef --- /dev/null +++ b/src/kernel/DEMKinematicMisc.cu @@ -0,0 +1,121 @@ +// DEM kernels that does things such as modifying the system as per user instruction +#include +#include +#include +_kernelIncludes_; + +// Definitions of analytical entites and clump components are below +_clumpTemplateDefs_; +_analyticalEntityDefs_; + +__device__ __forceinline__ float getApproxAbsVel(deme::DEMSimParams* simParams, + const deme::bodyID_t& ownerID, + const float* absVel_owner, + const float* absAngVel_owner, + const float3& myRelPos) { + float abs_v = absVel_owner[ownerID]; + float abs_angv = absAngVel_owner[ownerID]; + if (!isfinite(abs_v) || !isfinite(abs_angv)) { + // May produce messy error messages, but it's still good to know what entities went wrong + DEME_ABORT_KERNEL( + "Absolute velocity or angular velocity for ownerID %llu is infinite (it's worse than " + "max-velocity-exceeded-allowance).\n", + static_cast(ownerID)); + } + // Compute this primitive's velocity estimate. Angular contribution is optional because it is unnecessary for + // pure single-sphere systems, but important for multi-sphere clumps and meshes. + float vel = abs_v; + if (simParams->useAngVelMargin) { + vel += length(myRelPos) * abs_angv; + } + + vel = fminf(vel, simParams->dyn.approxMaxVel); + return vel; +} + +DEME_KERNEL void computeMarginFromAbsv_implSph(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + const float* absVel_owner, + const float* absAngVel_owner, + float* ts, + unsigned int* maxDrift, + size_t n) { + size_t sphereID = blockIdx.x * blockDim.x + threadIdx.x; + if (sphereID < n) { + float3 myRelPos; + float myRadius; // Not actually used + // Get my component offset info from either jitified arrays or global memory + // Outputs myRelPos, myRadius + // Use an input named exactly `sphereID' which is the id of this sphere component + { _componentAcqStrat_; } + + deme::bodyID_t ownerID = granData->ownerClumpBody[sphereID]; + float vel = getApproxAbsVel(simParams, ownerID, absVel_owner, absAngVel_owner, myRelPos); + unsigned int my_family = granData->familyID[ownerID]; + granData->marginSizeSphere[sphereID] = + (double)(vel * simParams->dyn.expSafetyMulti + simParams->dyn.expSafetyAdder) * (*ts) * (*maxDrift) + + granData->familyExtraMarginSize[my_family]; + } +} + +DEME_KERNEL void computeMarginFromAbsv_implTri(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + const float* absVel_owner, + const float* absAngVel_owner, + float* ts, + unsigned int* maxDrift, + float* maxTriTriPenetration, + bool meshUniversalContact, + size_t n) { + size_t triID = blockIdx.x * blockDim.x + threadIdx.x; + if (triID < n) { + float3 triBNode1 = granData->relPosNode1[triID]; + float3 triBNode2 = granData->relPosNode2[triID]; + float3 triBNode3 = granData->relPosNode3[triID]; + float3 myRelPos = triangleCentroid(triBNode1, triBNode2, triBNode3); + + deme::bodyID_t ownerID = granData->ownerTriMesh[triID]; + float vel = getApproxAbsVel(simParams, ownerID, absVel_owner, absAngVel_owner, myRelPos); + unsigned int my_family = granData->familyID[ownerID]; + + // Compute additional margin based on max tri-tri penetration if meshUniversalContact is enabled. This is needed + // as our meshed particle representation is surface only, so we need to account for existing penetration length + // in our future-proof contact detection, always. + double penetrationMargin = maxTriTriPenetration[triID]; + penetrationMargin = (meshUniversalContact && penetrationMargin > 0.0) ? penetrationMargin : 0.0; + // Clamp penetration margin to the maximum allowed value to prevent super large margins + if (penetrationMargin > simParams->capTriTriPenetration) { + penetrationMargin = simParams->capTriTriPenetration; + } + // We hope that penetrationMargin is small, so it's absorbed into the velocity-induce margin. + // But if not, it should prevail to avoid losing contacts involving triangles inside another mesh. + double finalMargin = + (double)(vel * simParams->dyn.expSafetyMulti + simParams->dyn.expSafetyAdder) * (*ts) * (*maxDrift) + + granData->familyExtraMarginSize[my_family]; + if (!simParams->meshParticlesLowPoly && finalMargin < penetrationMargin) { + finalMargin = penetrationMargin; + } + + granData->marginSizeTriangle[triID] = finalMargin; + } +} + +DEME_KERNEL void computeMarginFromAbsv_implAnal(deme::DEMSimParams* simParams, + deme::DEMDataKT* granData, + const float* absVel_owner, + const float* absAngVel_owner, + float* ts, + unsigned int* maxDrift, + size_t n) { + size_t objID = blockIdx.x * blockDim.x + threadIdx.x; + if (objID < n) { + deme::bodyID_t ownerID = granData->ownerAnalBody[objID]; + float3 myRelPos = make_float3(objRelPosX[objID], objRelPosY[objID], objRelPosZ[objID]); + + float vel = getApproxAbsVel(simParams, ownerID, absVel_owner, absAngVel_owner, myRelPos); + unsigned int my_family = granData->familyID[ownerID]; + granData->marginSizeAnalytical[objID] = + (double)(vel * simParams->dyn.expSafetyMulti + simParams->dyn.expSafetyAdder) * (*ts) * (*maxDrift) + + granData->familyExtraMarginSize[my_family]; + } +} diff --git a/src/kernel/DEMMiscKernels.cu b/src/kernel/DEMMiscKernels.cu deleted file mode 100644 index 918dc103..00000000 --- a/src/kernel/DEMMiscKernels.cu +++ /dev/null @@ -1,69 +0,0 @@ -// DEM misc. kernels -#include -#include - -__global__ void markOwnerToChange(deme::notStupidBool_t* idBool, - float* ownerFactors, - deme::bodyID_t* dIDs, - float* dFactors, - size_t n) { - size_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < n) { - deme::bodyID_t myOwner = dIDs[myID]; - float myFactor = dFactors[myID]; - idBool[myOwner] = 1; - ownerFactors[myOwner] = myFactor; - } -} - -template -__global__ void modifyComponents(DEMData* granData, deme::notStupidBool_t* idBool, float* factors, size_t n) { - size_t sphereID = blockIdx.x * blockDim.x + threadIdx.x; - if (sphereID < n) { - // Get my owner ID - deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; - // If not marked, we have nothing to do - if (idBool[myOwner]) { - float factor = factors[myOwner]; - // Expand radius and relPos - granData->relPosSphereX[sphereID] *= factor; - granData->relPosSphereY[sphereID] *= factor; - granData->relPosSphereZ[sphereID] *= factor; - granData->radiiSphere[sphereID] *= factor; - } - } -} - -__global__ void computeMarginFromAbsv(deme::DEMSimParams* simParams, - deme::DEMDataKT* granData, - float* ts, - unsigned int* maxDrift, - size_t n) { - size_t ownerID = blockIdx.x * blockDim.x + threadIdx.x; - if (ownerID < n) { - float absv = granData->marginSize[ownerID]; - unsigned int my_family = granData->familyID[ownerID]; - if (!isfinite(absv)) { - // May produce messy error messages, but it's still good to know what entities went wrong - DEME_ABORT_KERNEL("Absolute velocity for ownerID %llu is not finite. This happened at time %.9g.\n", - static_cast(ownerID), simParams->timeElapsed); - } - if (absv > simParams->approxMaxVel) { - absv = simParams->approxMaxVel; - } - // User-specified extra margin also needs to be added here. This marginSize is used for bin--sph or bin--tri - // contacts but not entirely the same as the one used for sph--sph or sph--tri contacts, since the latter is - // stricter. - granData->marginSize[ownerID] = - (double)(absv * simParams->expSafetyMulti + simParams->expSafetyAdder) * (*ts) * (*maxDrift) + - granData->familyExtraMarginSize[my_family]; - } -} - -__global__ void fillMarginValues(deme::DEMSimParams* simParams, deme::DEMDataKT* granData, size_t n) { - size_t ownerID = blockIdx.x * blockDim.x + threadIdx.x; - if (ownerID < n) { - unsigned int my_family = granData->familyID[ownerID]; - granData->marginSize[ownerID] = simParams->beta + granData->familyExtraMarginSize[my_family]; - } -} diff --git a/src/kernel/DEMModeratorKernels.cu b/src/kernel/DEMModeratorKernels.cu index 2db3dcc1..df338d43 100644 --- a/src/kernel/DEMModeratorKernels.cu +++ b/src/kernel/DEMModeratorKernels.cu @@ -1,4 +1,4 @@ -// DEM kernels that does some wildcard stuff, such as modifying the system as per user instruction +// DEM kernels that does things such as modifying the system as per user instruction #include #include _kernelIncludes_; @@ -7,7 +7,7 @@ _kernelIncludes_; _massDefs_; _moiDefs_; -__global__ void applyFamilyChanges(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, size_t nOwnerBodies) { +DEME_KERNEL void applyFamilyChanges(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, size_t nOwnerBodies) { deme::bodyID_t myOwner = blockIdx.x * blockDim.x + threadIdx.x; if (myOwner < nOwnerBodies) { // The user may make references to owner positions, velocities, accelerations and simulation time @@ -48,8 +48,8 @@ __global__ void applyFamilyChanges(deme::DEMSimParams* simParams, deme::DEMDataD float accY = acc.y; float accZ = acc.z; - float ts = simParams->h; - float time = simParams->timeElapsed; + float ts = simParams->dyn.h; + float time = simParams->dyn.timeElapsed; // Carry out user's instructions { _familyChangeRules_; } diff --git a/src/kernel/DEMOwnerQueryKernels.cu b/src/kernel/DEMOwnerQueryKernels.cu index 7045c12b..af6a43c9 100644 --- a/src/kernel/DEMOwnerQueryKernels.cu +++ b/src/kernel/DEMOwnerQueryKernels.cu @@ -8,12 +8,12 @@ _massDefs_; _moiDefs_; _volumeDefs_; -__global__ void inspectOwnerProperty(deme::DEMDataDT* granData, - deme::DEMSimParams* simParams, - float* quantity, - deme::notStupidBool_t* not_in_region, - size_t nOwnerBodies, - deme::ownerType_t owner_type) { +DEME_KERNEL void inspectOwnerProperty(deme::DEMDataDT* granData, + deme::DEMSimParams* simParams, + float* quantity, + deme::notStupidBool_t* not_in_region, + size_t nOwnerBodies, + deme::ownerType_t owner_type) { deme::bodyID_t myOwner = blockIdx.x * blockDim.x + threadIdx.x; if (myOwner < nOwnerBodies) { deme::ownerType_t myType = granData->ownerTypes[myOwner]; diff --git a/src/kernel/DEMPrepForceKernels.cu b/src/kernel/DEMPrepForceKernels.cu deleted file mode 100644 index 3db0ebf6..00000000 --- a/src/kernel/DEMPrepForceKernels.cu +++ /dev/null @@ -1,81 +0,0 @@ -// DEM force computation related custom kernels -#include -#include -_kernelIncludes_; - -inline __device__ void cleanUpContactForces(size_t thisContact, - deme::DEMSimParams* simParams, - deme::DEMDataDT* granData) { - const float3 zeros = make_float3(0, 0, 0); - granData->contactForces[thisContact] = zeros; - granData->contactTorque_convToForce[thisContact] = zeros; -} - -inline __device__ void cleanUpAcc(size_t thisClump, deme::DEMSimParams* simParams, deme::DEMDataDT* granData) { - // If should not clear acc arrays, then just mark it to be clear in the next ts - if (granData->accSpecified[thisClump]) { - granData->accSpecified[thisClump] = 0; - } else { - granData->aX[thisClump] = 0; - granData->aY[thisClump] = 0; - granData->aZ[thisClump] = 0; - } - if (granData->angAccSpecified[thisClump]) { - granData->angAccSpecified[thisClump] = 0; - } else { - granData->alphaX[thisClump] = 0; - granData->alphaY[thisClump] = 0; - granData->alphaZ[thisClump] = 0; - } -} - -__global__ void prepareAccArrays(deme::DEMSimParams* simParams, deme::DEMDataDT* granData) { - size_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < simParams->nOwnerBodies) { - cleanUpAcc(myID, simParams, granData); - } -} - -__global__ void prepareForceArrays(deme::DEMSimParams* simParams, deme::DEMDataDT* granData, size_t nContactPairs) { - size_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - cleanUpContactForces(myID, simParams, granData); - } -} - -__global__ void rearrangeContactWildcards(deme::DEMDataDT* granData, - float* newWildcards, - deme::notStupidBool_t* sentry, - unsigned int nWildcards, - size_t nContactPairs) { - size_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - deme::contactPairs_t map_from = granData->contactMapping[myID]; - if (map_from == deme::NULL_MAPPING_PARTNER) { - // If it is a NULL ID then kT says this contact is new. Initialize all wildcard arrays. - for (size_t i = 0; i < nWildcards; i++) { - newWildcards[nContactPairs * i + myID] = 0; - } - } else { - // Not a new contact, need to map it from somewhere in the old history array - for (size_t i = 0; i < nWildcards; i++) { - newWildcards[nContactPairs * i + myID] = granData->contactWildcards[i][map_from]; - } - // This sentry trys to make sure that all `alive' contacts got mapped to some place - sentry[map_from] = 0; - } - } -} - -__global__ void markAliveContacts(float* wildcard, deme::notStupidBool_t* sentry, size_t nContactPairs) { - size_t myID = blockIdx.x * blockDim.x + threadIdx.x; - if (myID < nContactPairs) { - float myEntry = abs(wildcard[myID]); - // If this is alive then mark it - if (myEntry > DEME_TINY_FLOAT) { - sentry[myID] = 1; - } else { - sentry[myID] = 0; - } - } -} diff --git a/src/kernel/DEMSphereQueryKernels.cu b/src/kernel/DEMSphereQueryKernels.cu index fc59b9e8..cf08dbfd 100644 --- a/src/kernel/DEMSphereQueryKernels.cu +++ b/src/kernel/DEMSphereQueryKernels.cu @@ -10,19 +10,18 @@ _clumpTemplateDefs_; // _massDefs_; // _moiDefs_; -__global__ void inspectSphereProperty(deme::DEMDataDT* granData, - deme::DEMSimParams* simParams, - float* quantity, - deme::notStupidBool_t* not_in_region, - size_t nSpheres, - deme::ownerType_t owner_type) { +DEME_KERNEL void inspectSphereProperty(deme::DEMDataDT* granData, + deme::DEMSimParams* simParams, + float* quantity, + deme::notStupidBool_t* not_in_region, + size_t nSpheres, + deme::ownerType_t owner_type) { size_t sphereID = blockIdx.x * blockDim.x + threadIdx.x; if (sphereID < nSpheres) { // Get my owner ID deme::bodyID_t myOwner = granData->ownerClumpBody[sphereID]; float3 myRelPos; float myRadius; - float oriQw, oriQx, oriQy, oriQz; double ownerX, ownerY, ownerZ; // Get my component offset info from either jitified arrays or global memory // Outputs myRelPos, myRadius @@ -32,11 +31,14 @@ __global__ void inspectSphereProperty(deme::DEMDataDT* granData, voxelIDToPosition( ownerX, ownerY, ownerZ, granData->voxelID[myOwner], granData->locX[myOwner], granData->locY[myOwner], granData->locZ[myOwner], _nvXp2_, _nvYp2_, _voxelSize_, _l_); - oriQw = granData->oriQw[myOwner]; - oriQx = granData->oriQx[myOwner]; - oriQy = granData->oriQy[myOwner]; - oriQz = granData->oriQz[myOwner]; - applyOriQToVector3(myRelPos.x, myRelPos.y, myRelPos.z, oriQw, oriQx, oriQy, oriQz); + // Keep these scalar names available for injected inspector snippets. Built-in and user-provided sphere + // query code has historically referenced oriQw/oriQx/oriQy/oriQz directly. + const deme::oriQ_t oriQw = granData->oriQw[myOwner]; + const deme::oriQ_t oriQx = granData->oriQx[myOwner]; + const deme::oriQ_t oriQy = granData->oriQy[myOwner]; + const deme::oriQ_t oriQz = granData->oriQz[myOwner]; + const float4 oriQ = make_float4(oriQx, oriQy, oriQz, oriQw); + applyOriQToVector3(myRelPos, oriQ); // Use sphereXYZ to determine if this sphere is in the region that should be counted // And don't forget adding LBF as an offset diff --git a/src/kernel/DEMTriangleBoxIntersect.cu b/src/kernel/DEMTriangleBoxIntersect.cu index ad929060..80b5913b 100644 --- a/src/kernel/DEMTriangleBoxIntersect.cu +++ b/src/kernel/DEMTriangleBoxIntersect.cu @@ -135,240 +135,250 @@ /* Thanks to David Hunt for finding a ">="-bug! */ /********************************************************/ -#ifndef DEME_TRI_BOX_INTSCT -#define DEME_TRI_BOX_INTSCT +#pragma once +// Fast triangle-box (cube) overlap predicate for the *union* of two triangles: +// - Triangle A: (v0, v1, v2) in box-centered coordinates +// - Triangle B: (v0+t, v1+t, v2+t) where t is a constant translation in the same coordinate system +// +// Key idea (Option C): +// - Compute edges/abs/normal once (from A in box frame). +// - For the 2nd triangle, reuse SAT projections by shifting min/max with the translation-induced delta. +// - This avoids running a full second SAT for the sandwich triangle. +// +// The function supports bounds gating via testA/testB: if one is false it is skipped. -#include -#include +#include -#define DEME_DIR_X 0 -#define DEME_DIR_Y 1 -#define DEME_DIR_Z 2 +__device__ __forceinline__ bool _deme_sep_axis_fp32(float mn, float mx, float rad, float eps) { + // Separating axis exists if interval [mn,mx] is entirely outside [-rad,rad] + return (mn > rad + eps) || (mx < -rad - eps); +} -#define CROSS(dest, v1, v2) \ - dest[0] = v1[1] * v2[2] - v1[2] * v2[1]; \ - dest[1] = v1[2] * v2[0] - v1[0] * v2[2]; \ - dest[2] = v1[0] * v2[1] - v1[1] * v2[0]; +__device__ __forceinline__ bool triBoxOverlapBinLocalEdgesUnionShiftFP32(const float3& v0, + const float3& v1, + const float3& v2, + const float3& t, + float h, + bool testA, + bool testB, + float eps = 0.0f) { + bool okA = testA; + bool okB = testB; -#define DOT(v1, v2) (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]) + if (!okA && !okB) + return false; -#define SUB(dest, v1, v2) \ - dest[0] = v1[0] - v2[0]; \ - dest[1] = v1[1] - v2[1]; \ - dest[2] = v1[2] - v2[2]; + // Edges in box-centered frame (numerically aligned with reference) + const float3 e0 = {v1.x - v0.x, v1.y - v0.y, v1.z - v0.z}; + const float3 e1 = {v2.x - v1.x, v2.y - v1.y, v2.z - v1.z}; + const float3 e2 = {v0.x - v2.x, v0.y - v2.y, v0.z - v2.z}; -#define SUBTRACT(dest, v1, v2) \ - dest[0] = v1.x - v2[0]; \ - dest[1] = v1.y - v2[1]; \ - dest[2] = v1.z - v2[2]; + const float3 ae0 = {fabsf(e0.x), fabsf(e0.y), fabsf(e0.z)}; + const float3 ae1 = {fabsf(e1.x), fabsf(e1.y), fabsf(e1.z)}; + const float3 ae2 = {fabsf(e2.x), fabsf(e2.y), fabsf(e2.z)}; -#define FINDMINMAX(x0, x1, x2, min, max) \ - min = max = x0; \ - if (x1 < min) \ - min = x1; \ - if (x1 > max) \ - max = x1; \ - if (x2 < min) \ - min = x2; \ - if (x2 > max) \ - max = x2; + float p0, p1, p2, mn, mx, rad; -inline __device__ bool planeBoxOverlap(float normal[3], float vert[3], float maxbox[3]) { - int q; - float vmin[3], vmax[3], v; - for (q = DEME_DIR_X; q <= DEME_DIR_Z; q++) { - v = vert[q]; - if (normal[q] > 0.0f) { - vmin[q] = -maxbox[q] - v; - vmax[q] = maxbox[q] - v; - } else { - vmin[q] = maxbox[q] - v; - vmax[q] = -maxbox[q] - v; - } + // ---- 9 cross-axis tests ---- + // Edge e0: X01 + p0 = e0.z * v0.y - e0.y * v0.z; + p2 = e0.z * v2.y - e0.y * v2.z; + mn = fminf(p0, p2); + mx = fmaxf(p0, p2); + rad = h * (ae0.z + ae0.y); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e0.z * t.y - e0.y * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; } - if (DOT(normal, vmin) > 0.0f) + if (!okA && !okB) return false; - if (DOT(normal, vmax) >= 0.0f) - return true; - return false; -} - -/*======================== X-tests ========================*/ -#define AXISTEST_X01(a, b, fa, fb) \ - p0 = a * v0[DEME_DIR_Y] - b * v0[DEME_DIR_Z]; \ - p2 = a * v2[DEME_DIR_Y] - b * v2[DEME_DIR_Z]; \ - if (p0 < p2) { \ - min = p0; \ - max = p2; \ - } else { \ - min = p2; \ - max = p0; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_Y] + fb * boxhalfsize[DEME_DIR_Z]; \ - if (min > rad || max < -rad) \ + // Edge e0: Y02 + p0 = -e0.z * v0.x + e0.x * v0.z; + p2 = -e0.z * v2.x + e0.x * v2.z; + mn = fminf(p0, p2); + mx = fmaxf(p0, p2); + rad = h * (ae0.z + ae0.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = -e0.z * t.x + e0.x * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -#define AXISTEST_X2(a, b, fa, fb) \ - p0 = a * v0[DEME_DIR_Y] - b * v0[DEME_DIR_Z]; \ - p1 = a * v1[DEME_DIR_Y] - b * v1[DEME_DIR_Z]; \ - if (p0 < p1) { \ - min = p0; \ - max = p1; \ - } else { \ - min = p1; \ - max = p0; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_Y] + fb * boxhalfsize[DEME_DIR_Z]; \ - if (min > rad || max < -rad) \ + // Edge e0: Z12 + p1 = e0.y * v1.x - e0.x * v1.y; + p2 = e0.y * v2.x - e0.x * v2.y; + mn = fminf(p1, p2); + mx = fmaxf(p1, p2); + rad = h * (ae0.y + ae0.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e0.y * t.x - e0.x * t.y; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -/*======================== Y-tests ========================*/ -#define AXISTEST_Y02(a, b, fa, fb) \ - p0 = -a * v0[DEME_DIR_X] + b * v0[DEME_DIR_Z]; \ - p2 = -a * v2[DEME_DIR_X] + b * v2[DEME_DIR_Z]; \ - if (p0 < p2) { \ - min = p0; \ - max = p2; \ - } else { \ - min = p2; \ - max = p0; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_X] + fb * boxhalfsize[DEME_DIR_Z]; \ - if (min > rad || max < -rad) \ + // Edge e1: X01 + p0 = e1.z * v0.y - e1.y * v0.z; + p2 = e1.z * v2.y - e1.y * v2.z; + mn = fminf(p0, p2); + mx = fmaxf(p0, p2); + rad = h * (ae1.z + ae1.y); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e1.z * t.y - e1.y * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -#define AXISTEST_Y1(a, b, fa, fb) \ - p0 = -a * v0[DEME_DIR_X] + b * v0[DEME_DIR_Z]; \ - p1 = -a * v1[DEME_DIR_X] + b * v1[DEME_DIR_Z]; \ - if (p0 < p1) { \ - min = p0; \ - max = p1; \ - } else { \ - min = p1; \ - max = p0; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_X] + fb * boxhalfsize[DEME_DIR_Z]; \ - if (min > rad || max < -rad) \ + // Edge e1: Y02 + p0 = -e1.z * v0.x + e1.x * v0.z; + p2 = -e1.z * v2.x + e1.x * v2.z; + mn = fminf(p0, p2); + mx = fmaxf(p0, p2); + rad = h * (ae1.z + ae1.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = -e1.z * t.x + e1.x * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -/*======================== Z-tests ========================*/ - -#define AXISTEST_Z12(a, b, fa, fb) \ - p1 = a * v1[DEME_DIR_X] - b * v1[DEME_DIR_Y]; \ - p2 = a * v2[DEME_DIR_X] - b * v2[DEME_DIR_Y]; \ - if (p2 < p1) { \ - min = p2; \ - max = p1; \ - } else { \ - min = p1; \ - max = p2; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_X] + fb * boxhalfsize[DEME_DIR_Y]; \ - if (min > rad || max < -rad) \ + // Edge e1: Z0 (original uses v0 and v1) + p0 = e1.y * v0.x - e1.x * v0.y; + p1 = e1.y * v1.x - e1.x * v1.y; + mn = fminf(p0, p1); + mx = fmaxf(p0, p1); + rad = h * (ae1.y + ae1.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e1.y * t.x - e1.x * t.y; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -#define AXISTEST_Z0(a, b, fa, fb) \ - p0 = a * v0[DEME_DIR_X] - b * v0[DEME_DIR_Y]; \ - p1 = a * v1[DEME_DIR_X] - b * v1[DEME_DIR_Y]; \ - if (p0 < p1) { \ - min = p0; \ - max = p1; \ - } else { \ - min = p1; \ - max = p0; \ - } \ - rad = fa * boxhalfsize[DEME_DIR_X] + fb * boxhalfsize[DEME_DIR_Y]; \ - if (min > rad || max < -rad) \ + // Edge e2: X2 (original uses v0 and v1) + p0 = e2.z * v0.y - e2.y * v0.z; + p1 = e2.z * v1.y - e2.y * v1.z; + mn = fminf(p0, p1); + mx = fmaxf(p0, p1); + rad = h * (ae2.z + ae2.y); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e2.z * t.y - e2.y * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; -/** -* Figure out whether a triangle touches an SD. Function gets hit a lot, very desirable to be fast. -Input: -- boxcenter: defines center of the box -- boxhalfsize: half the size of the box in an axis aligned context -- vA, vB, vC: the three vertices of the triangle -Output: -- "true" if there is overlap; "false" otherwise -NOTE: This function works with "float" - precision is not paramount. -*/ -inline __device__ bool check_TriangleBoxOverlap(float boxcenter[3], - float boxhalfsize[3], - const float3& vA, - const float3& vB, - const float3& vC) { - /** Use the separating axis theorem to test overlap between triangle and box. - We test for overlap in these directions: - 1) the {x,y,z}-directions (actually, since we use the AABB of the triangle we do not even need to test these) - 2) normal of the triangle - 3) crossproduct(edge from tri, {x,y,z}-directin) this gives 3x3=9 more tests */ - float v0[3], v1[3], v2[3]; - float min, max, p0, p1, p2, rad, fex, fey, fez; - float normal[3], e0[3], e1[3], e2[3]; - - /* This is the fastest branch on Sun */ - /* move everything so that the boxcenter is in (0,0,0) */ - SUBTRACT(v0, vA, boxcenter); - SUBTRACT(v1, vB, boxcenter); - SUBTRACT(v2, vC, boxcenter); - - /* compute triangle edges */ - SUB(e0, v1, v0); /* tri edge 0 */ - SUB(e1, v2, v1); /* tri edge 1 */ - SUB(e2, v0, v2); /* tri edge 2 */ - - /* Case 3) */ - /* test the 9 tests first (this was faster) */ - fex = fabsf(e0[DEME_DIR_X]); - fey = fabsf(e0[DEME_DIR_Y]); - fez = fabsf(e0[DEME_DIR_Z]); - AXISTEST_X01(e0[DEME_DIR_Z], e0[DEME_DIR_Y], fez, fey); - AXISTEST_Y02(e0[DEME_DIR_Z], e0[DEME_DIR_X], fez, fex); - AXISTEST_Z12(e0[DEME_DIR_Y], e0[DEME_DIR_X], fey, fex); - - fex = fabsf(e1[DEME_DIR_X]); - fey = fabsf(e1[DEME_DIR_Y]); - fez = fabsf(e1[DEME_DIR_Z]); - AXISTEST_X01(e1[DEME_DIR_Z], e1[DEME_DIR_Y], fez, fey); - AXISTEST_Y02(e1[DEME_DIR_Z], e1[DEME_DIR_X], fez, fex); - AXISTEST_Z0(e1[DEME_DIR_Y], e1[DEME_DIR_X], fey, fex); - - fex = fabsf(e2[DEME_DIR_X]); - fey = fabsf(e2[DEME_DIR_Y]); - fez = fabsf(e2[DEME_DIR_Z]); - AXISTEST_X2(e2[DEME_DIR_Z], e2[DEME_DIR_Y], fez, fey); - AXISTEST_Y1(e2[DEME_DIR_Z], e2[DEME_DIR_X], fez, fex); - AXISTEST_Z12(e2[DEME_DIR_Y], e2[DEME_DIR_X], fey, fex); - - /* Case 1) */ - /* first test overlap in the {x,y,z}-directions */ - /* find min, max of the triangle each direction, and test for overlap in */ - /* that direction -- this is equivalent to testing a minimal AABB around */ - /* the triangle against the AABB */ + // Edge e2: Y1 + p0 = -e2.z * v0.x + e2.x * v0.z; + p1 = -e2.z * v1.x + e2.x * v1.z; + mn = fminf(p0, p1); + mx = fmaxf(p0, p1); + rad = h * (ae2.z + ae2.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = -e2.z * t.x + e2.x * t.z; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) + return false; - /* test in DEME_DIR_X-direction */ - FINDMINMAX(v0[DEME_DIR_X], v1[DEME_DIR_X], v2[DEME_DIR_X], min, max); - if (min > boxhalfsize[DEME_DIR_X] || max < -boxhalfsize[DEME_DIR_X]) + // Edge e2: Z12 (original uses v1 and v2) + p1 = e2.y * v1.x - e2.x * v1.y; + p2 = e2.y * v2.x - e2.x * v2.y; + mn = fminf(p1, p2); + mx = fmaxf(p1, p2); + rad = h * (ae2.y + ae2.x); + if (okA && _deme_sep_axis_fp32(mn, mx, rad, eps)) + okA = false; + if (okB) { + const float d = e2.y * t.x - e2.x * t.y; + if (_deme_sep_axis_fp32(mn + d, mx + d, rad, eps)) + okB = false; + } + if (!okA && !okB) return false; - /* test in DEME_DIR_Y-direction */ - FINDMINMAX(v0[DEME_DIR_Y], v1[DEME_DIR_Y], v2[DEME_DIR_Y], min, max); - if (min > boxhalfsize[DEME_DIR_Y] || max < -boxhalfsize[DEME_DIR_Y]) + // ---- Case 1) overlap in X/Y/Z ---- + float minx = fminf(v0.x, fminf(v1.x, v2.x)); + float maxx = fmaxf(v0.x, fmaxf(v1.x, v2.x)); + if (okA && (minx > h + eps || maxx < -h - eps)) + okA = false; + if (okB) { + const float minxB = minx + t.x; + const float maxxB = maxx + t.x; + if (minxB > h + eps || maxxB < -h - eps) + okB = false; + } + if (!okA && !okB) return false; - /* test in DEME_DIR_Z-direction */ - FINDMINMAX(v0[DEME_DIR_Z], v1[DEME_DIR_Z], v2[DEME_DIR_Z], min, max); - if (min > boxhalfsize[DEME_DIR_Z] || max < -boxhalfsize[DEME_DIR_Z]) + float miny = fminf(v0.y, fminf(v1.y, v2.y)); + float maxy = fmaxf(v0.y, fmaxf(v1.y, v2.y)); + if (okA && (miny > h + eps || maxy < -h - eps)) + okA = false; + if (okB) { + const float minyB = miny + t.y; + const float maxyB = maxy + t.y; + if (minyB > h + eps || maxyB < -h - eps) + okB = false; + } + if (!okA && !okB) return false; - /* Case 2) */ - /* test if the box intersects the plane of the triangle */ - /* compute plane equation of triangle: normal*x+d=0 */ - CROSS(normal, e0, e1); - if (!planeBoxOverlap(normal, v0, boxhalfsize)) + float minz = fminf(v0.z, fminf(v1.z, v2.z)); + float maxz = fmaxf(v0.z, fmaxf(v1.z, v2.z)); + if (okA && (minz > h + eps || maxz < -h - eps)) + okA = false; + if (okB) { + const float minzB = minz + t.z; + const float maxzB = maxz + t.z; + if (minzB > h + eps || maxzB < -h - eps) + okB = false; + } + if (!okA && !okB) return false; - return true; /* box and triangle overlaps */ -} + // ---- Case 2) plane test ---- + // normal = cross(e0, v2-v0) + const float3 v20 = {v2.x - v0.x, v2.y - v0.y, v2.z - v0.z}; + const float3 n = {e0.y * v20.z - e0.z * v20.y, e0.z * v20.x - e0.x * v20.z, e0.x * v20.y - e0.y * v20.x}; + + const float distA = n.x * v0.x + n.y * v0.y + n.z * v0.z; + const float rPlane = h * (fabsf(n.x) + fabsf(n.y) + fabsf(n.z)) + eps; -#endif + if (okA && fabsf(distA) > rPlane) + okA = false; + if (okB) { + const float distB = distA + (n.x * t.x + n.y * t.y + n.z * t.z); + if (fabsf(distB) > rPlane) + okB = false; + } + + return okA || okB; +} diff --git a/src/kernel/DEMUserScripts/ForceModel2D.cu b/src/kernel/DEMUserScripts/ForceModel2D.cu index 7383b883..3212e8c1 100644 --- a/src/kernel/DEMUserScripts/ForceModel2D.cu +++ b/src/kernel/DEMUserScripts/ForceModel2D.cu @@ -23,12 +23,10 @@ if (overlapDepth > 0.0) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, 0.0, delta_tan_z); @@ -53,8 +51,10 @@ if (overlapDepth > 0.0) { // Normal force part { - sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Derive the equivalent contact radius from the actual overlap geometry. This also supports mesh-mesh + // contacts, where the analytical two-sphere effective radius is not representative. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -71,7 +71,7 @@ if (overlapDepth > 0.0) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -104,7 +104,7 @@ if (overlapDepth > 0.0) { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); diff --git a/src/kernel/DEMUserScripts/ForceModelMooring.cu b/src/kernel/DEMUserScripts/ForceModelMooring.cu index b2f6aaee..81d5a304 100644 --- a/src/kernel/DEMUserScripts/ForceModelMooring.cu +++ b/src/kernel/DEMUserScripts/ForceModelMooring.cu @@ -20,10 +20,10 @@ float3 rotVelCPA, rotVelCPB; rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } -float mass_eff, sqrt_Rd, beta; +float mass_eff, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -83,14 +83,12 @@ if (innerInteraction > DEME_TINY_FLOAT) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, - AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, - BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -110,8 +108,9 @@ if (innerInteraction > DEME_TINY_FLOAT) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(temp * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Once the bond is inactive, use the actual overlap geometry for ordinary Hertzian contact. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -128,7 +127,7 @@ if (innerInteraction > DEME_TINY_FLOAT) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -162,7 +161,7 @@ if (innerInteraction > DEME_TINY_FLOAT) { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); @@ -187,4 +186,4 @@ if (innerInteraction > DEME_TINY_FLOAT) { delta_tan_z = delta_tan.z; } } -} \ No newline at end of file +} diff --git a/src/kernel/DEMUserScripts/ForceModelMooringPosition.cu b/src/kernel/DEMUserScripts/ForceModelMooringPosition.cu index 41d5b697..26f92c8b 100644 --- a/src/kernel/DEMUserScripts/ForceModelMooringPosition.cu +++ b/src/kernel/DEMUserScripts/ForceModelMooringPosition.cu @@ -20,10 +20,10 @@ float3 rotVelCPA, rotVelCPB; rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } -float mass_eff, sqrt_Rd, beta; +float mass_eff, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -96,14 +96,12 @@ if (innerInteraction > DEME_TINY_FLOAT) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, - AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, - BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -123,8 +121,9 @@ if (innerInteraction > DEME_TINY_FLOAT) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(temp * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Once the bond is inactive, use the actual overlap geometry for ordinary Hertzian contact. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -141,7 +140,7 @@ if (innerInteraction > DEME_TINY_FLOAT) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -175,7 +174,7 @@ if (innerInteraction > DEME_TINY_FLOAT) { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); @@ -200,4 +199,4 @@ if (innerInteraction > DEME_TINY_FLOAT) { delta_tan_z = delta_tan.z; } } -} \ No newline at end of file +} diff --git a/src/kernel/DEMUserScripts/ForceModelWithCohesion.cu b/src/kernel/DEMUserScripts/ForceModelWithCohesion.cu index 150d5138..d6622d06 100644 --- a/src/kernel/DEMUserScripts/ForceModelWithCohesion.cu +++ b/src/kernel/DEMUserScripts/ForceModelWithCohesion.cu @@ -20,22 +20,16 @@ if (overlapDepth > 0) { Cohesion_coeff = Cohesion[bodyAMatType][bodyBMatType]; } - float3 rotVelCPA, rotVelCPB; - { - // We also need the relative velocity between A and B in global frame to use in the damping terms - // To get that, we need contact points' rotational velocity in GLOBAL frame - // This is local rotational velocity (the portion of linear vel contributed by rotation) - rotVelCPA = cross(ARotVel, locCPA); - rotVelCPB = cross(BRotVel, locCPB); - // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); - } + // We also need the relative velocity between A and B in global frame to use in the damping terms + // To get that, we need contact points' rotational velocity in GLOBAL frame + // This is local rotational velocity (the portion of linear vel contributed by rotation) + float3 rotVelCPA = cross(ARotVel, locCPA); + float3 rotVelCPB = cross(BRotVel, locCPB); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, beta, cnt_rad; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -55,31 +49,34 @@ if (overlapDepth > 0) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Contact radius (radial distance from contact center axis) called cnt_rad, computed from area + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2.f * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); - const float k_n = deme::TWO_OVER_THREE * Sn; - const float gamma_n = deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(Sn * mass_eff); + const float k_n = (2.f / 3.f) * Sn; + const float gamma_n = (2.f * sqrtf(5.f / 6.f)) * beta * sqrtf(Sn * mass_eff); force += (k_n * overlapDepth + gamma_n * projection) * B2A; // printf("normal force: %f, %f, %f\n", force.x, force.y, force.z); } // Rolling resistance part - if (Crr_cnt > 0.0) { + if (Crr_cnt > 0.f) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); - const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); - const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); + // Use geometric equivalence R_eff ~= cnt_rad^2 / overlapDepth + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; + + const float kn_simple = (4.f / 3.f) * E_cnt * sqrtf(R_eff); + const float gn_simple = -2.f * sqrtf((5.f / 3.f) * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); const float d_coeff = gn_simple / (2.f * sqrtf(kn_simple * mass_eff)); - if (d_coeff < 1.0) { + if (d_coeff < 1.f) { float t_collision = deme::PI * sqrtf(mass_eff / (kn_simple * (1.f - d_coeff * d_coeff))); if (delta_time <= t_collision) { should_add_rolling_resistance = false; @@ -104,9 +101,9 @@ if (overlapDepth > 0) { } // Tangential force part - if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; - const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); + if (mu_cnt > 0.f) { + const float kt = 8.f * G_cnt * cnt_rad; + const float gt = -2.f * sqrtf(5.f / 3.f) * beta * sqrtf(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); if (ft > DEME_TINY_FLOAT) { @@ -124,7 +121,7 @@ if (overlapDepth > 0) { // printf("tangent force: %f, %f, %f\n", tangent_force.x, tangent_force.y, tangent_force.z); } - // Make sure we update those wildcards (in this case, contact history) + // Finally, make sure we update those wildcards (in this case, contact history) delta_tan_x = delta_tan.x; delta_tan_y = delta_tan.y; delta_tan_z = delta_tan.z; diff --git a/src/kernel/DEMUserScripts/ForceModelWithElectrostatic.cu b/src/kernel/DEMUserScripts/ForceModelWithElectrostatic.cu index d9ed0e5a..b3871c0a 100644 --- a/src/kernel/DEMUserScripts/ForceModelWithElectrostatic.cu +++ b/src/kernel/DEMUserScripts/ForceModelWithElectrostatic.cu @@ -29,14 +29,12 @@ if (overlapDepth > 0) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -56,8 +54,10 @@ if (overlapDepth > 0) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Derive the equivalent contact radius from the actual overlap geometry so non-spherical contacts use + // their measured overlap area rather than an inaccurate analytical radius. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -74,7 +74,7 @@ if (overlapDepth > 0) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -105,7 +105,7 @@ if (overlapDepth > 0) { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); @@ -144,22 +144,19 @@ if (overlapDepth > 0) { const float k = 8.99e9; const double ABdist2 = dot(bodyAPos - bodyBPos, bodyAPos - bodyBPos); // If Q_A and Q_B are the same sign, then the force pushes A away from B, so B2A is the direction. - force += k * Q_A[AGeo] * Q_B[BGeo] / ABdist2 * (B2A); + force += k * Q[AOwner] * Q[BOwner] / ABdist2 * (B2A); // Fun part: we can modify the electric charge on the fly. But we have to use atomic, since multiple contacts // can modify the same Q. // But this is not recommend unless you understand what you are doing, and there are a lot of details related to it. - // For example, although the charges transfer between geometries, the geometries within one clump cannot - // re-distribute elec charges among them, since no contact among geometries in one clump. Still, you could write - // your own subroutine to further modify those geometry and/or own wildcards in your script, or within the force - // model. + // You could write your own subroutine to further modify those wildcards in your script, or within the force model. // On the other hand, if you do not need to modify the wildcards, you just need to use them for calculating // the force, then that is probably easier and with less strings attached to it. I can see this being more // useful. if (overlapDepth > 0) { // Exchange the elec charge only in physical contact - float avg_Q = (Q_A[AGeo] + Q_B[BGeo]) / 2.; - float A_change_dir = (abs(avg_Q - Q_A[AGeo]) > 1e-11) ? (avg_Q - Q_A[AGeo]) / abs(avg_Q - Q_A[AGeo]) : 0.; + float avg_Q = (Q[AOwner] + Q[BOwner]) / 2.; + float A_change_dir = (abs(avg_Q - Q[AOwner]) > 1e-11) ? (avg_Q - Q[AOwner]) / abs(avg_Q - Q[AOwner]) : 0.; // Modify the charge they carry... the rate is 1e-8 per second - atomicAdd(Q_A + AGeo, A_change_dir * 1e-8 * ts); - atomicAdd(Q_B + BGeo, -A_change_dir * 1e-8 * ts); + atomicAdd(Q + AOwner, A_change_dir * 1e-8 * ts); + atomicAdd(Q + BOwner, -A_change_dir * 1e-8 * ts); } -} \ No newline at end of file +} diff --git a/src/kernel/DEMUserScripts/ForceModelWithFractureModel.cu b/src/kernel/DEMUserScripts/ForceModelWithFractureModel.cu index e990562d..08f57abd 100644 --- a/src/kernel/DEMUserScripts/ForceModelWithFractureModel.cu +++ b/src/kernel/DEMUserScripts/ForceModelWithFractureModel.cu @@ -20,10 +20,10 @@ float3 rotVelCPA, rotVelCPB; rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } -float mass_eff, sqrt_Rd, beta; +float mass_eff, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -131,14 +131,12 @@ if (unbroken > DEME_TINY_FLOAT) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, - AOriQ.y, AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, - BOriQ.y, BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // A few re-usables - float mass_eff, sqrt_Rd, beta; + float mass_eff, cnt_rad, beta; float3 vrel_tan; float3 delta_tan = make_float3(delta_tan_x, delta_tan_y, delta_tan_z); @@ -158,8 +156,9 @@ if (unbroken > DEME_TINY_FLOAT) { } mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - sqrt_Rd = sqrt(temp * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Broken bonds fall back to ordinary Hertzian contact, based on the measured overlap geometry. + cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -176,7 +175,7 @@ if (unbroken > DEME_TINY_FLOAT) { // Figure out if we should apply rolling resistance force bool should_add_rolling_resistance = true; { - const float R_eff = sqrtf((ARadius * BRadius) / (ARadius + BRadius)); + const float R_eff = (cnt_rad * cnt_rad) / overlapDepth; const float kn_simple = deme::FOUR_OVER_THREE * E_cnt * sqrtf(R_eff); const float gn_simple = -2.f * sqrtf(deme::FIVE_OVER_THREE * mass_eff * E_cnt) * beta * powf(R_eff, 0.25f); @@ -210,7 +209,7 @@ if (unbroken > DEME_TINY_FLOAT) { // Tangential force part if (mu_cnt > 0.0) { - const float kt = 8. * G_cnt * sqrt_Rd; + const float kt = 8. * G_cnt * cnt_rad; const float gt = -deme::TWO_TIMES_SQRT_FIVE_OVER_SIX * beta * sqrt(mass_eff * kt); float3 tangent_force = -kt * delta_tan - gt * vrel_tan; const float ft = length(tangent_force); @@ -235,4 +234,4 @@ if (unbroken > DEME_TINY_FLOAT) { delta_tan_z = delta_tan.z; } } -} \ No newline at end of file +} diff --git a/src/kernel/DEMUserScripts/ForceModelWithGravity.cu b/src/kernel/DEMUserScripts/ForceModelWithGravity.cu index cd4d256f..ad3d2bb5 100644 --- a/src/kernel/DEMUserScripts/ForceModelWithGravity.cu +++ b/src/kernel/DEMUserScripts/ForceModelWithGravity.cu @@ -23,10 +23,8 @@ if (overlapDepth > 0) { rotVelCPA = cross(ARotVel, locCPA); rotVelCPB = cross(BRotVel, locCPB); // This is mapping from local rotational velocity to global - applyOriQToVector3(rotVelCPA.x, rotVelCPA.y, rotVelCPA.z, AOriQ.w, AOriQ.x, AOriQ.y, - AOriQ.z); - applyOriQToVector3(rotVelCPB.x, rotVelCPB.y, rotVelCPB.z, BOriQ.w, BOriQ.x, BOriQ.y, - BOriQ.z); + applyOriQToVector3(rotVelCPA, AOriQ); + applyOriQToVector3(rotVelCPB, BOriQ); } // The (total) relative linear velocity of A relative to B @@ -34,8 +32,9 @@ if (overlapDepth > 0) { const float projection = dot(velB2A, B2A); const float mass_eff = (AOwnerMass * BOwnerMass) / (AOwnerMass + BOwnerMass); - const float sqrt_Rd = sqrt(overlapDepth * (ARadius * BRadius) / (ARadius + BRadius)); - const float Sn = 2. * E_cnt * sqrt_Rd; + // Use the measured overlap area so the Hertzian response remains meaningful for mesh-mesh contacts. + const float cnt_rad = sqrtf(overlapArea / deme::PI); + const float Sn = 2. * E_cnt * cnt_rad; const float loge = (CoR_cnt < DEME_TINY_FLOAT) ? log(DEME_TINY_FLOAT) : log(CoR_cnt); const float beta = loge / sqrt(loge * loge + deme::PI_SQUARED); @@ -52,5 +51,8 @@ if (overlapDepth > 0) { const double Gconst = 6.674e-11 * 86400 * 86400 / 1.496e+11 / 1.496e+11 / 1.496e+11; const double ABdist2 = dot(bodyAPos - bodyBPos, bodyAPos - bodyBPos); // To A, gravity pulls it towards B, so -B2A direction - force += Gconst * my_mass_A[AGeo] * my_mass_B[BGeo] / ABdist2 * (-B2A); + force += Gconst * my_mass_A[AOwner] * my_mass_B[BOwner] / ABdist2 * (-B2A); + // Note that this G force is applied at the contact point which is in general out of the sphere (for non-touching + // spheres the contact point is in between them), but the vector of the force still goes through the center of the + // sphere so the effect is the same. } diff --git a/src/visualization/CMakeLists.txt b/src/visualization/CMakeLists.txt new file mode 100644 index 00000000..de2a9645 --- /dev/null +++ b/src/visualization/CMakeLists.txt @@ -0,0 +1,25 @@ +# The visualizer stays in a separate object target so the C++ library and Python extension can both include it without +# forcing raylib into DEME's solver internals. +set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparty/imgui) +install(FILES ${IMGUI_DIR}/LICENSE.txt DESTINATION ${CMAKE_INSTALL_DATADIR}/DEME/licenses RENAME imgui-LICENSE.txt) +add_library(visualization_bridge OBJECT VisualizerBridge.cpp) +target_include_directories(visualization_bridge PRIVATE ${ProjectIncludeSource} ${ProjectIncludeGenerated}) +set_target_properties(visualization_bridge PROPERTIES POSITION_INDEPENDENT_CODE TRUE CXX_STANDARD ${CXXSTD_SUPPORTED}) +add_library(visualization OBJECT DEMVisualizer.cpp NativeRenderer.cpp + ${IMGUI_DIR}/imgui.cpp ${IMGUI_DIR}/imgui_draw.cpp + ${IMGUI_DIR}/imgui_tables.cpp ${IMGUI_DIR}/imgui_widgets.cpp + ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp) + +target_include_directories(visualization + PUBLIC ${ProjectIncludeSource} + PUBLIC ${ProjectIncludeGenerated} + PRIVATE ${IMGUI_DIR} ${IMGUI_DIR}/backends + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../thirdparty/raylib/src/external +) + +target_link_libraries(visualization PUBLIC raylib) + +set_target_properties(visualization PROPERTIES + POSITION_INDEPENDENT_CODE TRUE + CXX_STANDARD ${CXXSTD_SUPPORTED} +) diff --git a/src/visualization/DEMVisualizer.cpp b/src/visualization/DEMVisualizer.cpp new file mode 100644 index 00000000..b0d4e6b2 --- /dev/null +++ b/src/visualization/DEMVisualizer.cpp @@ -0,0 +1,546 @@ +// Copyright (c) 2021, SBEL GPU Development Team +// Copyright (c) 2021, University of Wisconsin - Madison +// SPDX-License-Identifier: BSD-3-Clause + +#include "DEM/utils/DEMVisualizer.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/utils/Logger.hpp" +// raylib defines convenience macros such as PI, so keep it after DEME headers to avoid altering their declarations. +#include "NativeRenderer.h" +#include "ViewerInteraction.h" +#include +#include + +namespace deme { +namespace { + +Color toRayColor(DEMVisualizerColor color) { + return {color.r, color.g, color.b, color.a}; +} + +DEMVisualizerColor defaultFamilyColor(family_t family) { + // The multiplicative hash keeps adjacent family IDs visually distinct without maintaining a fixed-size palette. + const std::uint32_t hash = static_cast(family) * 2654435761u; + return {static_cast(80u + (hash & 0x7fu)), static_cast(80u + ((hash >> 8u) & 0x7fu)), + static_cast(80u + ((hash >> 16u) & 0x7fu)), 255}; +} + +bool finite(Vector3 p) { + return std::isfinite(p.x) && std::isfinite(p.y) && std::isfinite(p.z); +} + +} // namespace + +// Viewer-owned scene/frame storage insulates OpenGL from solver internals and keeps resource lifetime on the UI thread. +struct DEMVisualizer::Impl { + Impl(std::function scene_callback, + std::function frame_callback) + : capture_scene(std::move(scene_callback)), capture_frame(std::move(frame_callback)) { + visible.fill(true); + } + std::function capture_scene; + std::function capture_frame; + int width = 1440, height = 900, target_fps = 60; + std::string title = "DEM-Engine | Simulation inspector"; + bool initialized = false, render_spheres = true, render_triangles = true; + bool paused = false, step_requested = false, wireframe = false, custom_camera = false; + bool scene_loaded = false, auto_range = true, grid = true; + int resolution = 12, color_mode = 0; + float scalar_min = 0, scalar_max = 1; + std::string screenshot; + DEMVisualizerColor background{24, 29, 39, 255}; + Camera3D camera{{4, 4, 3}, {0, 0, 0}, {0, 0, 1}, 45, CAMERA_PERSPECTIVE}; + std::map family_colors; + std::array visible; + std::array family_counts{}; + bodyID_t selected = NULL_BODYID; + size_t selected_geometry = std::numeric_limits::max(); + bool selected_sphere = false; + ImGuiContext* ui_context = nullptr; + bool ui_renderer_ready = false; + visualization::NativeRenderer renderer; + DEMVisualizationScene scene; + DEMVisualizationFrame frame; + std::vector> colors; + std::vector owner_categories; + + int sidebarWidth() const { return std::min(340, GetScreenWidth() / 2); } + int viewportWidth() const { return std::max(1, GetScreenWidth() - sidebarWidth()); } + bool ownerVisible(bodyID_t owner) const { + return owner < frame.families.size() && owner < owner_categories.size() && visible[frame.families[owner]] && + ((render_spheres && (owner_categories[owner] & 1)) || + (render_triangles && (owner_categories[owner] & 2))); + } + void clearSelection() { + selected = NULL_BODYID; + selected_geometry = std::numeric_limits::max(); + } + + // Compute bounds only on demand, including component offsets and mesh extents rather than just owner centers. + void fit(bool selection_only) { + Vector3 lo{INFINITY, INFINITY, INFINITY}, hi{-INFINITY, -INFINITY, -INFINITY}; + bool found = false; + auto add = [&](bodyID_t owner, float3 point, float radius) { + if (!ownerVisible(owner) || (selection_only && owner != selected)) + return; + Vector3 p = visualization::WorldPoint(point, frame.positions[owner], frame.orientations[owner]); + if (!finite(p)) + return; + lo = Vector3Min(lo, Vector3Subtract(p, {radius, radius, radius})); + hi = Vector3Max(hi, Vector3Add(p, {radius, radius, radius})); + found = true; + }; + if (render_spheres) + for (const auto& s : scene.spheres) + add(s.owner, s.offset, s.radius); + if (render_triangles) + for (const auto& t : scene.triangles) { + add(t.owner, t.a, 0); + add(t.owner, t.b, 0); + add(t.owner, t.c, 0); + } + if (!found) + return; + Vector3 direction = Vector3Normalize(Vector3Subtract(camera.position, camera.target)); + if (Vector3Length(direction) < 0.5f) + direction = Vector3Normalize({1, 1, 0.7f}); + camera.target = Vector3Scale(Vector3Add(lo, hi), 0.5f); + float radius = std::max(Vector3Distance(lo, hi) * 0.5f, 0.001f); + float half_angle = camera.fovy * DEG2RAD * 0.5f; + float aspect = static_cast(viewportWidth()) / std::max(GetScreenHeight(), 1); + half_angle = std::min(half_angle, std::atan(std::tan(half_angle) * aspect)); + camera.position = Vector3Add(camera.target, Vector3Scale(direction, radius / std::sin(half_angle) * 1.15f)); + } + + // Compute colors per owner. Only speed mode/selection requests velocity data from the solver. + void updateColors() { + colors.resize(frame.positions.size()); + family_counts.fill(0); + float low = INFINITY, high = -INFINITY; + auto scalar = [&](size_t i) { + return color_mode == 2 && i < frame.velocities.size() + ? Vector3Length(visualization::ToVector(frame.velocities[i])) + : frame.positions[i].z; + }; + for (size_t i = 0; i < colors.size(); ++i) { + if (i < owner_categories.size() && owner_categories[i]) + ++family_counts[frame.families[i]]; + float value = scalar(i); + if (ownerVisible(static_cast(i)) && std::isfinite(value)) { + low = std::min(low, value); + high = std::max(high, value); + } + } + if (auto_range && std::isfinite(low)) { + scalar_min = low; + scalar_max = high; + } + for (size_t i = 0; i < colors.size(); ++i) { + family_t family = frame.families[i]; + auto it = family_colors.find(family); + auto color = it == family_colors.end() ? defaultFamilyColor(family) : it->second; + if (color_mode) { + float range = scalar_max - scalar_min; + float t = range > 0 ? (scalar(i) - scalar_min) / range : 0.5f; + colors[i] = visualization::ScalarColor(std::isfinite(t) ? t : 0.f); + } else { + colors[i] = {color.r / 255.f, color.g / 255.f, color.b / 255.f, color.a / 255.f}; + } + colors[i][3] = ownerVisible(static_cast(i)) && finite(visualization::ToVector(frame.positions[i])) + ? colors[i][3] + : 0.f; + } + } + + // All inspection state stays in the viewer; buttons only queue application-visible stepping requests. + void sidebar() { + ImGui::SetNextWindowPos({static_cast(viewportWidth()), 0}); + ImGui::SetNextWindowSize({static_cast(sidebarWidth()), static_cast(GetScreenHeight())}); + ImGui::Begin("Inspector", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); + ImGui::TextColored({0.35f, 0.8f, 0.95f, 1}, "DEM-ENGINE"); + ImGui::TextDisabled("Simulation inspector / Z up"); + ImGui::Separator(); + ImGui::Text("Time %.6g s", frame.simulation_time); + ImGui::Text("%d FPS | %.1f ms", GetFPS(), GetFrameTime() * 1000.f); + if (ImGui::Button(paused ? "Resume [Space]" : "Pause [Space]")) { + paused = !paused; + step_requested = false; + } + ImGui::SameLine(); + if (ImGui::Button("Step [.]")) { + paused = true; + step_requested = true; + } + if (ImGui::Button("Frame all [F]")) + fit(false); + ImGui::SameLine(); + if (ImGui::Button("Frame selected")) + fit(true); + ImGui::Spacing(); + if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("%zu owners", frame.positions.size()); + ImGui::Checkbox("Spheres", &render_spheres); + ImGui::SameLine(); + ImGui::TextDisabled("%zu", scene.spheres.size()); + ImGui::Checkbox("Meshes", &render_triangles); + ImGui::SameLine(); + ImGui::TextDisabled("%zu facets", scene.triangles.size()); + ImGui::Checkbox("Wireframe", &wireframe); + ImGui::SameLine(); + ImGui::Checkbox("XY grid", &grid); + int quality = resolution == 6 ? 0 : (resolution == 12 ? 1 : 2); + ImGui::TextUnformatted("Sphere detail"); + ImGui::SetNextItemWidth(-1); + if (ImGui::Combo("##sphere_detail", &quality, "Low\0Medium\0High\0")) { + resolution = quality == 0 ? 6 : (quality == 1 ? 12 : 24); + renderer.SetScene(scene, resolution); + } + } + if (ImGui::CollapsingHeader("Color", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SetNextItemWidth(-1); + ImGui::Combo("##color_mode", &color_mode, "Family\0Owner height (m)\0Owner speed (m/s)\0"); + if (color_mode) { + ImGui::Checkbox("Automatic range", &auto_range); + if (!auto_range) { + ImGui::DragFloat("Minimum", &scalar_min, 0.01f); + ImGui::DragFloat("Maximum", &scalar_max, 0.01f); + } + ImVec2 p = ImGui::GetCursorScreenPos(); + float w = ImGui::GetContentRegionAvail().x; + auto* draw = ImGui::GetWindowDrawList(); + for (int i = 0; i < 64; ++i) { + auto c = visualization::ScalarColor(i / 63.f); + draw->AddRectFilled({p.x + w * i / 64, p.y}, {p.x + w * (i + 1) / 64, p.y + 12}, + ImGui::ColorConvertFloat4ToU32({c[0], c[1], c[2], 1})); + } + ImGui::Dummy({w, 14}); + ImGui::Text("%.4g ... %.4g", scalar_min, scalar_max); + } + } + if (ImGui::CollapsingHeader("Families", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::SmallButton("Show all")) + visible.fill(true); + ImGui::SameLine(); + if (ImGui::SmallButton("Hide all")) + visible.fill(false); + ImGui::BeginChild("family_list", {0, 130}, ImGuiChildFlags_Borders); + for (size_t i = 0; i < family_counts.size(); ++i) + if (family_counts[i]) { + ImGui::PushID(static_cast(i)); + auto family = static_cast(i); + auto it = family_colors.find(family); + auto c = it == family_colors.end() ? defaultFamilyColor(family) : it->second; + float rgb[]{c.r / 255.f, c.g / 255.f, c.b / 255.f}; + if (ImGui::ColorEdit3("##color", rgb, ImGuiColorEditFlags_NoInputs)) + family_colors[family] = {static_cast(rgb[0] * 255), + static_cast(rgb[1] * 255), + static_cast(rgb[2] * 255), 255}; + ImGui::SameLine(); + std::string label = "Family " + std::to_string(i) + " (" + std::to_string(family_counts[i]) + ")"; + ImGui::Checkbox(label.c_str(), &visible[i]); + ImGui::PopID(); + } + ImGui::EndChild(); + } + if (ImGui::CollapsingHeader("Selection", ImGuiTreeNodeFlags_DefaultOpen)) { + if (selected < frame.positions.size()) { + auto p = frame.positions[selected]; + ImGui::Text("Owner %u | Family %u", selected, static_cast(frame.families[selected])); + ImGui::Text("%s %zu", selected_sphere ? "Sphere" : "Triangle", selected_geometry); + ImGui::Text("Position %.5g, %.5g, %.5g", p.x, p.y, p.z); + if (selected < frame.velocities.size()) { + auto v = frame.velocities[selected]; + ImGui::Text("Velocity %.5g, %.5g, %.5g", v.x, v.y, v.z); + } + auto q = frame.orientations[selected]; + ImGui::Text("Quaternion (x y z w)"); + ImGui::Text("%.4g %.4g %.4g %.4g", q.x, q.y, q.z, q.w); + if (ImGui::SmallButton("Clear selection")) + clearSelection(); + } else + ImGui::TextDisabled("Right-click geometry to inspect."); + } + ImGui::Separator(); + ImGui::TextDisabled("Left drag: orbit | Middle drag: pan"); + ImGui::TextDisabled("Wheel: zoom | Right click: select"); + ImGui::TextDisabled("F: frame all | Shift+F: selected"); + if (ImGui::Button("Screenshot [F12]")) + screenshot = "deme-screenshot.png"; + ImGui::End(); + } +}; + +DEMVisualizer::DEMVisualizer(std::function scene, + std::function frame) + : m_impl(std::make_unique(std::move(scene), std::move(frame))) {} +DEMVisualizer::~DEMVisualizer() { + Close(); +} + +void DEMVisualizer::Initialize() { + auto& v = *m_impl; + if (v.initialized) + return; + if (IsWindowReady()) + DEME_ERROR("Only one DEMVisualizer window can be initialized in a process at a time."); + SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE); + InitWindow(v.width, v.height, v.title.c_str()); + if (!IsWindowReady()) + DEME_ERROR("DEMVisualizer could not create a window. Check DISPLAY/WSLg and OpenGL availability."); + v.initialized = true; + SetWindowMinSize(640, 480); + ::SetTargetFPS(v.target_fps); + v.ui_context = ImGui::CreateContext(); + auto& io = ImGui::GetIO(); + io.IniFilename = nullptr; + io.BackendPlatformName = "deme_raylib"; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + ImFontConfig font_config; + font_config.SizePixels = 16.f; + io.Fonts->AddFontDefault(&font_config); + ImGui::StyleColorsDark(); + auto& style = ImGui::GetStyle(); + style.WindowPadding = {16, 16}; + style.FramePadding = {7, 5}; + style.ItemSpacing = {8, 8}; + style.FrameRounding = 4; + style.WindowBorderSize = 0; + style.Colors[ImGuiCol_WindowBg] = {0.075f, 0.09f, 0.12f, 1}; + style.Colors[ImGuiCol_Header] = {0.15f, 0.22f, 0.29f, 1}; + style.Colors[ImGuiCol_Button] = {0.13f, 0.3f, 0.39f, 1}; + if (!ImGui_ImplOpenGL3_Init("#version 330")) + DEME_ERROR("Could not initialize the visualizer UI renderer."); + v.ui_renderer_ready = true; + v.renderer.Initialize(); + v.scene_loaded = false; +} + +bool DEMVisualizer::Run() const { + return m_impl->initialized && !WindowShouldClose(); +} + +// Capture at a synchronized solver boundary. UI redraw while paused follows exactly the same non-blocking path. +void DEMVisualizer::Render() { + auto& v = *m_impl; + if (!v.initialized) + DEME_ERROR("DEMVisualizer::Render requires Initialize() first."); + if (IsWindowMinimized()) { + PollInputEvents(); + WaitTime(0.02); + return; + } + ImGui::SetCurrentContext(v.ui_context); + v.capture_frame(v.frame, v.color_mode == 2 || v.selected != NULL_BODYID); + if (!v.scene_loaded || v.scene.revision != v.frame.revision) { + bool first = !v.scene_loaded; + v.scene = v.capture_scene(); + // Analytical owners and combined masters have no drawable geometry; exclude them from legends and filters. + v.owner_categories.assign(v.frame.positions.size(), 0); + for (const auto& s : v.scene.spheres) + v.owner_categories.at(s.owner) |= 1; + for (const auto& t : v.scene.triangles) + v.owner_categories.at(t.owner) |= 2; + v.renderer.SetScene(v.scene, v.resolution); + v.scene_loaded = true; + v.clearSelection(); + if (first && !v.custom_camera) + v.fit(false); + } + v.updateColors(); + visualization::BeginInputFrame(); + ImGui_ImplOpenGL3_NewFrame(); + ImGui::NewFrame(); + v.sidebar(); + auto& io = ImGui::GetIO(); + if (!io.WantCaptureKeyboard) { + if (IsKeyPressed(KEY_SPACE)) + SetPaused(!v.paused); + if (IsKeyPressed(KEY_PERIOD)) + RequestStep(); + if (IsKeyPressed(KEY_F)) + v.fit(IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)); + if (IsKeyPressed(KEY_F12)) + v.screenshot = "deme-screenshot.png"; + } + if (!io.WantCaptureMouse && GetMouseX() < v.viewportWidth()) + visualization::Navigate(v.camera); + // Mode may change in the UI; fetch velocities immediately rather than displaying a height-colored speed frame. + if (v.color_mode == 2 && v.frame.velocities.size() != v.frame.positions.size()) + v.capture_frame(v.frame, true); + v.updateColors(); + v.renderer.SetFrame(v.frame, v.colors); + if (!io.WantCaptureMouse && IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) + PickAt(GetMouseX(), GetMouseY()); + BeginDrawing(); + ClearBackground(toRayColor(v.background)); + int render_width = std::max(1, v.viewportWidth() * GetRenderWidth() / std::max(GetScreenWidth(), 1)); + rlViewport(0, 0, render_width, GetRenderHeight()); + v.renderer.Draw(v.camera, render_width, GetRenderHeight(), v.render_spheres, v.render_triangles, v.wireframe, + v.selected); + if (v.grid) + visualization::DrawReferenceGrid(v.camera, render_width, GetRenderHeight()); + rlViewport(0, 0, GetRenderWidth(), GetRenderHeight()); + ImGui::Render(); + rlDrawRenderBatchActive(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + if (!v.screenshot.empty()) { + // raylib's TakeScreenshot strips directory components; ExportImage preserves the caller's full path. + Image image = LoadImageFromScreen(); + bool saved = image.data && ExportImage(image, v.screenshot.c_str()); + UnloadImage(image); + if (!saved) + DEME_WARNING("Could not save visualizer screenshot to %s.", v.screenshot.c_str()); + v.screenshot.clear(); + } + EndDrawing(); +} + +void DEMVisualizer::Close() { + if (!m_impl || !m_impl->initialized) + return; + auto& v = *m_impl; + ImGui::SetCurrentContext(v.ui_context); + v.renderer.Close(); + if (v.ui_renderer_ready) + ImGui_ImplOpenGL3_Shutdown(); + v.ui_renderer_ready = false; + ImGui::DestroyContext(v.ui_context); + v.ui_context = nullptr; + CloseWindow(); + v.initialized = false; + v.scene_loaded = false; +} + +void DEMVisualizer::SetWindowSize(int width, int height) { + if (width <= 0 || height <= 0) + DEME_ERROR("DEMVisualizer window dimensions must be positive."); + m_impl->width = width; + m_impl->height = height; + if (m_impl->initialized) + ::SetWindowSize(width, height); +} +void DEMVisualizer::SetWindowTitle(const std::string& title) { + m_impl->title = title; + if (m_impl->initialized) + ::SetWindowTitle(title.c_str()); +} +void DEMVisualizer::SetTargetFPS(int fps) { + if (fps <= 0) + DEME_ERROR("DEMVisualizer target FPS must be positive (got %d).", fps); + m_impl->target_fps = fps; + if (m_impl->initialized) + ::SetTargetFPS(fps); +} +void DEMVisualizer::SetCameraPosition(float3 position) { + if (!finite(visualization::ToVector(position))) + DEME_ERROR("Camera position must be finite."); + m_impl->camera.position = visualization::ToVector(position); + m_impl->custom_camera = true; +} +void DEMVisualizer::SetCameraTarget(float3 target) { + if (!finite(visualization::ToVector(target))) + DEME_ERROR("Camera target must be finite."); + m_impl->camera.target = visualization::ToVector(target); + m_impl->custom_camera = true; +} +void DEMVisualizer::SetBackgroundColor(DEMVisualizerColor color) { + m_impl->background = color; +} +void DEMVisualizer::SetFamilyColor(family_t family, DEMVisualizerColor color) { + m_impl->family_colors[family] = color; +} +void DEMVisualizer::SetRenderSpheres(bool render) { + m_impl->render_spheres = render; +} +void DEMVisualizer::SetRenderTriangles(bool render) { + m_impl->render_triangles = render; +} +bool DEMVisualizer::IsRenderingSpheres() const { + return m_impl->render_spheres; +} +bool DEMVisualizer::IsRenderingTriangles() const { + return m_impl->render_triangles; +} +bool DEMVisualizer::ShouldStep() { + if (!m_impl->paused) + return true; + bool step = m_impl->step_requested; + m_impl->step_requested = false; + return step; +} +void DEMVisualizer::SetPaused(bool paused) { + m_impl->paused = paused; + m_impl->step_requested = false; +} +bool DEMVisualizer::IsPaused() const { + return m_impl->paused; +} +void DEMVisualizer::RequestStep() { + m_impl->paused = true; + m_impl->step_requested = true; +} +void DEMVisualizer::SetFamilyVisible(family_t family, bool visible) { + m_impl->visible[family] = visible; +} +void DEMVisualizer::SetColorMode(DEMVisualizerColorMode mode) { + if (mode < DEMVisualizerColorMode::FAMILY || mode > DEMVisualizerColorMode::SPEED) + DEME_ERROR("Unknown visualizer color mode."); + m_impl->color_mode = static_cast(mode); +} +void DEMVisualizer::FrameAll() { + if (m_impl->initialized) + m_impl->fit(false); +} +void DEMVisualizer::FrameSelected() { + if (m_impl->initialized) + m_impl->fit(true); +} + +// IDs encode geometry indices, not owner IDs. Several selected components can therefore refer to the same owner. +void DEMVisualizer::PickAt(int x, int y) { + auto& v = *m_impl; + if (!v.initialized || !v.scene_loaded) + return; + int w = std::max(1, v.viewportWidth() * GetRenderWidth() / std::max(GetScreenWidth(), 1)); + int px = x * GetRenderWidth() / std::max(GetScreenWidth(), 1); + int py = y * GetRenderHeight() / std::max(GetScreenHeight(), 1); + auto id = v.renderer.Pick(v.camera, w, GetRenderHeight(), px, py, v.render_spheres, v.render_triangles); + v.clearSelection(); + if (!id) + return; + size_t index = id - 1; + if (index < v.scene.spheres.size()) { + v.selected = v.scene.spheres[index].owner; + v.selected_geometry = index; + v.selected_sphere = true; + } else if (index - v.scene.spheres.size() < v.scene.triangles.size()) { + v.selected_geometry = index - v.scene.spheres.size(); + v.selected = v.scene.triangles[v.selected_geometry].owner; + v.selected_sphere = false; + } +} +bodyID_t DEMVisualizer::GetSelectedOwner() const { + return m_impl->selected; +} +size_t DEMVisualizer::GetSelectedGeometryID() const { + return m_impl->selected_geometry; +} +bool DEMVisualizer::IsSelectedSphere() const { + return m_impl->selected != NULL_BODYID && m_impl->selected_sphere; +} +void DEMVisualizer::RequestScreenshot(const std::string& path) { + if (path.empty()) + DEME_ERROR("Screenshot path must not be empty."); + m_impl->screenshot = path; +} + +} // namespace deme diff --git a/src/visualization/NativeRenderer.cpp b/src/visualization/NativeRenderer.cpp new file mode 100644 index 00000000..27374108 --- /dev/null +++ b/src/visualization/NativeRenderer.cpp @@ -0,0 +1,298 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include "NativeRenderer.h" + +#include +#include +#include + +#include "core/utils/Logger.hpp" +#include +#include "RayMathCompat.h" +#include + +namespace deme { +namespace visualization { +namespace { + +// Explicit attribute locations are shared by both VAOs. Identity is integer data to preserve all 32 owner/element bits. +const char* vertex_shader = R"glsl(#version 330 +layout(location=0) in vec3 position; +layout(location=1) in vec3 normal; +layout(location=2) in vec4 component; +layout(location=3) in uvec2 identity; +uniform mat4 mvp; +uniform samplerBuffer owners; +uniform bool sphere; +uniform uint selected; +out vec3 worldNormal; +flat out vec4 tint; +flat out uint element; +vec3 rotateQ(vec4 q, vec3 p) { return p + 2.0 * cross(q.xyz, cross(q.xyz,p) + q.w*p); } +void main() { + int index = int(identity.x)*3; + vec3 p = texelFetch(owners,index).xyz; + vec4 q = texelFetch(owners,index+1); + tint = texelFetch(owners,index+2); + if (identity.x == selected) tint.rgb = mix(tint.rgb,vec3(1.0,0.78,0.2),0.65); + vec3 local = sphere ? component.xyz + component.w*position : position; + gl_Position = mvp * vec4(p + rotateQ(q,local),1.0); + worldNormal = rotateQ(q,normal); + element = identity.y; +} +)glsl"; + +const char* fragment_shader = R"glsl(#version 330 +in vec3 worldNormal; +flat in vec4 tint; +flat in uint element; +uniform bool picking; +out vec4 color; +void main() { + if (tint.a <= 0.0) discard; + if (picking) { + color = vec4(float(element & 255u),float((element >> 8u)&255u), + float((element >> 16u)&255u),float((element >> 24u)&255u))/255.0; + } else { + vec3 n = normalize(worldNormal); + if (!gl_FrontFacing) n = -n; + float light = 0.34 + 0.66*max(dot(n,normalize(vec3(0.4,-0.5,0.8))),0.0); + color = vec4(tint.rgb*light,tint.a); + } +} +)glsl"; + +struct Vertex { + Vector3 position, normal; +}; +struct Instance { + float x, y, z, radius; + std::uint32_t owner, element; +}; +struct MeshVertex { + Vector3 position, normal; + std::uint32_t owner, element; +}; + +Vector3 vector(float3 v) { + return {v.x, v.y, v.z}; +} + +// Bind interleaved position/normal attributes, leaving the integer identity layout to the caller. +void vertexAttributes(size_t stride) { + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, static_cast(stride), nullptr); + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, static_cast(stride), reinterpret_cast(sizeof(Vector3))); +} + +} // namespace + +void NativeRenderer::Initialize() { + shader = LoadShaderFromMemory(vertex_shader, fragment_shader); + if (!shader.id || shader.id == rlGetShaderIdDefault()) { + shader = {}; + DEME_ERROR("Native visualizer requires OpenGL 3.3 and could not compile its geometry shaders."); + } + glGenVertexArrays(1, &sphere_vao); + glGenBuffers(1, &sphere_vertices); + glGenBuffers(1, &sphere_instances); + glGenVertexArrays(1, &mesh_vao); + glGenBuffers(1, &mesh_vertices); + glGenBuffers(1, &mesh_indices); + glGenBuffers(1, &owner_buffer); + glGenTextures(1, &owner_texture); +} + +void NativeRenderer::Close() { + if (pick_target.id) + UnloadRenderTexture(pick_target); + pick_target = {}; + if (shader.id) + UnloadShader(shader); + shader = {}; + glDeleteVertexArrays(1, &sphere_vao); + glDeleteVertexArrays(1, &mesh_vao); + const unsigned int buffers[] = {sphere_vertices, sphere_instances, mesh_vertices, mesh_indices, owner_buffer}; + glDeleteBuffers(5, buffers); + glDeleteTextures(1, &owner_texture); + sphere_vao = mesh_vao = sphere_vertices = sphere_instances = mesh_vertices = mesh_indices = owner_buffer = + owner_texture = 0; + owner_capacity = 0; +} + +// Build shared sphere topology and static component instances. Facet vertices retain their own flat normal and ID. +void NativeRenderer::SetScene(const DEMVisualizationScene& scene, int resolution) { + if (scene.spheres.size() > INT_MAX || scene.triangles.size() > INT_MAX / 3 || + scene.spheres.size() + scene.triangles.size() >= std::numeric_limits::max()) { + DEME_ERROR("Visualization geometry exceeds the OpenGL draw/index capacity."); + } + Mesh sphere = GenMeshSphere(1.0f, resolution, resolution * 2); + std::vector vertices; + vertices.reserve(sphere.triangleCount * 3); + for (int i = 0; i < sphere.triangleCount * 3; ++i) { + int v = sphere.indices ? sphere.indices[i] : i; + vertices.push_back({{sphere.vertices[v * 3], sphere.vertices[v * 3 + 1], sphere.vertices[v * 3 + 2]}, + {sphere.normals[v * 3], sphere.normals[v * 3 + 1], sphere.normals[v * 3 + 2]}}); + } + UnloadMesh(sphere); + sphere_vertex_count = static_cast(vertices.size()); + sphere_count = static_cast(scene.spheres.size()); + std::vector instances; + instances.reserve(scene.spheres.size()); + for (size_t i = 0; i < scene.spheres.size(); ++i) { + const auto& s = scene.spheres[i]; + instances.push_back({s.offset.x, s.offset.y, s.offset.z, s.radius, s.owner, static_cast(i + 1)}); + } + glBindVertexArray(sphere_vao); + glBindBuffer(GL_ARRAY_BUFFER, sphere_vertices); + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW); + vertexAttributes(sizeof(Vertex)); + glBindBuffer(GL_ARRAY_BUFFER, sphere_instances); + glBufferData(GL_ARRAY_BUFFER, instances.size() * sizeof(Instance), instances.data(), GL_STATIC_DRAW); + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Instance), nullptr); + glVertexAttribDivisor(2, 1); + glEnableVertexAttribArray(3); + glVertexAttribIPointer(3, 2, GL_UNSIGNED_INT, sizeof(Instance), reinterpret_cast(offsetof(Instance, owner))); + glVertexAttribDivisor(3, 1); + + std::vector mesh; + std::vector indices; + mesh.reserve(scene.triangles.size() * 3); + indices.reserve(scene.triangles.size() * 3); + for (size_t i = 0; i < scene.triangles.size(); ++i) { + const auto& t = scene.triangles[i]; + Vector3 a = vector(t.a), b = vector(t.b), c = vector(t.c); + Vector3 normal = Vector3Normalize(Vector3CrossProduct(Vector3Subtract(b, a), Vector3Subtract(c, a))); + for (auto p : {a, b, c}) { + indices.push_back(static_cast(mesh.size())); + mesh.push_back({p, normal, t.owner, static_cast(scene.spheres.size() + i + 1)}); + } + } + mesh_index_count = static_cast(indices.size()); + glBindVertexArray(mesh_vao); + glBindBuffer(GL_ARRAY_BUFFER, mesh_vertices); + glBufferData(GL_ARRAY_BUFFER, mesh.size() * sizeof(MeshVertex), mesh.data(), GL_STATIC_DRAW); + vertexAttributes(sizeof(MeshVertex)); + glEnableVertexAttribArray(3); + glVertexAttribIPointer(3, 2, GL_UNSIGNED_INT, sizeof(MeshVertex), + reinterpret_cast(offsetof(MeshVertex, owner))); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh_indices); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(std::uint32_t), indices.data(), GL_STATIC_DRAW); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); +} + +// The only per-frame geometry upload is O(owners), independent of component/facet counts. +void NativeRenderer::SetFrame(const DEMVisualizationFrame& frame, const std::vector>& colors) { + const size_t count = frame.positions.size(); + int max_texels = 0; + glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &max_texels); + if (count > static_cast(max_texels) / 3 || count > INT_MAX / 3) { + DEME_ERROR("Visualization needs %zu owner texels; this OpenGL device supports %d.", count * 3, max_texels); + } + owner_data.resize(count * 3); + for (size_t i = 0; i < count; ++i) { + auto p = frame.positions[i]; + auto q = frame.orientations[i]; + owner_data[i * 3] = {p.x, p.y, p.z, 0}; + owner_data[i * 3 + 1] = {q.x, q.y, q.z, q.w}; + owner_data[i * 3 + 2] = colors[i]; + } + glBindBuffer(GL_TEXTURE_BUFFER, owner_buffer); + size_t bytes = owner_data.size() * sizeof(owner_data[0]); + if (bytes > owner_capacity || !owner_capacity) { + owner_capacity = std::max(bytes, sizeof(owner_data[0])); + glBufferData(GL_TEXTURE_BUFFER, owner_capacity, nullptr, GL_DYNAMIC_DRAW); + } + if (bytes) + glBufferSubData(GL_TEXTURE_BUFFER, 0, bytes, owner_data.data()); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, owner_texture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, owner_buffer); + glBindTexture(GL_TEXTURE_BUFFER, 0); + glBindBuffer(GL_TEXTURE_BUFFER, 0); +} + +// Draw directly with OpenGL; flush raylib first and restore the state its overlay drawing expects afterwards. +void NativeRenderer::Draw(const Camera3D& camera, + int width, + int height, + bool spheres, + bool triangles, + bool wireframe, + bodyID_t selected, + bool picking) { + rlDrawRenderBatchActive(); + float distance = Vector3Distance(camera.position, camera.target); + double near_clip = std::max(0.00001, static_cast(distance) * 0.0001); + Matrix projection = MatrixPerspective(camera.fovy * DEG2RAD, static_cast(width) / std::max(height, 1), + near_clip, std::max(1000.0, static_cast(distance) * 100.0)); + Matrix mvp = MatrixMultiply(GetCameraMatrix(camera), projection); + glUseProgram(shader.id); + glUniformMatrix4fv(glGetUniformLocation(shader.id, "mvp"), 1, GL_FALSE, MatrixToFloat(mvp)); + glUniform1i(glGetUniformLocation(shader.id, "owners"), 0); + glUniform1i(glGetUniformLocation(shader.id, "picking"), picking); + glUniform1ui(glGetUniformLocation(shader.id, "selected"), selected); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, owner_texture); + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + if (picking) { + glDisable(GL_BLEND); + } else { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + // DEM mesh facets must remain visible from either side. The fragment shader flips back-face lighting normals. + glDisable(GL_CULL_FACE); + if (wireframe && !picking) + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + if (spheres && sphere_count) { + glUniform1i(glGetUniformLocation(shader.id, "sphere"), 1); + glBindVertexArray(sphere_vao); + glDrawArraysInstanced(GL_TRIANGLES, 0, sphere_vertex_count, sphere_count); + } + if (triangles && mesh_index_count) { + glUniform1i(glGetUniformLocation(shader.id, "sphere"), 0); + glBindVertexArray(mesh_vao); + glDrawElements(GL_TRIANGLES, mesh_index_count, GL_UNSIGNED_INT, nullptr); + } + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_BUFFER, 0); + glUseProgram(0); + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glEnable(GL_CULL_FACE); +} + +// A separate single-sample target preserves exact IDs even when the visible window uses MSAA. +std::uint32_t +NativeRenderer::Pick(const Camera3D& camera, int width, int height, int x, int y, bool spheres, bool triangles) { + if (width <= 0 || height <= 0 || x < 0 || y < 0 || x >= width || y >= height) + return 0; + if (pick_target.texture.width != width || pick_target.texture.height != height) { + if (pick_target.id) + UnloadRenderTexture(pick_target); + pick_target = LoadRenderTexture(width, height); + if (!pick_target.id) + DEME_ERROR("Unable to allocate visualizer picking framebuffer."); + } + BeginTextureMode(pick_target); + ClearBackground({0, 0, 0, 0}); + GLboolean dither = glIsEnabled(GL_DITHER); + glDisable(GL_DITHER); + Draw(camera, width, height, spheres, triangles, false, NULL_BODYID, true); + unsigned char pixel[4]{}; + glReadPixels(x, height - 1 - y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + if (dither) + glEnable(GL_DITHER); + EndTextureMode(); + return std::uint32_t(pixel[0]) | (std::uint32_t(pixel[1]) << 8) | (std::uint32_t(pixel[2]) << 16) | + (std::uint32_t(pixel[3]) << 24); +} + +} // namespace visualization +} // namespace deme diff --git a/src/visualization/NativeRenderer.h b/src/visualization/NativeRenderer.h new file mode 100644 index 00000000..cf88b8ef --- /dev/null +++ b/src/visualization/NativeRenderer.h @@ -0,0 +1,47 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#pragma once + +#include +#include +#include + +#include "DEM/Defines.h" +#include "DEM/utils/VisualizationData.h" +#include + +namespace deme { +namespace visualization { + +// Persistent OpenGL resources. The window/context must outlive this object; Close is also safe on empty resources. +// Local geometry is uploaded by SetScene; SetFrame only changes a texture buffer containing three vec4s per owner. +class NativeRenderer { + public: + void Initialize(); + void Close(); + void SetScene(const DEMVisualizationScene& scene, int sphere_resolution); + void SetFrame(const DEMVisualizationFrame& frame, const std::vector>& colors); + void Draw(const Camera3D& camera, + int width, + int height, + bool spheres, + bool triangles, + bool wireframe, + bodyID_t selected, + bool picking = false); + // Returns zero for background; otherwise one plus the sphere index or sphere_count plus triangle index. + std::uint32_t Pick(const Camera3D& camera, int width, int height, int x, int y, bool spheres, bool triangles); + + private: + Shader shader{}; + unsigned int sphere_vao = 0, sphere_vertices = 0, sphere_instances = 0; + unsigned int mesh_vao = 0, mesh_vertices = 0, mesh_indices = 0; + unsigned int owner_buffer = 0, owner_texture = 0; + size_t owner_capacity = 0; + int sphere_vertex_count = 0, sphere_count = 0, mesh_index_count = 0; + RenderTexture2D pick_target{}; + std::vector> owner_data; +}; + +} // namespace visualization +} // namespace deme diff --git a/src/visualization/RayMathCompat.h b/src/visualization/RayMathCompat.h new file mode 100644 index 00000000..14aea800 --- /dev/null +++ b/src/visualization/RayMathCompat.h @@ -0,0 +1,9 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#pragma once + +// raymath's float3 is an unrelated return-buffer type; rename it locally to avoid colliding with CUDA's float3. +// Include this wrapper everywhere in the viewer rather than exposing the workaround to public headers. +#define float3 RayMathFloat3 +#include +#undef float3 diff --git a/src/visualization/ViewerInteraction.h b/src/visualization/ViewerInteraction.h new file mode 100644 index 00000000..4bde5288 --- /dev/null +++ b/src/visualization/ViewerInteraction.h @@ -0,0 +1,123 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#pragma once + +#include +#include +#include + +#include +#include +#include +#include "RayMathCompat.h" + +namespace deme { +namespace visualization { + +inline Vector3 ToVector(float3 p) { + return {p.x, p.y, p.z}; +} + +// Use the same projection for geometry and reference lines, including non-square viewports and small DEM units. +inline Matrix Projection(const Camera3D& camera, int width, int height) { + double distance = Vector3Distance(camera.position, camera.target); + return MatrixPerspective(camera.fovy * DEG2RAD, static_cast(width) / std::max(height, 1), + std::max(0.00001, distance * 0.0001), std::max(1000.0, distance * 100.0)); +} + +// DEME is Z-up. A distance-scaled XY grid provides context without uploading simulation geometry through raylib. +inline void DrawReferenceGrid(const Camera3D& camera, int width, int height) { + float spacing = + std::pow(10.f, std::floor(std::log10(std::max(Vector3Distance(camera.position, camera.target), 1.e-5f))) - 1); + float cx = std::floor(camera.target.x / spacing) * spacing; + float cy = std::floor(camera.target.y / spacing) * spacing; + BeginMode3D(camera); + rlSetMatrixProjection(Projection(camera, width, height)); + for (int i = -10; i <= 10; ++i) { + DrawLine3D({cx + i * spacing, cy - 10 * spacing, 0}, {cx + i * spacing, cy + 10 * spacing, 0}, + {57, 65, 79, 255}); + DrawLine3D({cx - 10 * spacing, cy + i * spacing, 0}, {cx + 10 * spacing, cy + i * spacing, 0}, + {57, 65, 79, 255}); + } + DrawLine3D({0, 0, 0}, {spacing * 3, 0, 0}, {220, 95, 95, 255}); + DrawLine3D({0, 0, 0}, {0, spacing * 3, 0}, {95, 200, 125, 255}); + DrawLine3D({0, 0, 0}, {0, 0, spacing * 3}, {95, 150, 240, 255}); + EndMode3D(); +} + +// Owner quaternions are x,y,z,w. This also handles components of combined owners using their reimposed poses. +inline Vector3 WorldPoint(float3 local, float3 position, float4 orientation) { + return Vector3Add(ToVector(position), + Vector3RotateByQuaternion( + ToVector(local), Quaternion{orientation.x, orientation.y, orientation.z, orientation.w})); +} + +// A small platform bridge keeps ImGui independent of raylib's private GLFW window. Render backend is upstream ImGui. +inline void BeginInputFrame() { + auto& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(GetScreenWidth()), static_cast(GetScreenHeight())); + io.DisplayFramebufferScale = ImVec2(static_cast(GetRenderWidth()) / std::max(GetScreenWidth(), 1), + static_cast(GetRenderHeight()) / std::max(GetScreenHeight(), 1)); + io.DeltaTime = std::max(GetFrameTime(), 0.001f); + auto p = GetMousePosition(); + io.AddMousePosEvent(p.x, p.y); + io.AddFocusEvent(IsWindowFocused()); + io.AddMouseButtonEvent(0, IsMouseButtonDown(MOUSE_BUTTON_LEFT)); + io.AddMouseButtonEvent(1, IsMouseButtonDown(MOUSE_BUTTON_RIGHT)); + io.AddMouseButtonEvent(2, IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)); + auto wheel = GetMouseWheelMoveV(); + io.AddMouseWheelEvent(wheel.x, wheel.y); + io.AddKeyEvent(ImGuiMod_Ctrl, IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL)); + io.AddKeyEvent(ImGuiMod_Shift, IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)); + io.AddKeyEvent(ImGuiMod_Alt, IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT)); + io.AddKeyEvent(ImGuiMod_Super, IsKeyDown(KEY_LEFT_SUPER) || IsKeyDown(KEY_RIGHT_SUPER)); + const int keys[] = {KEY_TAB, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_PAGE_UP, KEY_PAGE_DOWN, + KEY_HOME, KEY_END, KEY_INSERT, KEY_DELETE, KEY_BACKSPACE, KEY_SPACE, KEY_ENTER, + KEY_ESCAPE, KEY_A, KEY_C, KEY_V, KEY_X, KEY_Y, KEY_Z}; + const ImGuiKey mapped[] = {ImGuiKey_Tab, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, + ImGuiKey_DownArrow, ImGuiKey_PageUp, ImGuiKey_PageDown, ImGuiKey_Home, + ImGuiKey_End, ImGuiKey_Insert, ImGuiKey_Delete, ImGuiKey_Backspace, + ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_A, + ImGuiKey_C, ImGuiKey_V, ImGuiKey_X, ImGuiKey_Y, + ImGuiKey_Z}; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) + io.AddKeyEvent(mapped[i], IsKeyDown(keys[i])); + for (int c = GetCharPressed(); c; c = GetCharPressed()) + io.AddInputCharacter(static_cast(c)); +} + +// Z-up orbit camera with distance-scaled panning. Clamping pitch prevents the up vector becoming singular. +inline void Navigate(Camera3D& camera) { + Vector3 offset = Vector3Subtract(camera.position, camera.target); + float distance = std::max(Vector3Length(offset), 1.e-5f); + Vector2 delta = GetMouseDelta(); + float yaw = std::atan2(offset.y, offset.x); + float pitch = std::asin(std::clamp(offset.z / distance, -1.f, 1.f)); + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { + yaw -= delta.x * 0.006f; + pitch = std::clamp(pitch + delta.y * 0.006f, -1.55f, 1.55f); + } + if (IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) { + Vector3 forward = Vector3Normalize(Vector3Negate(offset)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); + Vector3 up = Vector3CrossProduct(right, forward); + float scale = distance * 0.0015f; + camera.target = Vector3Add( + camera.target, Vector3Add(Vector3Scale(right, -delta.x * scale), Vector3Scale(up, delta.y * scale))); + } + distance = std::clamp(distance * std::exp(-GetMouseWheelMove() * 0.12f), 1.e-5f, 1.e12f); + camera.position = + Vector3Add(camera.target, {distance * std::cos(pitch) * std::cos(yaw), + distance * std::cos(pitch) * std::sin(yaw), distance * std::sin(pitch)}); +} + +// Perceptually ordered blue/cyan/yellow ramp, shared by owner colors and the scalar legend. +inline std::array ScalarColor(float t) { + t = std::clamp(t, 0.f, 1.f); + const Vector3 a{0.16f, 0.28f, 0.75f}, b{0.1f, 0.8f, 0.75f}, c{1.f, 0.85f, 0.2f}; + auto color = t < 0.5f ? Vector3Lerp(a, b, t * 2) : Vector3Lerp(b, c, (t - 0.5f) * 2); + return {color.x, color.y, color.z, 1.f}; +} + +} // namespace visualization +} // namespace deme diff --git a/src/visualization/VisualizerBridge.cpp b/src/visualization/VisualizerBridge.cpp new file mode 100644 index 00000000..70164ae0 --- /dev/null +++ b/src/visualization/VisualizerBridge.cpp @@ -0,0 +1,16 @@ +// Copyright (c) 2026, SBEL GPU Development Team +// SPDX-License-Identifier: BSD-3-Clause +#include "DEM/API.h" +#include "DEM/utils/DEMVisualizer.h" + +namespace deme { + +// This small bridge belongs to the solver archive (and the Python extension), never the graphics shared library. +// Keep the solver reference non-owning, as in the original API; the Python binding retains its keep_alive policy. +DEMVisualizer::DEMVisualizer(DEMSolver& solver) + : DEMVisualizer([&solver]() { return solver.GetVisualizationScene(); }, + [&solver](DEMVisualizationFrame& frame, bool velocities) { + solver.GetVisualizationFrame(frame, velocities); + }) {} + +} // namespace deme diff --git a/thirdparty/imgui/LICENSE.txt b/thirdparty/imgui/LICENSE.txt new file mode 100644 index 00000000..00ae473a --- /dev/null +++ b/thirdparty/imgui/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2025 Omar Cornut + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/imgui/README.deme.md b/thirdparty/imgui/README.deme.md new file mode 100644 index 00000000..a4e4acfc --- /dev/null +++ b/thirdparty/imgui/README.deme.md @@ -0,0 +1,6 @@ +Dear ImGui v1.91.9b +================== + +Unmodified core and OpenGL 3 backend from https://github.com/ocornut/imgui/tree/v1.91.9b. +Vendored to keep offline builds and source distributions reproducible. +License: MIT (LICENSE.txt). DEME supplies the raylib input bridge in src/visualization. diff --git a/thirdparty/imgui/backends/imgui_impl_opengl3.cpp b/thirdparty/imgui/backends/imgui_impl_opengl3.cpp new file mode 100644 index 00000000..7d0b5362 --- /dev/null +++ b/thirdparty/imgui/backends/imgui_impl_opengl3.cpp @@ -0,0 +1,985 @@ +// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline +// - Desktop GL: 2.x 3.x 4.x +// - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [x] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset) [Desktop OpenGL only!] + +// About WebGL/ES: +// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES. +// - This is done automatically on iOS, Android and Emscripten targets. +// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2025-02-18: OpenGL: Lazily reinitialize embedded GL loader for when calling backend from e.g. other DLL boundaries. (#8406) +// 2024-10-07: OpenGL: Changed default texture sampler to Clamp instead of Repeat/Wrap. +// 2024-06-28: OpenGL: ImGui_ImplOpenGL3_NewFrame() recreates font texture if it has been destroyed by ImGui_ImplOpenGL3_DestroyFontsTexture(). (#7748) +// 2024-05-07: OpenGL: Update loader for Linux to support EGL/GLVND. (#7562) +// 2024-04-16: OpenGL: Detect ES3 contexts on desktop based on version string, to e.g. avoid calling glPolygonMode() on them. (#7447) +// 2024-01-09: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" and variants, fixing regression on distros missing a symlink. +// 2023-11-08: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" instead of "libGL.so.1", accommodating for NetBSD systems having only "libGL.so.3" available. (#6983) +// 2023-10-05: OpenGL: Rename symbols in our internal loader so that LTO compilation with another copy of gl3w is possible. (#6875, #6668, #4445) +// 2023-06-20: OpenGL: Fixed erroneous use glGetIntegerv(GL_CONTEXT_PROFILE_MASK) on contexts lower than 3.2. (#6539, #6333) +// 2023-05-09: OpenGL: Support for glBindSampler() backup/restore on ES3. (#6375) +// 2023-04-18: OpenGL: Restore front and back polygon mode separately when supported by context. (#6333) +// 2023-03-23: OpenGL: Properly restoring "no shader program bound" if it was the case prior to running the rendering function. (#6267, #6220, #6224) +// 2023-03-15: OpenGL: Fixed GL loader crash when GL_VERSION returns NULL. (#6154, #4445, #3530) +// 2023-03-06: OpenGL: Fixed restoration of a potentially deleted OpenGL program, by calling glIsProgram(). (#6220, #6224) +// 2022-11-09: OpenGL: Reverted use of glBufferSubData(), too many corruptions issues + old issues seemingly can't be reproed with Intel drivers nowadays (revert 2021-12-15 and 2022-05-23 changes). +// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. +// 2022-09-27: OpenGL: Added ability to '#define IMGUI_IMPL_OPENGL_DEBUG'. +// 2022-05-23: OpenGL: Reworking 2021-12-15 "Using buffer orphaning" so it only happens on Intel GPU, seems to cause problems otherwise. (#4468, #4825, #4832, #5127). +// 2022-05-13: OpenGL: Fixed state corruption on OpenGL ES 2.0 due to not preserving GL_ELEMENT_ARRAY_BUFFER_BINDING and vertex attribute states. +// 2021-12-15: OpenGL: Using buffer orphaning + glBufferSubData(), seems to fix leaks with multi-viewports with some Intel HD drivers. +// 2021-08-23: OpenGL: Fixed ES 3.0 shader ("#version 300 es") use normal precision floats to avoid wobbly rendering at HD resolutions. +// 2021-08-19: OpenGL: Embed and use our own minimal GL loader (imgui_impl_opengl3_loader.h), removing requirement and support for third-party loader. +// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). +// 2021-06-25: OpenGL: Use OES_vertex_array extension on Emscripten + backup/restore current state. +// 2021-06-21: OpenGL: Destroy individual vertex/fragment shader objects right after they are linked into the main shader. +// 2021-05-24: OpenGL: Access GL_CLIP_ORIGIN when "GL_ARB_clip_control" extension is detected, inside of just OpenGL 4.5 version. +// 2021-05-19: OpenGL: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) +// 2021-04-06: OpenGL: Don't try to read GL_CLIP_ORIGIN unless we're OpenGL 4.5 or greater. +// 2021-02-18: OpenGL: Change blending equation to preserve alpha in output buffer. +// 2021-01-03: OpenGL: Backup, setup and restore GL_STENCIL_TEST state. +// 2020-10-23: OpenGL: Backup, setup and restore GL_PRIMITIVE_RESTART state. +// 2020-10-15: OpenGL: Use glGetString(GL_VERSION) instead of glGetIntegerv(GL_MAJOR_VERSION, ...) when the later returns zero (e.g. Desktop GL 2.x) +// 2020-09-17: OpenGL: Fix to avoid compiling/calling glBindSampler() on ES or pre-3.3 context which have the defines set by a loader. +// 2020-07-10: OpenGL: Added support for glad2 OpenGL loader. +// 2020-05-08: OpenGL: Made default GLSL version 150 (instead of 130) on OSX. +// 2020-04-21: OpenGL: Fixed handling of glClipControl(GL_UPPER_LEFT) by inverting projection matrix. +// 2020-04-12: OpenGL: Fixed context version check mistakenly testing for 4.0+ instead of 3.2+ to enable ImGuiBackendFlags_RendererHasVtxOffset. +// 2020-03-24: OpenGL: Added support for glbinding 2.x OpenGL loader. +// 2020-01-07: OpenGL: Added support for glbinding 3.x OpenGL loader. +// 2019-10-25: OpenGL: Using a combination of GL define and runtime GL version to decide whether to use glDrawElementsBaseVertex(). Fix building with pre-3.2 GL loaders. +// 2019-09-22: OpenGL: Detect default GL loader using __has_include compiler facility. +// 2019-09-16: OpenGL: Tweak initialization code to allow application calling ImGui_ImplOpenGL3_CreateFontsTexture() before the first NewFrame() call. +// 2019-05-29: OpenGL: Desktop GL only: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag. +// 2019-04-30: OpenGL: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. +// 2019-03-29: OpenGL: Not calling glBindBuffer more than necessary in the render loop. +// 2019-03-15: OpenGL: Added a GL call + comments in ImGui_ImplOpenGL3_Init() to detect uninitialized GL function loaders early. +// 2019-03-03: OpenGL: Fix support for ES 2.0 (WebGL 1.0). +// 2019-02-20: OpenGL: Fix for OSX not supporting OpenGL 4.5, we don't try to read GL_CLIP_ORIGIN even if defined by the headers/loader. +// 2019-02-11: OpenGL: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display. +// 2019-02-01: OpenGL: Using GLSL 410 shaders for any version over 410 (e.g. 430, 450). +// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window. +// 2018-11-13: OpenGL: Support for GL 4.5's glClipControl(GL_UPPER_LEFT) / GL_CLIP_ORIGIN. +// 2018-08-29: OpenGL: Added support for more OpenGL loaders: glew and glad, with comments indicative that any loader can be used. +// 2018-08-09: OpenGL: Default to OpenGL ES 3 on iOS and Android. GLSL version default to "#version 300 ES". +// 2018-07-30: OpenGL: Support for GLSL 300 ES and 410 core. Fixes for Emscripten compilation. +// 2018-07-10: OpenGL: Support for more GLSL versions (based on the GLSL version string). Added error output when shaders fail to compile/link. +// 2018-06-08: Misc: Extracted imgui_impl_opengl3.cpp/.h away from the old combined GLFW/SDL+OpenGL3 examples. +// 2018-06-08: OpenGL: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle. +// 2018-05-25: OpenGL: Removed unnecessary backup/restore of GL_ELEMENT_ARRAY_BUFFER_BINDING since this is part of the VAO state. +// 2018-05-14: OpenGL: Making the call to glBindSampler() optional so 3.2 context won't fail if the function is a nullptr pointer. +// 2018-03-06: OpenGL: Added const char* glsl_version parameter to ImGui_ImplOpenGL3_Init() so user can override the GLSL version e.g. "#version 150". +// 2018-02-23: OpenGL: Create the VAO in the render function so the setup can more easily be used with multiple shared GL context. +// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplSdlGL3_RenderDrawData() in the .h file so you can call it yourself. +// 2018-01-07: OpenGL: Changed GLSL shader version from 330 to 150. +// 2017-09-01: OpenGL: Save and restore current bound sampler. Save and restore current polygon mode. +// 2017-05-01: OpenGL: Fixed save and restore of current blend func state. +// 2017-05-01: OpenGL: Fixed save and restore of current GL_ACTIVE_TEXTURE. +// 2016-09-05: OpenGL: Fixed save and restore of current scissor rectangle. +// 2016-07-29: OpenGL: Explicitly setting GL_UNPACK_ROW_LENGTH to reduce issues because SDL changes it. (#752) + +//---------------------------------------- +// OpenGL GLSL GLSL +// version version string +//---------------------------------------- +// 2.0 110 "#version 110" +// 2.1 120 "#version 120" +// 3.0 130 "#version 130" +// 3.1 140 "#version 140" +// 3.2 150 "#version 150" +// 3.3 330 "#version 330 core" +// 4.0 400 "#version 400 core" +// 4.1 410 "#version 410 core" +// 4.2 420 "#version 410 core" +// 4.3 430 "#version 430 core" +// ES 2.0 100 "#version 100" = WebGL 1.0 +// ES 3.0 300 "#version 300 es" = WebGL 2.0 +//---------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_impl_opengl3.h" +#include +#include // intptr_t +#if defined(__APPLE__) +#include +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: ignore unknown flags +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used +#pragma clang diagnostic ignored "-Wnonportable-system-include-path" +#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader) +#endif +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader) +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#endif + +// GL includes +#if defined(IMGUI_IMPL_OPENGL_ES2) +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) +#include // Use GL ES 2 +#else +#include // Use GL ES 2 +#endif +#if defined(__EMSCRIPTEN__) +#ifndef GL_GLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES +#endif +#include +#endif +#elif defined(IMGUI_IMPL_OPENGL_ES3) +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) +#include // Use GL ES 3 +#else +#include // Use GL ES 3 +#endif +#elif !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM) +// Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers. +// Helper libraries are often used for this purpose! Here we are using our own minimal custom loader based on gl3w. +// In the rest of your app/engine, you can use another loader of your choice (gl3w, glew, glad, glbinding, glext, glLoadGen, etc.). +// If you happen to be developing a new feature for this backend (imgui_impl_opengl3.cpp): +// - You may need to regenerate imgui_impl_opengl3_loader.h to add new symbols. See https://github.com/dearimgui/gl3w_stripped +// Typically you would run: python3 ./gl3w_gen.py --output ../imgui/backends/imgui_impl_opengl3_loader.h --ref ../imgui/backends/imgui_impl_opengl3.cpp ./extra_symbols.txt +// - You can temporarily use an unstripped version. See https://github.com/dearimgui/gl3w_stripped/releases +// Changes to this backend using new APIs should be accompanied by a regenerated stripped loader version. +#define IMGL3W_IMPL +#define IMGUI_IMPL_OPENGL_LOADER_IMGL3W +#include "imgui_impl_opengl3_loader.h" +#endif + +// Vertex arrays are not supported on ES2/WebGL1 unless Emscripten which uses an extension +#ifndef IMGUI_IMPL_OPENGL_ES2 +#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +#elif defined(__EMSCRIPTEN__) +#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +#define glBindVertexArray glBindVertexArrayOES +#define glGenVertexArrays glGenVertexArraysOES +#define glDeleteVertexArrays glDeleteVertexArraysOES +#define GL_VERTEX_ARRAY_BINDING GL_VERTEX_ARRAY_BINDING_OES +#endif + +// Desktop GL 2.0+ has extension and glPolygonMode() which GL ES and WebGL don't have.. +// A desktop ES context can technically compile fine with our loader, so we also perform a runtime checks +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) +#define IMGUI_IMPL_OPENGL_HAS_EXTENSIONS // has glGetIntegerv(GL_NUM_EXTENSIONS) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE // may have glPolygonMode() +#endif + +// Desktop GL 2.1+ and GL ES 3.0+ have glBindBuffer() with GL_PIXEL_UNPACK_BUFFER target. +#if !defined(IMGUI_IMPL_OPENGL_ES2) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK +#endif + +// Desktop GL 3.1+ has GL_PRIMITIVE_RESTART state +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_1) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART +#endif + +// Desktop GL 3.2+ has glDrawElementsBaseVertex() which GL ES and WebGL don't have. +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_2) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET +#endif + +// Desktop GL 3.3+ and GL ES 3.0+ have glBindSampler() +#if !defined(IMGUI_IMPL_OPENGL_ES2) && (defined(IMGUI_IMPL_OPENGL_ES3) || defined(GL_VERSION_3_3)) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER +#endif + +// [Debugging] +//#define IMGUI_IMPL_OPENGL_DEBUG +#ifdef IMGUI_IMPL_OPENGL_DEBUG +#include +#define GL_CALL(_CALL) do { _CALL; GLenum gl_err = glGetError(); if (gl_err != 0) fprintf(stderr, "GL error 0x%x returned from '%s'.\n", gl_err, #_CALL); } while (0) // Call with error check +#else +#define GL_CALL(_CALL) _CALL // Call without error check +#endif + +// OpenGL Data +struct ImGui_ImplOpenGL3_Data +{ + GLuint GlVersion; // Extracted at runtime using GL_MAJOR_VERSION, GL_MINOR_VERSION queries (e.g. 320 for GL 3.2) + char GlslVersionString[32]; // Specified by user or detected based on compile time GL settings. + bool GlProfileIsES2; + bool GlProfileIsES3; + bool GlProfileIsCompat; + GLint GlProfileMask; + GLuint FontTexture; + GLuint ShaderHandle; + GLint AttribLocationTex; // Uniforms location + GLint AttribLocationProjMtx; + GLuint AttribLocationVtxPos; // Vertex attributes location + GLuint AttribLocationVtxUV; + GLuint AttribLocationVtxColor; + unsigned int VboHandle, ElementsHandle; + GLsizeiptr VertexBufferSize; + GLsizeiptr IndexBufferSize; + bool HasPolygonMode; + bool HasClipOrigin; + bool UseBufferSubData; + + ImGui_ImplOpenGL3_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static ImGui_ImplOpenGL3_Data* ImGui_ImplOpenGL3_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplOpenGL3_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +// OpenGL vertex attribute state (for ES 1.0 and ES 2.0 only) +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +struct ImGui_ImplOpenGL3_VtxAttribState +{ + GLint Enabled, Size, Type, Normalized, Stride; + GLvoid* Ptr; + + void GetState(GLint index) + { + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &Enabled); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &Size); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &Type); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &Normalized); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &Stride); + glGetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &Ptr); + } + void SetState(GLint index) + { + glVertexAttribPointer(index, Size, Type, (GLboolean)Normalized, Stride, Ptr); + if (Enabled) glEnableVertexAttribArray(index); else glDisableVertexAttribArray(index); + } +}; +#endif + +// Not static to allow third-party code to use that if they want to (but undocumented) +bool ImGui_ImplOpenGL3_InitLoader(); +bool ImGui_ImplOpenGL3_InitLoader() +{ + // Initialize our loader +#ifdef IMGUI_IMPL_OPENGL_LOADER_IMGL3W + if (glGetIntegerv == nullptr && imgl3wInit() != 0) + { + fprintf(stderr, "Failed to initialize OpenGL loader!\n"); + return false; + } +#endif + return true; +} + +// Functions +bool ImGui_ImplOpenGL3_Init(const char* glsl_version) +{ + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + // Initialize loader + if (!ImGui_ImplOpenGL3_InitLoader()) + return false; + + // Setup backend capabilities flags + ImGui_ImplOpenGL3_Data* bd = IM_NEW(ImGui_ImplOpenGL3_Data)(); + io.BackendRendererUserData = (void*)bd; + io.BackendRendererName = "imgui_impl_opengl3"; + + // Query for GL version (e.g. 320 for GL 3.2) + const char* gl_version_str = (const char*)glGetString(GL_VERSION); +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GLES 2 + bd->GlVersion = 200; + bd->GlProfileIsES2 = true; + IM_UNUSED(gl_version_str); +#else + // Desktop or GLES 3 + GLint major = 0; + GLint minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + if (major == 0 && minor == 0) + sscanf(gl_version_str, "%d.%d", &major, &minor); // Query GL_VERSION in desktop GL 2.x, the string will start with "." + bd->GlVersion = (GLuint)(major * 100 + minor * 10); +#if defined(GL_CONTEXT_PROFILE_MASK) + if (bd->GlVersion >= 320) + glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &bd->GlProfileMask); + bd->GlProfileIsCompat = (bd->GlProfileMask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) != 0; +#endif + +#if defined(IMGUI_IMPL_OPENGL_ES3) + bd->GlProfileIsES3 = true; +#else + if (strncmp(gl_version_str, "OpenGL ES 3", 11) == 0) + bd->GlProfileIsES3 = true; +#endif + + bd->UseBufferSubData = false; + /* + // Query vendor to enable glBufferSubData kludge +#ifdef _WIN32 + if (const char* vendor = (const char*)glGetString(GL_VENDOR)) + if (strncmp(vendor, "Intel", 5) == 0) + bd->UseBufferSubData = true; +#endif + */ +#endif + +#ifdef IMGUI_IMPL_OPENGL_DEBUG + printf("GlVersion = %d, \"%s\"\nGlProfileIsCompat = %d\nGlProfileMask = 0x%X\nGlProfileIsES2/IsEs3 = %d/%d\nGL_VENDOR = '%s'\nGL_RENDERER = '%s'\n", bd->GlVersion, gl_version_str, bd->GlProfileIsCompat, bd->GlProfileMask, bd->GlProfileIsES2, bd->GlProfileIsES3, (const char*)glGetString(GL_VENDOR), (const char*)glGetString(GL_RENDERER)); // [DEBUG] +#endif + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET + if (bd->GlVersion >= 320) + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. +#endif + + // Store GLSL version string so we can refer to it later in case we recreate shaders. + // Note: GLSL version is NOT the same as GL version. Leave this to nullptr if unsure. + if (glsl_version == nullptr) + { +#if defined(IMGUI_IMPL_OPENGL_ES2) + glsl_version = "#version 100"; +#elif defined(IMGUI_IMPL_OPENGL_ES3) + glsl_version = "#version 300 es"; +#elif defined(__APPLE__) + glsl_version = "#version 150"; +#else + glsl_version = "#version 130"; +#endif + } + IM_ASSERT((int)strlen(glsl_version) + 2 < IM_ARRAYSIZE(bd->GlslVersionString)); + strcpy(bd->GlslVersionString, glsl_version); + strcat(bd->GlslVersionString, "\n"); + + // Make an arbitrary GL call (we don't actually need the result) + // IF YOU GET A CRASH HERE: it probably means the OpenGL function loader didn't do its job. Let us know! + GLint current_texture; + glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_texture); + + // Detect extensions we support +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + bd->HasPolygonMode = (!bd->GlProfileIsES2 && !bd->GlProfileIsES3); +#endif + bd->HasClipOrigin = (bd->GlVersion >= 450); +#ifdef IMGUI_IMPL_OPENGL_HAS_EXTENSIONS + GLint num_extensions = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions); + for (GLint i = 0; i < num_extensions; i++) + { + const char* extension = (const char*)glGetStringi(GL_EXTENSIONS, i); + if (extension != nullptr && strcmp(extension, "GL_ARB_clip_control") == 0) + bd->HasClipOrigin = true; + } +#endif + + return true; +} + +void ImGui_ImplOpenGL3_Shutdown() +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + ImGui_ImplOpenGL3_DestroyDeviceObjects(); + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset; + IM_DELETE(bd); +} + +void ImGui_ImplOpenGL3_NewFrame() +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplOpenGL3_Init()?"); + + ImGui_ImplOpenGL3_InitLoader(); // Lazily init loader if not already done for e.g. DLL boundaries. + + if (!bd->ShaderHandle) + ImGui_ImplOpenGL3_CreateDeviceObjects(); + if (!bd->FontTexture) + ImGui_ImplOpenGL3_CreateFontsTexture(); +} + +static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_width, int fb_height, GLuint vertex_array_object) +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glEnable(GL_SCISSOR_TEST); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + if (bd->GlVersion >= 310) + glDisable(GL_PRIMITIVE_RESTART); +#endif +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + if (bd->HasPolygonMode) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +#endif + + // Support for GL 4.5 rarely used glClipControl(GL_UPPER_LEFT) +#if defined(GL_CLIP_ORIGIN) + bool clip_origin_lower_left = true; + if (bd->HasClipOrigin) + { + GLenum current_clip_origin = 0; glGetIntegerv(GL_CLIP_ORIGIN, (GLint*)¤t_clip_origin); + if (current_clip_origin == GL_UPPER_LEFT) + clip_origin_lower_left = false; + } +#endif + + // Setup viewport, orthographic projection matrix + // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. + GL_CALL(glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height)); + float L = draw_data->DisplayPos.x; + float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; + float T = draw_data->DisplayPos.y; + float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; +#if defined(GL_CLIP_ORIGIN) + if (!clip_origin_lower_left) { float tmp = T; T = B; B = tmp; } // Swap top and bottom if origin is upper left +#endif + const float ortho_projection[4][4] = + { + { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f/(T-B), 0.0f, 0.0f }, + { 0.0f, 0.0f, -1.0f, 0.0f }, + { (R+L)/(L-R), (T+B)/(B-T), 0.0f, 1.0f }, + }; + glUseProgram(bd->ShaderHandle); + glUniform1i(bd->AttribLocationTex, 0); + glUniformMatrix4fv(bd->AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]); + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + if (bd->GlVersion >= 330 || bd->GlProfileIsES3) + glBindSampler(0, 0); // We use combined texture/sampler state. Applications using GL 3.3 and GL ES 3.0 may set that otherwise. +#endif + + (void)vertex_array_object; +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + glBindVertexArray(vertex_array_object); +#endif + + // Bind vertex/index buffers and setup attributes for ImDrawVert + GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, bd->VboHandle)); + GL_CALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bd->ElementsHandle)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxPos)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxUV)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxColor)); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col))); +} + +// OpenGL3 Render function. +// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly. +// This is in order to be able to run within an OpenGL engine that doesn't do so. +void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) +{ + // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) + int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x); + int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y); + if (fb_width <= 0 || fb_height <= 0) + return; + + ImGui_ImplOpenGL3_InitLoader(); // Lazily init loader if not already done for e.g. DLL boundaries. + + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + + // Backup GL state + GLenum last_active_texture; glGetIntegerv(GL_ACTIVE_TEXTURE, (GLint*)&last_active_texture); + glActiveTexture(GL_TEXTURE0); + GLuint last_program; glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&last_program); + GLuint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, (GLint*)&last_texture); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + GLuint last_sampler; if (bd->GlVersion >= 330 || bd->GlProfileIsES3) { glGetIntegerv(GL_SAMPLER_BINDING, (GLint*)&last_sampler); } else { last_sampler = 0; } +#endif + GLuint last_array_buffer; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, (GLint*)&last_array_buffer); +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + // This is part of VAO on OpenGL 3.0+ and OpenGL ES 3.0+. + GLint last_element_array_buffer; glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &last_element_array_buffer); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_pos; last_vtx_attrib_state_pos.GetState(bd->AttribLocationVtxPos); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_uv; last_vtx_attrib_state_uv.GetState(bd->AttribLocationVtxUV); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_color; last_vtx_attrib_state_color.GetState(bd->AttribLocationVtxColor); +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GLuint last_vertex_array_object; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, (GLint*)&last_vertex_array_object); +#endif +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + GLint last_polygon_mode[2]; if (bd->HasPolygonMode) { glGetIntegerv(GL_POLYGON_MODE, last_polygon_mode); } +#endif + GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport); + GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box); + GLenum last_blend_src_rgb; glGetIntegerv(GL_BLEND_SRC_RGB, (GLint*)&last_blend_src_rgb); + GLenum last_blend_dst_rgb; glGetIntegerv(GL_BLEND_DST_RGB, (GLint*)&last_blend_dst_rgb); + GLenum last_blend_src_alpha; glGetIntegerv(GL_BLEND_SRC_ALPHA, (GLint*)&last_blend_src_alpha); + GLenum last_blend_dst_alpha; glGetIntegerv(GL_BLEND_DST_ALPHA, (GLint*)&last_blend_dst_alpha); + GLenum last_blend_equation_rgb; glGetIntegerv(GL_BLEND_EQUATION_RGB, (GLint*)&last_blend_equation_rgb); + GLenum last_blend_equation_alpha; glGetIntegerv(GL_BLEND_EQUATION_ALPHA, (GLint*)&last_blend_equation_alpha); + GLboolean last_enable_blend = glIsEnabled(GL_BLEND); + GLboolean last_enable_cull_face = glIsEnabled(GL_CULL_FACE); + GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST); + GLboolean last_enable_stencil_test = glIsEnabled(GL_STENCIL_TEST); + GLboolean last_enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + GLboolean last_enable_primitive_restart = (bd->GlVersion >= 310) ? glIsEnabled(GL_PRIMITIVE_RESTART) : GL_FALSE; +#endif + + // Setup desired GL state + // Recreate the VAO every time (this is to easily allow multiple GL contexts to be rendered to. VAO are not shared among GL contexts) + // The renderer would actually work without any VAO bound, but then our VertexAttrib calls would overwrite the default one currently bound. + GLuint vertex_array_object = 0; +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GL_CALL(glGenVertexArrays(1, &vertex_array_object)); +#endif + ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); + + // Will project scissor/clipping rectangles into framebuffer space + ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports + ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) + + // Render command lists + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* draw_list = draw_data->CmdLists[n]; + + // Upload vertex/index buffers + // - OpenGL drivers are in a very sorry state nowadays.... + // During 2021 we attempted to switch from glBufferData() to orphaning+glBufferSubData() following reports + // of leaks on Intel GPU when using multi-viewports on Windows. + // - After this we kept hearing of various display corruptions issues. We started disabling on non-Intel GPU, but issues still got reported on Intel. + // - We are now back to using exclusively glBufferData(). So bd->UseBufferSubData IS ALWAYS FALSE in this code. + // We are keeping the old code path for a while in case people finding new issues may want to test the bd->UseBufferSubData path. + // - See https://github.com/ocornut/imgui/issues/4468 and please report any corruption issues. + const GLsizeiptr vtx_buffer_size = (GLsizeiptr)draw_list->VtxBuffer.Size * (int)sizeof(ImDrawVert); + const GLsizeiptr idx_buffer_size = (GLsizeiptr)draw_list->IdxBuffer.Size * (int)sizeof(ImDrawIdx); + if (bd->UseBufferSubData) + { + if (bd->VertexBufferSize < vtx_buffer_size) + { + bd->VertexBufferSize = vtx_buffer_size; + GL_CALL(glBufferData(GL_ARRAY_BUFFER, bd->VertexBufferSize, nullptr, GL_STREAM_DRAW)); + } + if (bd->IndexBufferSize < idx_buffer_size) + { + bd->IndexBufferSize = idx_buffer_size; + GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, bd->IndexBufferSize, nullptr, GL_STREAM_DRAW)); + } + GL_CALL(glBufferSubData(GL_ARRAY_BUFFER, 0, vtx_buffer_size, (const GLvoid*)draw_list->VtxBuffer.Data)); + GL_CALL(glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, idx_buffer_size, (const GLvoid*)draw_list->IdxBuffer.Data)); + } + else + { + GL_CALL(glBufferData(GL_ARRAY_BUFFER, vtx_buffer_size, (const GLvoid*)draw_list->VtxBuffer.Data, GL_STREAM_DRAW)); + GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx_buffer_size, (const GLvoid*)draw_list->IdxBuffer.Data, GL_STREAM_DRAW)); + } + + for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback != nullptr) + { + // User callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.) + if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) + ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); + else + pcmd->UserCallback(draw_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // Apply scissor/clipping rectangle (Y is inverted in OpenGL) + GL_CALL(glScissor((int)clip_min.x, (int)((float)fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y))); + + // Bind texture, Draw + GL_CALL(glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID())); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET + if (bd->GlVersion >= 320) + GL_CALL(glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset)); + else +#endif + GL_CALL(glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)))); + } + } + } + + // Destroy the temporary VAO +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GL_CALL(glDeleteVertexArrays(1, &vertex_array_object)); +#endif + + // Restore modified GL state + // This "glIsProgram()" check is required because if the program is "pending deletion" at the time of binding backup, it will have been deleted by now and will cause an OpenGL error. See #6220. + if (last_program == 0 || glIsProgram(last_program)) glUseProgram(last_program); + glBindTexture(GL_TEXTURE_2D, last_texture); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + if (bd->GlVersion >= 330 || bd->GlProfileIsES3) + glBindSampler(0, last_sampler); +#endif + glActiveTexture(last_active_texture); +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + glBindVertexArray(last_vertex_array_object); +#endif + glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer); + last_vtx_attrib_state_pos.SetState(bd->AttribLocationVtxPos); + last_vtx_attrib_state_uv.SetState(bd->AttribLocationVtxUV); + last_vtx_attrib_state_color.SetState(bd->AttribLocationVtxColor); +#endif + glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha); + glBlendFuncSeparate(last_blend_src_rgb, last_blend_dst_rgb, last_blend_src_alpha, last_blend_dst_alpha); + if (last_enable_blend) glEnable(GL_BLEND); else glDisable(GL_BLEND); + if (last_enable_cull_face) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE); + if (last_enable_depth_test) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); + if (last_enable_stencil_test) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST); + if (last_enable_scissor_test) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + if (bd->GlVersion >= 310) { if (last_enable_primitive_restart) glEnable(GL_PRIMITIVE_RESTART); else glDisable(GL_PRIMITIVE_RESTART); } +#endif + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + // Desktop OpenGL 3.0 and OpenGL 3.1 had separate polygon draw modes for front-facing and back-facing faces of polygons + if (bd->HasPolygonMode) { if (bd->GlVersion <= 310 || bd->GlProfileIsCompat) { glPolygonMode(GL_FRONT, (GLenum)last_polygon_mode[0]); glPolygonMode(GL_BACK, (GLenum)last_polygon_mode[1]); } else { glPolygonMode(GL_FRONT_AND_BACK, (GLenum)last_polygon_mode[0]); } } +#endif // IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + + glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]); + glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]); + (void)bd; // Not all compilation paths use this +} + +bool ImGui_ImplOpenGL3_CreateFontsTexture() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + + // Build texture atlas + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. + + // Upload texture to graphics system + // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling) + GLint last_texture; + GL_CALL(glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture)); + GL_CALL(glGenTextures(1, &bd->FontTexture)); + GL_CALL(glBindTexture(GL_TEXTURE_2D, bd->FontTexture)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); +#ifdef GL_UNPACK_ROW_LENGTH // Not on WebGL/ES + GL_CALL(glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)); +#endif + GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); + + // Store identifier + io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture); + + // Restore state + GL_CALL(glBindTexture(GL_TEXTURE_2D, last_texture)); + + return true; +} + +void ImGui_ImplOpenGL3_DestroyFontsTexture() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->FontTexture) + { + glDeleteTextures(1, &bd->FontTexture); + io.Fonts->SetTexID(0); + bd->FontTexture = 0; + } +} + +// If you get an error please report on github. You may try different GL context version or GLSL version. See GL<>GLSL version table at the top of this file. +static bool CheckShader(GLuint handle, const char* desc) +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + GLint status = 0, log_length = 0; + glGetShaderiv(handle, GL_COMPILE_STATUS, &status); + glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &log_length); + if ((GLboolean)status == GL_FALSE) + fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to compile %s! With GLSL: %s\n", desc, bd->GlslVersionString); + if (log_length > 1) + { + ImVector buf; + buf.resize((int)(log_length + 1)); + glGetShaderInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin()); + fprintf(stderr, "%s\n", buf.begin()); + } + return (GLboolean)status == GL_TRUE; +} + +// If you get an error please report on GitHub. You may try different GL context version or GLSL version. +static bool CheckProgram(GLuint handle, const char* desc) +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + GLint status = 0, log_length = 0; + glGetProgramiv(handle, GL_LINK_STATUS, &status); + glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &log_length); + if ((GLboolean)status == GL_FALSE) + fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to link %s! With GLSL %s\n", desc, bd->GlslVersionString); + if (log_length > 1) + { + ImVector buf; + buf.resize((int)(log_length + 1)); + glGetProgramInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin()); + fprintf(stderr, "%s\n", buf.begin()); + } + return (GLboolean)status == GL_TRUE; +} + +bool ImGui_ImplOpenGL3_CreateDeviceObjects() +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + + // Backup GL state + GLint last_texture, last_array_buffer; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); + glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + GLint last_pixel_unpack_buffer = 0; + if (bd->GlVersion >= 210) { glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &last_pixel_unpack_buffer); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); } +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GLint last_vertex_array; + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array); +#endif + + // Parse GLSL version string + int glsl_version = 130; + sscanf(bd->GlslVersionString, "#version %d", &glsl_version); + + const GLchar* vertex_shader_glsl_120 = + "uniform mat4 ProjMtx;\n" + "attribute vec2 Position;\n" + "attribute vec2 UV;\n" + "attribute vec4 Color;\n" + "varying vec2 Frag_UV;\n" + "varying vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " Frag_UV = UV;\n" + " Frag_Color = Color;\n" + " gl_Position = ProjMtx * vec4(Position.xy,0,1);\n" + "}\n"; + + const GLchar* vertex_shader_glsl_130 = + "uniform mat4 ProjMtx;\n" + "in vec2 Position;\n" + "in vec2 UV;\n" + "in vec4 Color;\n" + "out vec2 Frag_UV;\n" + "out vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " Frag_UV = UV;\n" + " Frag_Color = Color;\n" + " gl_Position = ProjMtx * vec4(Position.xy,0,1);\n" + "}\n"; + + const GLchar* vertex_shader_glsl_300_es = + "precision highp float;\n" + "layout (location = 0) in vec2 Position;\n" + "layout (location = 1) in vec2 UV;\n" + "layout (location = 2) in vec4 Color;\n" + "uniform mat4 ProjMtx;\n" + "out vec2 Frag_UV;\n" + "out vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " Frag_UV = UV;\n" + " Frag_Color = Color;\n" + " gl_Position = ProjMtx * vec4(Position.xy,0,1);\n" + "}\n"; + + const GLchar* vertex_shader_glsl_410_core = + "layout (location = 0) in vec2 Position;\n" + "layout (location = 1) in vec2 UV;\n" + "layout (location = 2) in vec4 Color;\n" + "uniform mat4 ProjMtx;\n" + "out vec2 Frag_UV;\n" + "out vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " Frag_UV = UV;\n" + " Frag_Color = Color;\n" + " gl_Position = ProjMtx * vec4(Position.xy,0,1);\n" + "}\n"; + + const GLchar* fragment_shader_glsl_120 = + "#ifdef GL_ES\n" + " precision mediump float;\n" + "#endif\n" + "uniform sampler2D Texture;\n" + "varying vec2 Frag_UV;\n" + "varying vec4 Frag_Color;\n" + "void main()\n" + "{\n" + " gl_FragColor = Frag_Color * texture2D(Texture, Frag_UV.st);\n" + "}\n"; + + const GLchar* fragment_shader_glsl_130 = + "uniform sampler2D Texture;\n" + "in vec2 Frag_UV;\n" + "in vec4 Frag_Color;\n" + "out vec4 Out_Color;\n" + "void main()\n" + "{\n" + " Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" + "}\n"; + + const GLchar* fragment_shader_glsl_300_es = + "precision mediump float;\n" + "uniform sampler2D Texture;\n" + "in vec2 Frag_UV;\n" + "in vec4 Frag_Color;\n" + "layout (location = 0) out vec4 Out_Color;\n" + "void main()\n" + "{\n" + " Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" + "}\n"; + + const GLchar* fragment_shader_glsl_410_core = + "in vec2 Frag_UV;\n" + "in vec4 Frag_Color;\n" + "uniform sampler2D Texture;\n" + "layout (location = 0) out vec4 Out_Color;\n" + "void main()\n" + "{\n" + " Out_Color = Frag_Color * texture(Texture, Frag_UV.st);\n" + "}\n"; + + // Select shaders matching our GLSL versions + const GLchar* vertex_shader = nullptr; + const GLchar* fragment_shader = nullptr; + if (glsl_version < 130) + { + vertex_shader = vertex_shader_glsl_120; + fragment_shader = fragment_shader_glsl_120; + } + else if (glsl_version >= 410) + { + vertex_shader = vertex_shader_glsl_410_core; + fragment_shader = fragment_shader_glsl_410_core; + } + else if (glsl_version == 300) + { + vertex_shader = vertex_shader_glsl_300_es; + fragment_shader = fragment_shader_glsl_300_es; + } + else + { + vertex_shader = vertex_shader_glsl_130; + fragment_shader = fragment_shader_glsl_130; + } + + // Create shaders + const GLchar* vertex_shader_with_version[2] = { bd->GlslVersionString, vertex_shader }; + GLuint vert_handle; + GL_CALL(vert_handle = glCreateShader(GL_VERTEX_SHADER)); + glShaderSource(vert_handle, 2, vertex_shader_with_version, nullptr); + glCompileShader(vert_handle); + CheckShader(vert_handle, "vertex shader"); + + const GLchar* fragment_shader_with_version[2] = { bd->GlslVersionString, fragment_shader }; + GLuint frag_handle; + GL_CALL(frag_handle = glCreateShader(GL_FRAGMENT_SHADER)); + glShaderSource(frag_handle, 2, fragment_shader_with_version, nullptr); + glCompileShader(frag_handle); + CheckShader(frag_handle, "fragment shader"); + + // Link + bd->ShaderHandle = glCreateProgram(); + glAttachShader(bd->ShaderHandle, vert_handle); + glAttachShader(bd->ShaderHandle, frag_handle); + glLinkProgram(bd->ShaderHandle); + CheckProgram(bd->ShaderHandle, "shader program"); + + glDetachShader(bd->ShaderHandle, vert_handle); + glDetachShader(bd->ShaderHandle, frag_handle); + glDeleteShader(vert_handle); + glDeleteShader(frag_handle); + + bd->AttribLocationTex = glGetUniformLocation(bd->ShaderHandle, "Texture"); + bd->AttribLocationProjMtx = glGetUniformLocation(bd->ShaderHandle, "ProjMtx"); + bd->AttribLocationVtxPos = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Position"); + bd->AttribLocationVtxUV = (GLuint)glGetAttribLocation(bd->ShaderHandle, "UV"); + bd->AttribLocationVtxColor = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Color"); + + // Create buffers + glGenBuffers(1, &bd->VboHandle); + glGenBuffers(1, &bd->ElementsHandle); + + ImGui_ImplOpenGL3_CreateFontsTexture(); + + // Restore modified GL state + glBindTexture(GL_TEXTURE_2D, last_texture); + glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + if (bd->GlVersion >= 210) { glBindBuffer(GL_PIXEL_UNPACK_BUFFER, last_pixel_unpack_buffer); } +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + glBindVertexArray(last_vertex_array); +#endif + + return true; +} + +void ImGui_ImplOpenGL3_DestroyDeviceObjects() +{ + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->VboHandle) { glDeleteBuffers(1, &bd->VboHandle); bd->VboHandle = 0; } + if (bd->ElementsHandle) { glDeleteBuffers(1, &bd->ElementsHandle); bd->ElementsHandle = 0; } + if (bd->ShaderHandle) { glDeleteProgram(bd->ShaderHandle); bd->ShaderHandle = 0; } + ImGui_ImplOpenGL3_DestroyFontsTexture(); +} + +//----------------------------------------------------------------------------- + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/backends/imgui_impl_opengl3.h b/thirdparty/imgui/backends/imgui_impl_opengl3.h new file mode 100644 index 00000000..5de51cfd --- /dev/null +++ b/thirdparty/imgui/backends/imgui_impl_opengl3.h @@ -0,0 +1,66 @@ +// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline +// - Desktop GL: 2.x 3.x 4.x +// - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [x] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset) [Desktop OpenGL only!] + +// About WebGL/ES: +// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES. +// - This is done automatically on iOS, Android and Emscripten targets. +// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// About GLSL version: +// The 'glsl_version' initialization parameter should be nullptr (default) or a "#version XXX" string. +// On computer platform the GLSL version default to "#version 130". On OpenGL ES 3 platform it defaults to "#version 300 es" +// Only override if your GL version doesn't handle this GLSL version. See GLSL version table at the top of imgui_impl_opengl3.cpp. + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = nullptr); +IMGUI_IMPL_API void ImGui_ImplOpenGL3_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplOpenGL3_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data); + +// (Optional) Called by Init/NewFrame/Shutdown +IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateFontsTexture(); +IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyFontsTexture(); +IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateDeviceObjects(); +IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects(); + +// Configuration flags to add in your imconfig file: +//#define IMGUI_IMPL_OPENGL_ES2 // Enable ES 2 (Auto-detected on Emscripten) +//#define IMGUI_IMPL_OPENGL_ES3 // Enable ES 3 (Auto-detected on iOS/Android) + +// You can explicitly select GLES2 or GLES3 API by using one of the '#define IMGUI_IMPL_OPENGL_LOADER_XXX' in imconfig.h or compiler command-line. +#if !defined(IMGUI_IMPL_OPENGL_ES2) \ + && !defined(IMGUI_IMPL_OPENGL_ES3) + +// Try to detect GLES on matching platforms +#if defined(__APPLE__) +#include +#endif +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) || (defined(__ANDROID__)) +#define IMGUI_IMPL_OPENGL_ES3 // iOS, Android -> GL ES 3, "#version 300 es" +#elif defined(__EMSCRIPTEN__) || defined(__amigaos4__) +#define IMGUI_IMPL_OPENGL_ES2 // Emscripten -> GL ES 2, "#version 100" +#else +// Otherwise imgui_impl_opengl3_loader.h will be used. +#endif + +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/backends/imgui_impl_opengl3_loader.h b/thirdparty/imgui/backends/imgui_impl_opengl3_loader.h new file mode 100644 index 00000000..d6ffa5a2 --- /dev/null +++ b/thirdparty/imgui/backends/imgui_impl_opengl3_loader.h @@ -0,0 +1,916 @@ +//----------------------------------------------------------------------------- +// About imgui_impl_opengl3_loader.h: +// +// We embed our own OpenGL loader to not require user to provide their own or to have to use ours, +// which proved to be endless problems for users. +// Our loader is custom-generated, based on gl3w but automatically filtered to only include +// enums/functions that we use in our imgui_impl_opengl3.cpp source file in order to be small. +// +// YOU SHOULD NOT NEED TO INCLUDE/USE THIS DIRECTLY. THIS IS USED BY imgui_impl_opengl3.cpp ONLY. +// THE REST OF YOUR APP SHOULD USE A DIFFERENT GL LOADER: ANY GL LOADER OF YOUR CHOICE. +// +// IF YOU GET BUILD ERRORS IN THIS FILE (commonly macro redefinitions or function redefinitions): +// IT LIKELY MEANS THAT YOU ARE BUILDING 'imgui_impl_opengl3.cpp' OR INCLUDING 'imgui_impl_opengl3_loader.h' +// IN THE SAME COMPILATION UNIT AS ONE OF YOUR FILE WHICH IS USING A THIRD-PARTY OPENGL LOADER. +// (e.g. COULD HAPPEN IF YOU ARE DOING A UNITY/JUMBO BUILD, OR INCLUDING .CPP FILES FROM OTHERS) +// YOU SHOULD NOT BUILD BOTH IN THE SAME COMPILATION UNIT. +// BUT IF YOU REALLY WANT TO, you can '#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM' and imgui_impl_opengl3.cpp +// WILL NOT BE USING OUR LOADER, AND INSTEAD EXPECT ANOTHER/YOUR LOADER TO BE AVAILABLE IN THE COMPILATION UNIT. +// +// Regenerate with: +// python3 gl3w_gen.py --output ../imgui/backends/imgui_impl_opengl3_loader.h --ref ../imgui/backends/imgui_impl_opengl3.cpp ./extra_symbols.txt +// +// More info: +// https://github.com/dearimgui/gl3w_stripped +// https://github.com/ocornut/imgui/issues/4445 +//----------------------------------------------------------------------------- + +/* + * This file was generated with gl3w_gen.py, part of imgl3w + * (hosted at https://github.com/dearimgui/gl3w_stripped) + * + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __gl3w_h_ +#define __gl3w_h_ + +// Adapted from KHR/khrplatform.h to avoid including entire file. +#ifndef __khrplatform_h_ +typedef float khronos_float_t; +typedef signed char khronos_int8_t; +typedef unsigned char khronos_uint8_t; +typedef signed short int khronos_int16_t; +typedef unsigned short int khronos_uint16_t; +#ifdef _WIN64 +typedef signed long long int khronos_intptr_t; +typedef signed long long int khronos_ssize_t; +#else +typedef signed long int khronos_intptr_t; +typedef signed long int khronos_ssize_t; +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +typedef signed __int64 khronos_int64_t; +typedef unsigned __int64 khronos_uint64_t; +#elif (defined(__clang__) || defined(__GNUC__)) && (__cplusplus < 201100) +#include +typedef int64_t khronos_int64_t; +typedef uint64_t khronos_uint64_t; +#else +typedef signed long long khronos_int64_t; +typedef unsigned long long khronos_uint64_t; +#endif +#endif // __khrplatform_h_ + +#ifndef __gl_glcorearb_h_ +#define __gl_glcorearb_h_ 1 +#ifdef __cplusplus +extern "C" { +#endif +/* +** Copyright 2013-2020 The Khronos Group Inc. +** SPDX-License-Identifier: MIT +** +** This header is generated from the Khronos OpenGL / OpenGL ES XML +** API Registry. The current version of the Registry, generator scripts +** used to make the header, and the header can be found at +** https://github.com/KhronosGroup/OpenGL-Registry +*/ +#if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include +#endif +#ifndef APIENTRY +#define APIENTRY +#endif +#ifndef APIENTRYP +#define APIENTRYP APIENTRY * +#endif +#ifndef GLAPI +#define GLAPI extern +#endif +/* glcorearb.h is for use with OpenGL core profile implementations. +** It should should be placed in the same directory as gl.h and +** included as . +** +** glcorearb.h includes only APIs in the latest OpenGL core profile +** implementation together with APIs in newer ARB extensions which +** can be supported by the core profile. It does not, and never will +** include functionality removed from the core profile, such as +** fixed-function vertex and fragment processing. +** +** Do not #include both and either of or +** in the same source file. +*/ +/* Generated C header for: + * API: gl + * Profile: core + * Versions considered: .* + * Versions emitted: .* + * Default extensions included: glcore + * Additional extensions included: _nomatch_^ + * Extensions removed: _nomatch_^ + */ +#ifndef GL_VERSION_1_0 +typedef void GLvoid; +typedef unsigned int GLenum; + +typedef khronos_float_t GLfloat; +typedef int GLint; +typedef int GLsizei; +typedef unsigned int GLbitfield; +typedef double GLdouble; +typedef unsigned int GLuint; +typedef unsigned char GLboolean; +typedef khronos_uint8_t GLubyte; +#define GL_COLOR_BUFFER_BIT 0x00004000 +#define GL_FALSE 0 +#define GL_TRUE 1 +#define GL_TRIANGLES 0x0004 +#define GL_ONE 1 +#define GL_SRC_ALPHA 0x0302 +#define GL_ONE_MINUS_SRC_ALPHA 0x0303 +#define GL_FRONT 0x0404 +#define GL_BACK 0x0405 +#define GL_FRONT_AND_BACK 0x0408 +#define GL_POLYGON_MODE 0x0B40 +#define GL_CULL_FACE 0x0B44 +#define GL_DEPTH_TEST 0x0B71 +#define GL_STENCIL_TEST 0x0B90 +#define GL_VIEWPORT 0x0BA2 +#define GL_BLEND 0x0BE2 +#define GL_SCISSOR_BOX 0x0C10 +#define GL_SCISSOR_TEST 0x0C11 +#define GL_UNPACK_ROW_LENGTH 0x0CF2 +#define GL_PACK_ALIGNMENT 0x0D05 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_UNSIGNED_BYTE 0x1401 +#define GL_UNSIGNED_SHORT 0x1403 +#define GL_UNSIGNED_INT 0x1405 +#define GL_FLOAT 0x1406 +#define GL_RGBA 0x1908 +#define GL_FILL 0x1B02 +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 +#define GL_EXTENSIONS 0x1F03 +#define GL_LINEAR 0x2601 +#define GL_TEXTURE_MAG_FILTER 0x2800 +#define GL_TEXTURE_MIN_FILTER 0x2801 +#define GL_TEXTURE_WRAP_S 0x2802 +#define GL_TEXTURE_WRAP_T 0x2803 +#define GL_REPEAT 0x2901 +typedef void (APIENTRYP PFNGLPOLYGONMODEPROC) (GLenum face, GLenum mode); +typedef void (APIENTRYP PFNGLSCISSORPROC) (GLint x, GLint y, GLsizei width, GLsizei height); +typedef void (APIENTRYP PFNGLTEXPARAMETERIPROC) (GLenum target, GLenum pname, GLint param); +typedef void (APIENTRYP PFNGLTEXIMAGE2DPROC) (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels); +typedef void (APIENTRYP PFNGLCLEARPROC) (GLbitfield mask); +typedef void (APIENTRYP PFNGLCLEARCOLORPROC) (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); +typedef void (APIENTRYP PFNGLDISABLEPROC) (GLenum cap); +typedef void (APIENTRYP PFNGLENABLEPROC) (GLenum cap); +typedef void (APIENTRYP PFNGLFLUSHPROC) (void); +typedef void (APIENTRYP PFNGLPIXELSTOREIPROC) (GLenum pname, GLint param); +typedef void (APIENTRYP PFNGLREADPIXELSPROC) (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels); +typedef GLenum (APIENTRYP PFNGLGETERRORPROC) (void); +typedef void (APIENTRYP PFNGLGETINTEGERVPROC) (GLenum pname, GLint *data); +typedef const GLubyte *(APIENTRYP PFNGLGETSTRINGPROC) (GLenum name); +typedef GLboolean (APIENTRYP PFNGLISENABLEDPROC) (GLenum cap); +typedef void (APIENTRYP PFNGLVIEWPORTPROC) (GLint x, GLint y, GLsizei width, GLsizei height); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glPolygonMode (GLenum face, GLenum mode); +GLAPI void APIENTRY glScissor (GLint x, GLint y, GLsizei width, GLsizei height); +GLAPI void APIENTRY glTexParameteri (GLenum target, GLenum pname, GLint param); +GLAPI void APIENTRY glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels); +GLAPI void APIENTRY glClear (GLbitfield mask); +GLAPI void APIENTRY glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); +GLAPI void APIENTRY glDisable (GLenum cap); +GLAPI void APIENTRY glEnable (GLenum cap); +GLAPI void APIENTRY glFlush (void); +GLAPI void APIENTRY glPixelStorei (GLenum pname, GLint param); +GLAPI void APIENTRY glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels); +GLAPI GLenum APIENTRY glGetError (void); +GLAPI void APIENTRY glGetIntegerv (GLenum pname, GLint *data); +GLAPI const GLubyte *APIENTRY glGetString (GLenum name); +GLAPI GLboolean APIENTRY glIsEnabled (GLenum cap); +GLAPI void APIENTRY glViewport (GLint x, GLint y, GLsizei width, GLsizei height); +#endif +#endif /* GL_VERSION_1_0 */ +#ifndef GL_VERSION_1_1 +typedef khronos_float_t GLclampf; +typedef double GLclampd; +#define GL_TEXTURE_BINDING_2D 0x8069 +typedef void (APIENTRYP PFNGLDRAWELEMENTSPROC) (GLenum mode, GLsizei count, GLenum type, const void *indices); +typedef void (APIENTRYP PFNGLBINDTEXTUREPROC) (GLenum target, GLuint texture); +typedef void (APIENTRYP PFNGLDELETETEXTURESPROC) (GLsizei n, const GLuint *textures); +typedef void (APIENTRYP PFNGLGENTEXTURESPROC) (GLsizei n, GLuint *textures); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glDrawElements (GLenum mode, GLsizei count, GLenum type, const void *indices); +GLAPI void APIENTRY glBindTexture (GLenum target, GLuint texture); +GLAPI void APIENTRY glDeleteTextures (GLsizei n, const GLuint *textures); +GLAPI void APIENTRY glGenTextures (GLsizei n, GLuint *textures); +#endif +#endif /* GL_VERSION_1_1 */ +#ifndef GL_VERSION_1_2 +#define GL_CLAMP_TO_EDGE 0x812F +#endif /* GL_VERSION_1_2 */ +#ifndef GL_VERSION_1_3 +#define GL_TEXTURE0 0x84C0 +#define GL_ACTIVE_TEXTURE 0x84E0 +typedef void (APIENTRYP PFNGLACTIVETEXTUREPROC) (GLenum texture); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glActiveTexture (GLenum texture); +#endif +#endif /* GL_VERSION_1_3 */ +#ifndef GL_VERSION_1_4 +#define GL_BLEND_DST_RGB 0x80C8 +#define GL_BLEND_SRC_RGB 0x80C9 +#define GL_BLEND_DST_ALPHA 0x80CA +#define GL_BLEND_SRC_ALPHA 0x80CB +#define GL_FUNC_ADD 0x8006 +typedef void (APIENTRYP PFNGLBLENDFUNCSEPARATEPROC) (GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); +typedef void (APIENTRYP PFNGLBLENDEQUATIONPROC) (GLenum mode); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glBlendFuncSeparate (GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); +GLAPI void APIENTRY glBlendEquation (GLenum mode); +#endif +#endif /* GL_VERSION_1_4 */ +#ifndef GL_VERSION_1_5 +typedef khronos_ssize_t GLsizeiptr; +typedef khronos_intptr_t GLintptr; +#define GL_ARRAY_BUFFER 0x8892 +#define GL_ELEMENT_ARRAY_BUFFER 0x8893 +#define GL_ARRAY_BUFFER_BINDING 0x8894 +#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895 +#define GL_STREAM_DRAW 0x88E0 +typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer); +typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers); +typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers); +typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const void *data, GLenum usage); +typedef void (APIENTRYP PFNGLBUFFERSUBDATAPROC) (GLenum target, GLintptr offset, GLsizeiptr size, const void *data); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glBindBuffer (GLenum target, GLuint buffer); +GLAPI void APIENTRY glDeleteBuffers (GLsizei n, const GLuint *buffers); +GLAPI void APIENTRY glGenBuffers (GLsizei n, GLuint *buffers); +GLAPI void APIENTRY glBufferData (GLenum target, GLsizeiptr size, const void *data, GLenum usage); +GLAPI void APIENTRY glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const void *data); +#endif +#endif /* GL_VERSION_1_5 */ +#ifndef GL_VERSION_2_0 +typedef char GLchar; +typedef khronos_int16_t GLshort; +typedef khronos_int8_t GLbyte; +typedef khronos_uint16_t GLushort; +#define GL_BLEND_EQUATION_RGB 0x8009 +#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622 +#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623 +#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624 +#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625 +#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645 +#define GL_BLEND_EQUATION_ALPHA 0x883D +#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A +#define GL_FRAGMENT_SHADER 0x8B30 +#define GL_VERTEX_SHADER 0x8B31 +#define GL_COMPILE_STATUS 0x8B81 +#define GL_LINK_STATUS 0x8B82 +#define GL_INFO_LOG_LENGTH 0x8B84 +#define GL_CURRENT_PROGRAM 0x8B8D +#define GL_UPPER_LEFT 0x8CA2 +typedef void (APIENTRYP PFNGLBLENDEQUATIONSEPARATEPROC) (GLenum modeRGB, GLenum modeAlpha); +typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader); +typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader); +typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void); +typedef GLuint (APIENTRYP PFNGLCREATESHADERPROC) (GLenum type); +typedef void (APIENTRYP PFNGLDELETEPROGRAMPROC) (GLuint program); +typedef void (APIENTRYP PFNGLDELETESHADERPROC) (GLuint shader); +typedef void (APIENTRYP PFNGLDETACHSHADERPROC) (GLuint program, GLuint shader); +typedef void (APIENTRYP PFNGLDISABLEVERTEXATTRIBARRAYPROC) (GLuint index); +typedef void (APIENTRYP PFNGLENABLEVERTEXATTRIBARRAYPROC) (GLuint index); +typedef GLint (APIENTRYP PFNGLGETATTRIBLOCATIONPROC) (GLuint program, const GLchar *name); +typedef void (APIENTRYP PFNGLGETPROGRAMIVPROC) (GLuint program, GLenum pname, GLint *params); +typedef void (APIENTRYP PFNGLGETPROGRAMINFOLOGPROC) (GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog); +typedef void (APIENTRYP PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params); +typedef void (APIENTRYP PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog); +typedef GLint (APIENTRYP PFNGLGETUNIFORMLOCATIONPROC) (GLuint program, const GLchar *name); +typedef void (APIENTRYP PFNGLGETVERTEXATTRIBIVPROC) (GLuint index, GLenum pname, GLint *params); +typedef void (APIENTRYP PFNGLGETVERTEXATTRIBPOINTERVPROC) (GLuint index, GLenum pname, void **pointer); +typedef GLboolean (APIENTRYP PFNGLISPROGRAMPROC) (GLuint program); +typedef void (APIENTRYP PFNGLLINKPROGRAMPROC) (GLuint program); +typedef void (APIENTRYP PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length); +typedef void (APIENTRYP PFNGLUSEPROGRAMPROC) (GLuint program); +typedef void (APIENTRYP PFNGLUNIFORM1IPROC) (GLint location, GLint v0); +typedef void (APIENTRYP PFNGLUNIFORMMATRIX4FVPROC) (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); +typedef void (APIENTRYP PFNGLVERTEXATTRIBPOINTERPROC) (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glBlendEquationSeparate (GLenum modeRGB, GLenum modeAlpha); +GLAPI void APIENTRY glAttachShader (GLuint program, GLuint shader); +GLAPI void APIENTRY glCompileShader (GLuint shader); +GLAPI GLuint APIENTRY glCreateProgram (void); +GLAPI GLuint APIENTRY glCreateShader (GLenum type); +GLAPI void APIENTRY glDeleteProgram (GLuint program); +GLAPI void APIENTRY glDeleteShader (GLuint shader); +GLAPI void APIENTRY glDetachShader (GLuint program, GLuint shader); +GLAPI void APIENTRY glDisableVertexAttribArray (GLuint index); +GLAPI void APIENTRY glEnableVertexAttribArray (GLuint index); +GLAPI GLint APIENTRY glGetAttribLocation (GLuint program, const GLchar *name); +GLAPI void APIENTRY glGetProgramiv (GLuint program, GLenum pname, GLint *params); +GLAPI void APIENTRY glGetProgramInfoLog (GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog); +GLAPI void APIENTRY glGetShaderiv (GLuint shader, GLenum pname, GLint *params); +GLAPI void APIENTRY glGetShaderInfoLog (GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog); +GLAPI GLint APIENTRY glGetUniformLocation (GLuint program, const GLchar *name); +GLAPI void APIENTRY glGetVertexAttribiv (GLuint index, GLenum pname, GLint *params); +GLAPI void APIENTRY glGetVertexAttribPointerv (GLuint index, GLenum pname, void **pointer); +GLAPI GLboolean APIENTRY glIsProgram (GLuint program); +GLAPI void APIENTRY glLinkProgram (GLuint program); +GLAPI void APIENTRY glShaderSource (GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length); +GLAPI void APIENTRY glUseProgram (GLuint program); +GLAPI void APIENTRY glUniform1i (GLint location, GLint v0); +GLAPI void APIENTRY glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); +GLAPI void APIENTRY glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); +#endif +#endif /* GL_VERSION_2_0 */ +#ifndef GL_VERSION_2_1 +#define GL_PIXEL_UNPACK_BUFFER 0x88EC +#define GL_PIXEL_UNPACK_BUFFER_BINDING 0x88EF +#endif /* GL_VERSION_2_1 */ +#ifndef GL_VERSION_3_0 +typedef khronos_uint16_t GLhalf; +#define GL_MAJOR_VERSION 0x821B +#define GL_MINOR_VERSION 0x821C +#define GL_NUM_EXTENSIONS 0x821D +#define GL_FRAMEBUFFER_SRGB 0x8DB9 +#define GL_VERTEX_ARRAY_BINDING 0x85B5 +typedef void (APIENTRYP PFNGLGETBOOLEANI_VPROC) (GLenum target, GLuint index, GLboolean *data); +typedef void (APIENTRYP PFNGLGETINTEGERI_VPROC) (GLenum target, GLuint index, GLint *data); +typedef const GLubyte *(APIENTRYP PFNGLGETSTRINGIPROC) (GLenum name, GLuint index); +typedef void (APIENTRYP PFNGLBINDVERTEXARRAYPROC) (GLuint array); +typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *arrays); +typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI const GLubyte *APIENTRY glGetStringi (GLenum name, GLuint index); +GLAPI void APIENTRY glBindVertexArray (GLuint array); +GLAPI void APIENTRY glDeleteVertexArrays (GLsizei n, const GLuint *arrays); +GLAPI void APIENTRY glGenVertexArrays (GLsizei n, GLuint *arrays); +#endif +#endif /* GL_VERSION_3_0 */ +#ifndef GL_VERSION_3_1 +#define GL_VERSION_3_1 1 +#define GL_PRIMITIVE_RESTART 0x8F9D +#endif /* GL_VERSION_3_1 */ +#ifndef GL_VERSION_3_2 +#define GL_VERSION_3_2 1 +typedef struct __GLsync *GLsync; +typedef khronos_uint64_t GLuint64; +typedef khronos_int64_t GLint64; +#define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002 +#define GL_CONTEXT_PROFILE_MASK 0x9126 +typedef void (APIENTRYP PFNGLDRAWELEMENTSBASEVERTEXPROC) (GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex); +typedef void (APIENTRYP PFNGLGETINTEGER64I_VPROC) (GLenum target, GLuint index, GLint64 *data); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glDrawElementsBaseVertex (GLenum mode, GLsizei count, GLenum type, const void *indices, GLint basevertex); +#endif +#endif /* GL_VERSION_3_2 */ +#ifndef GL_VERSION_3_3 +#define GL_VERSION_3_3 1 +#define GL_SAMPLER_BINDING 0x8919 +typedef void (APIENTRYP PFNGLBINDSAMPLERPROC) (GLuint unit, GLuint sampler); +#ifdef GL_GLEXT_PROTOTYPES +GLAPI void APIENTRY glBindSampler (GLuint unit, GLuint sampler); +#endif +#endif /* GL_VERSION_3_3 */ +#ifndef GL_VERSION_4_1 +typedef void (APIENTRYP PFNGLGETFLOATI_VPROC) (GLenum target, GLuint index, GLfloat *data); +typedef void (APIENTRYP PFNGLGETDOUBLEI_VPROC) (GLenum target, GLuint index, GLdouble *data); +#endif /* GL_VERSION_4_1 */ +#ifndef GL_VERSION_4_3 +typedef void (APIENTRY *GLDEBUGPROC)(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam); +#endif /* GL_VERSION_4_3 */ +#ifndef GL_VERSION_4_5 +#define GL_CLIP_ORIGIN 0x935C +typedef void (APIENTRYP PFNGLGETTRANSFORMFEEDBACKI_VPROC) (GLuint xfb, GLenum pname, GLuint index, GLint *param); +typedef void (APIENTRYP PFNGLGETTRANSFORMFEEDBACKI64_VPROC) (GLuint xfb, GLenum pname, GLuint index, GLint64 *param); +#endif /* GL_VERSION_4_5 */ +#ifndef GL_ARB_bindless_texture +typedef khronos_uint64_t GLuint64EXT; +#endif /* GL_ARB_bindless_texture */ +#ifndef GL_ARB_cl_event +struct _cl_context; +struct _cl_event; +#endif /* GL_ARB_cl_event */ +#ifndef GL_ARB_clip_control +#define GL_ARB_clip_control 1 +#endif /* GL_ARB_clip_control */ +#ifndef GL_ARB_debug_output +typedef void (APIENTRY *GLDEBUGPROCARB)(GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam); +#endif /* GL_ARB_debug_output */ +#ifndef GL_EXT_EGL_image_storage +typedef void *GLeglImageOES; +#endif /* GL_EXT_EGL_image_storage */ +#ifndef GL_EXT_direct_state_access +typedef void (APIENTRYP PFNGLGETFLOATI_VEXTPROC) (GLenum pname, GLuint index, GLfloat *params); +typedef void (APIENTRYP PFNGLGETDOUBLEI_VEXTPROC) (GLenum pname, GLuint index, GLdouble *params); +typedef void (APIENTRYP PFNGLGETPOINTERI_VEXTPROC) (GLenum pname, GLuint index, void **params); +typedef void (APIENTRYP PFNGLGETVERTEXARRAYINTEGERI_VEXTPROC) (GLuint vaobj, GLuint index, GLenum pname, GLint *param); +typedef void (APIENTRYP PFNGLGETVERTEXARRAYPOINTERI_VEXTPROC) (GLuint vaobj, GLuint index, GLenum pname, void **param); +#endif /* GL_EXT_direct_state_access */ +#ifndef GL_NV_draw_vulkan_image +typedef void (APIENTRY *GLVULKANPROCNV)(void); +#endif /* GL_NV_draw_vulkan_image */ +#ifndef GL_NV_gpu_shader5 +typedef khronos_int64_t GLint64EXT; +#endif /* GL_NV_gpu_shader5 */ +#ifndef GL_NV_vertex_buffer_unified_memory +typedef void (APIENTRYP PFNGLGETINTEGERUI64I_VNVPROC) (GLenum value, GLuint index, GLuint64EXT *result); +#endif /* GL_NV_vertex_buffer_unified_memory */ +#ifdef __cplusplus +} +#endif +#endif + +#ifndef GL3W_API +#define GL3W_API +#endif + +#ifndef __gl_h_ +#define __gl_h_ +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define GL3W_OK 0 +#define GL3W_ERROR_INIT -1 +#define GL3W_ERROR_LIBRARY_OPEN -2 +#define GL3W_ERROR_OPENGL_VERSION -3 + +typedef void (*GL3WglProc)(void); +typedef GL3WglProc (*GL3WGetProcAddressProc)(const char *proc); + +/* gl3w api */ +GL3W_API int imgl3wInit(void); +GL3W_API int imgl3wInit2(GL3WGetProcAddressProc proc); +GL3W_API int imgl3wIsSupported(int major, int minor); +GL3W_API GL3WglProc imgl3wGetProcAddress(const char *proc); + +/* gl3w internal state */ +union ImGL3WProcs { + GL3WglProc ptr[59]; + struct { + PFNGLACTIVETEXTUREPROC ActiveTexture; + PFNGLATTACHSHADERPROC AttachShader; + PFNGLBINDBUFFERPROC BindBuffer; + PFNGLBINDSAMPLERPROC BindSampler; + PFNGLBINDTEXTUREPROC BindTexture; + PFNGLBINDVERTEXARRAYPROC BindVertexArray; + PFNGLBLENDEQUATIONPROC BlendEquation; + PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate; + PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate; + PFNGLBUFFERDATAPROC BufferData; + PFNGLBUFFERSUBDATAPROC BufferSubData; + PFNGLCLEARPROC Clear; + PFNGLCLEARCOLORPROC ClearColor; + PFNGLCOMPILESHADERPROC CompileShader; + PFNGLCREATEPROGRAMPROC CreateProgram; + PFNGLCREATESHADERPROC CreateShader; + PFNGLDELETEBUFFERSPROC DeleteBuffers; + PFNGLDELETEPROGRAMPROC DeleteProgram; + PFNGLDELETESHADERPROC DeleteShader; + PFNGLDELETETEXTURESPROC DeleteTextures; + PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays; + PFNGLDETACHSHADERPROC DetachShader; + PFNGLDISABLEPROC Disable; + PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray; + PFNGLDRAWELEMENTSPROC DrawElements; + PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex; + PFNGLENABLEPROC Enable; + PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray; + PFNGLFLUSHPROC Flush; + PFNGLGENBUFFERSPROC GenBuffers; + PFNGLGENTEXTURESPROC GenTextures; + PFNGLGENVERTEXARRAYSPROC GenVertexArrays; + PFNGLGETATTRIBLOCATIONPROC GetAttribLocation; + PFNGLGETERRORPROC GetError; + PFNGLGETINTEGERVPROC GetIntegerv; + PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog; + PFNGLGETPROGRAMIVPROC GetProgramiv; + PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog; + PFNGLGETSHADERIVPROC GetShaderiv; + PFNGLGETSTRINGPROC GetString; + PFNGLGETSTRINGIPROC GetStringi; + PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation; + PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv; + PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv; + PFNGLISENABLEDPROC IsEnabled; + PFNGLISPROGRAMPROC IsProgram; + PFNGLLINKPROGRAMPROC LinkProgram; + PFNGLPIXELSTOREIPROC PixelStorei; + PFNGLPOLYGONMODEPROC PolygonMode; + PFNGLREADPIXELSPROC ReadPixels; + PFNGLSCISSORPROC Scissor; + PFNGLSHADERSOURCEPROC ShaderSource; + PFNGLTEXIMAGE2DPROC TexImage2D; + PFNGLTEXPARAMETERIPROC TexParameteri; + PFNGLUNIFORM1IPROC Uniform1i; + PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv; + PFNGLUSEPROGRAMPROC UseProgram; + PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer; + PFNGLVIEWPORTPROC Viewport; + } gl; +}; + +GL3W_API extern union ImGL3WProcs imgl3wProcs; + +/* OpenGL functions */ +#define glActiveTexture imgl3wProcs.gl.ActiveTexture +#define glAttachShader imgl3wProcs.gl.AttachShader +#define glBindBuffer imgl3wProcs.gl.BindBuffer +#define glBindSampler imgl3wProcs.gl.BindSampler +#define glBindTexture imgl3wProcs.gl.BindTexture +#define glBindVertexArray imgl3wProcs.gl.BindVertexArray +#define glBlendEquation imgl3wProcs.gl.BlendEquation +#define glBlendEquationSeparate imgl3wProcs.gl.BlendEquationSeparate +#define glBlendFuncSeparate imgl3wProcs.gl.BlendFuncSeparate +#define glBufferData imgl3wProcs.gl.BufferData +#define glBufferSubData imgl3wProcs.gl.BufferSubData +#define glClear imgl3wProcs.gl.Clear +#define glClearColor imgl3wProcs.gl.ClearColor +#define glCompileShader imgl3wProcs.gl.CompileShader +#define glCreateProgram imgl3wProcs.gl.CreateProgram +#define glCreateShader imgl3wProcs.gl.CreateShader +#define glDeleteBuffers imgl3wProcs.gl.DeleteBuffers +#define glDeleteProgram imgl3wProcs.gl.DeleteProgram +#define glDeleteShader imgl3wProcs.gl.DeleteShader +#define glDeleteTextures imgl3wProcs.gl.DeleteTextures +#define glDeleteVertexArrays imgl3wProcs.gl.DeleteVertexArrays +#define glDetachShader imgl3wProcs.gl.DetachShader +#define glDisable imgl3wProcs.gl.Disable +#define glDisableVertexAttribArray imgl3wProcs.gl.DisableVertexAttribArray +#define glDrawElements imgl3wProcs.gl.DrawElements +#define glDrawElementsBaseVertex imgl3wProcs.gl.DrawElementsBaseVertex +#define glEnable imgl3wProcs.gl.Enable +#define glEnableVertexAttribArray imgl3wProcs.gl.EnableVertexAttribArray +#define glFlush imgl3wProcs.gl.Flush +#define glGenBuffers imgl3wProcs.gl.GenBuffers +#define glGenTextures imgl3wProcs.gl.GenTextures +#define glGenVertexArrays imgl3wProcs.gl.GenVertexArrays +#define glGetAttribLocation imgl3wProcs.gl.GetAttribLocation +#define glGetError imgl3wProcs.gl.GetError +#define glGetIntegerv imgl3wProcs.gl.GetIntegerv +#define glGetProgramInfoLog imgl3wProcs.gl.GetProgramInfoLog +#define glGetProgramiv imgl3wProcs.gl.GetProgramiv +#define glGetShaderInfoLog imgl3wProcs.gl.GetShaderInfoLog +#define glGetShaderiv imgl3wProcs.gl.GetShaderiv +#define glGetString imgl3wProcs.gl.GetString +#define glGetStringi imgl3wProcs.gl.GetStringi +#define glGetUniformLocation imgl3wProcs.gl.GetUniformLocation +#define glGetVertexAttribPointerv imgl3wProcs.gl.GetVertexAttribPointerv +#define glGetVertexAttribiv imgl3wProcs.gl.GetVertexAttribiv +#define glIsEnabled imgl3wProcs.gl.IsEnabled +#define glIsProgram imgl3wProcs.gl.IsProgram +#define glLinkProgram imgl3wProcs.gl.LinkProgram +#define glPixelStorei imgl3wProcs.gl.PixelStorei +#define glPolygonMode imgl3wProcs.gl.PolygonMode +#define glReadPixels imgl3wProcs.gl.ReadPixels +#define glScissor imgl3wProcs.gl.Scissor +#define glShaderSource imgl3wProcs.gl.ShaderSource +#define glTexImage2D imgl3wProcs.gl.TexImage2D +#define glTexParameteri imgl3wProcs.gl.TexParameteri +#define glUniform1i imgl3wProcs.gl.Uniform1i +#define glUniformMatrix4fv imgl3wProcs.gl.UniformMatrix4fv +#define glUseProgram imgl3wProcs.gl.UseProgram +#define glVertexAttribPointer imgl3wProcs.gl.VertexAttribPointer +#define glViewport imgl3wProcs.gl.Viewport + +#ifdef __cplusplus +} +#endif + +#endif + +#ifdef IMGL3W_IMPL +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define GL3W_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include + +static HMODULE libgl; +typedef PROC(__stdcall* GL3WglGetProcAddr)(LPCSTR); +static GL3WglGetProcAddr wgl_get_proc_address; + +static int open_libgl(void) +{ + libgl = LoadLibraryA("opengl32.dll"); + if (!libgl) + return GL3W_ERROR_LIBRARY_OPEN; + wgl_get_proc_address = (GL3WglGetProcAddr)GetProcAddress(libgl, "wglGetProcAddress"); + return GL3W_OK; +} + +static void close_libgl(void) { FreeLibrary(libgl); } +static GL3WglProc get_proc(const char *proc) +{ + GL3WglProc res; + res = (GL3WglProc)wgl_get_proc_address(proc); + if (!res) + res = (GL3WglProc)GetProcAddress(libgl, proc); + return res; +} +#elif defined(__APPLE__) +#include + +static void *libgl; +static int open_libgl(void) +{ + libgl = dlopen("/System/Library/Frameworks/OpenGL.framework/OpenGL", RTLD_LAZY | RTLD_LOCAL); + if (!libgl) + return GL3W_ERROR_LIBRARY_OPEN; + return GL3W_OK; +} + +static void close_libgl(void) { dlclose(libgl); } + +static GL3WglProc get_proc(const char *proc) +{ + GL3WglProc res; + *(void **)(&res) = dlsym(libgl, proc); + return res; +} +#else +#include + +static void* libgl; // OpenGL library +static void* libglx; // GLX library +static void* libegl; // EGL library +static GL3WGetProcAddressProc gl_get_proc_address; + +static void close_libgl(void) +{ + if (libgl) { + dlclose(libgl); + libgl = NULL; + } + if (libegl) { + dlclose(libegl); + libegl = NULL; + } + if (libglx) { + dlclose(libglx); + libglx = NULL; + } +} + +static int is_library_loaded(const char* name, void** lib) +{ + *lib = dlopen(name, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + return *lib != NULL; +} + +static int open_libs(void) +{ + // On Linux we have two APIs to get process addresses: EGL and GLX. + // EGL is supported under both X11 and Wayland, whereas GLX is X11-specific. + + libgl = NULL; + libegl = NULL; + libglx = NULL; + + // First check what's already loaded, the windowing library might have + // already loaded either EGL or GLX and we want to use the same one. + + if (is_library_loaded("libEGL.so.1", &libegl) || + is_library_loaded("libGLX.so.0", &libglx)) { + libgl = dlopen("libOpenGL.so.0", RTLD_LAZY | RTLD_LOCAL); + if (libgl) + return GL3W_OK; + else + close_libgl(); + } + + if (is_library_loaded("libGL.so", &libgl)) + return GL3W_OK; + if (is_library_loaded("libGL.so.1", &libgl)) + return GL3W_OK; + if (is_library_loaded("libGL.so.3", &libgl)) + return GL3W_OK; + + // Neither is already loaded, so we have to load one. Try EGL first + // because it is supported under both X11 and Wayland. + + // Load OpenGL + EGL + libgl = dlopen("libOpenGL.so.0", RTLD_LAZY | RTLD_LOCAL); + libegl = dlopen("libEGL.so.1", RTLD_LAZY | RTLD_LOCAL); + if (libgl && libegl) + return GL3W_OK; + else + close_libgl(); + + // Fall back to legacy libGL, which includes GLX + // While most systems use libGL.so.1, NetBSD seems to use that libGL.so.3. See https://github.com/ocornut/imgui/issues/6983 + libgl = dlopen("libGL.so", RTLD_LAZY | RTLD_LOCAL); + if (!libgl) + libgl = dlopen("libGL.so.1", RTLD_LAZY | RTLD_LOCAL); + if (!libgl) + libgl = dlopen("libGL.so.3", RTLD_LAZY | RTLD_LOCAL); + + if (libgl) + return GL3W_OK; + + return GL3W_ERROR_LIBRARY_OPEN; +} + +static int open_libgl(void) +{ + int res = open_libs(); + if (res) + return res; + + if (libegl) + *(void**)(&gl_get_proc_address) = dlsym(libegl, "eglGetProcAddress"); + else if (libglx) + *(void**)(&gl_get_proc_address) = dlsym(libglx, "glXGetProcAddressARB"); + else + *(void**)(&gl_get_proc_address) = dlsym(libgl, "glXGetProcAddressARB"); + + if (!gl_get_proc_address) { + close_libgl(); + return GL3W_ERROR_LIBRARY_OPEN; + } + + return GL3W_OK; +} + +static GL3WglProc get_proc(const char* proc) +{ + GL3WglProc res = NULL; + + // Before EGL version 1.5, eglGetProcAddress doesn't support querying core + // functions and may return a dummy function if we try, so try to load the + // function from the GL library directly first. + if (libegl) + *(void**)(&res) = dlsym(libgl, proc); + + if (!res) + res = gl_get_proc_address(proc); + + if (!libegl && !res) + *(void**)(&res) = dlsym(libgl, proc); + + return res; +} +#endif + +static struct { int major, minor; } version; + +static int parse_version(void) +{ + if (!glGetIntegerv) + return GL3W_ERROR_INIT; + glGetIntegerv(GL_MAJOR_VERSION, &version.major); + glGetIntegerv(GL_MINOR_VERSION, &version.minor); + if (version.major == 0 && version.minor == 0) + { + // Query GL_VERSION in desktop GL 2.x, the string will start with "." + if (const char* gl_version = (const char*)glGetString(GL_VERSION)) + sscanf(gl_version, "%d.%d", &version.major, &version.minor); + } + if (version.major < 2) + return GL3W_ERROR_OPENGL_VERSION; + return GL3W_OK; +} + +static void load_procs(GL3WGetProcAddressProc proc); + +int imgl3wInit(void) +{ + int res = open_libgl(); + if (res) + return res; + atexit(close_libgl); + return imgl3wInit2(get_proc); +} + +int imgl3wInit2(GL3WGetProcAddressProc proc) +{ + load_procs(proc); + return parse_version(); +} + +int imgl3wIsSupported(int major, int minor) +{ + if (major < 2) + return 0; + if (version.major == major) + return version.minor >= minor; + return version.major >= major; +} + +GL3WglProc imgl3wGetProcAddress(const char *proc) { return get_proc(proc); } + +static const char *proc_names[] = { + "glActiveTexture", + "glAttachShader", + "glBindBuffer", + "glBindSampler", + "glBindTexture", + "glBindVertexArray", + "glBlendEquation", + "glBlendEquationSeparate", + "glBlendFuncSeparate", + "glBufferData", + "glBufferSubData", + "glClear", + "glClearColor", + "glCompileShader", + "glCreateProgram", + "glCreateShader", + "glDeleteBuffers", + "glDeleteProgram", + "glDeleteShader", + "glDeleteTextures", + "glDeleteVertexArrays", + "glDetachShader", + "glDisable", + "glDisableVertexAttribArray", + "glDrawElements", + "glDrawElementsBaseVertex", + "glEnable", + "glEnableVertexAttribArray", + "glFlush", + "glGenBuffers", + "glGenTextures", + "glGenVertexArrays", + "glGetAttribLocation", + "glGetError", + "glGetIntegerv", + "glGetProgramInfoLog", + "glGetProgramiv", + "glGetShaderInfoLog", + "glGetShaderiv", + "glGetString", + "glGetStringi", + "glGetUniformLocation", + "glGetVertexAttribPointerv", + "glGetVertexAttribiv", + "glIsEnabled", + "glIsProgram", + "glLinkProgram", + "glPixelStorei", + "glPolygonMode", + "glReadPixels", + "glScissor", + "glShaderSource", + "glTexImage2D", + "glTexParameteri", + "glUniform1i", + "glUniformMatrix4fv", + "glUseProgram", + "glVertexAttribPointer", + "glViewport", +}; + +GL3W_API union ImGL3WProcs imgl3wProcs; + +static void load_procs(GL3WGetProcAddressProc proc) +{ + size_t i; + for (i = 0; i < GL3W_ARRAY_SIZE(proc_names); i++) + imgl3wProcs.ptr[i] = proc(proc_names[i]); +} + +#ifdef __cplusplus +} +#endif +#endif diff --git a/thirdparty/imgui/imconfig.h b/thirdparty/imgui/imconfig.h new file mode 100644 index 00000000..a1e29e84 --- /dev/null +++ b/thirdparty/imgui/imconfig.h @@ -0,0 +1,141 @@ +//----------------------------------------------------------------------------- +// DEAR IMGUI COMPILE-TIME OPTIONS +// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. +// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. +//----------------------------------------------------------------------------- +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. +// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using. +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export +//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import +//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden + +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names. +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//---- Disable all of Dear ImGui or don't implement standard windows/tools. +// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty. + +//---- Don't implement some functions to reduce linkage requirements. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. +//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert. +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Enable Test Engine / Automation features. +//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details. + +//---- Include imgui_user.h at the end of imgui.h as a convenience +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. +//#define IMGUI_INCLUDE_IMGUI_USER_H +//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" + +//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support. +//#define IMGUI_USE_BGRA_PACKED_COLOR + +//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate. +//#define IMGUI_USE_LEGACY_CRC32_ADLER + +//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 + +//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined. +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined. + +//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. +//#define IMGUI_USE_STB_SPRINTF + +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +//#define IMGUI_ENABLE_FREETYPE + +//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT) +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions. +// - Both require headers to be available in the include path + program to be linked with the library code (not provided). +// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) +//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE + +//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. +// This will be inlined as part of ImVec2 and ImVec4 class declarations. +/* +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ +//---- ...Or use Dear ImGui's own very basic math operators. +//#define IMGUI_DEFINE_MATH_OPERATORS + +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. +// Read about ImGuiBackendFlags_RendererHasVtxOffset for details. +//#define ImDrawIdx unsigned int + +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) +//struct ImDrawList; +//struct ImDrawCmd; +//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); +//#define ImDrawCallback MyImDrawCallback + +//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase) +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) +//#define IM_DEBUG_BREAK IM_ASSERT(0) +//#define IM_DEBUG_BREAK __debugbreak() + +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID + +//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files) +/* +namespace ImGui +{ + void MyFunction(const char* name, MyMatrix44* mtx); +} +*/ diff --git a/thirdparty/imgui/imgui.cpp b/thirdparty/imgui/imgui.cpp new file mode 100644 index 00000000..7ec19227 --- /dev/null +++ b/thirdparty/imgui/imgui.cpp @@ -0,0 +1,17061 @@ +// dear imgui, v1.91.9b +// (main code and documentation) + +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. + +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues?q=label%3Agallery (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Copyright (c) 2014-2025 Omar Cornut +// Developed by Omar Cornut and every direct or indirect contributors to the GitHub. +// See LICENSE.txt for copyright and licensing details (standard MIT License). +// This library is free but needs your support to sustain development and maintenance. +// Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts. +// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Funding +// Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + +// It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. +// Note that 'ImGui::' being a namespace, you can add functions into the namespace from your own source files, without +// modifying imgui.h or imgui.cpp. You may include imgui_internal.h to access internal data structures, but it doesn't +// come with any guarantee of forward compatibility. Discussing your changes on the GitHub Issue Tracker may lead you +// to a better solution or official support for them. + +/* + +Index of this file: + +DOCUMENTATION + +- MISSION STATEMENT +- CONTROLS GUIDE +- PROGRAMMER GUIDE + - READ FIRST + - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + - HOW A SIMPLE APPLICATION MAY LOOK LIKE + - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE +- API BREAKING CHANGES (read me when you update!) +- FREQUENTLY ASKED QUESTIONS (FAQ) + - Read all answers online: https://www.dearimgui.com/faq, or in docs/FAQ.md (with a Markdown viewer) + +CODE +(search for "[SECTION]" in the code to find them) + +// [SECTION] INCLUDES +// [SECTION] FORWARD DECLARATIONS +// [SECTION] CONTEXT AND MEMORY ALLOCATORS +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +// [SECTION] MISC HELPERS/UTILITIES (File functions) +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// [SECTION] ImGuiStorage +// [SECTION] ImGuiTextFilter +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex +// [SECTION] ImGuiListClipper +// [SECTION] STYLING +// [SECTION] RENDER HELPERS +// [SECTION] INITIALIZATION, SHUTDOWN +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] ID STACK +// [SECTION] INPUTS +// [SECTION] ERROR CHECKING, STATE RECOVERY +// [SECTION] ITEM SUBMISSION +// [SECTION] LAYOUT +// [SECTION] SCROLLING +// [SECTION] TOOLTIPS +// [SECTION] POPUPS +// [SECTION] WINDOW FOCUS +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +// [SECTION] DRAG AND DROP +// [SECTION] LOGGING/CAPTURING +// [SECTION] SETTINGS +// [SECTION] LOCALIZATION +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +// [SECTION] PLATFORM DEPENDENT HELPERS +// [SECTION] METRICS/DEBUGGER WINDOW +// [SECTION] DEBUG LOG WINDOW +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) + +*/ + +//----------------------------------------------------------------------------- +// DOCUMENTATION +//----------------------------------------------------------------------------- + +/* + + MISSION STATEMENT + ================= + + - Easy to use to create code-driven and data-driven tools. + - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools. + - Easy to hack and improve. + - Minimize setup and maintenance. + - Minimize state storage on user side. + - Minimize state synchronization. + - Portable, minimize dependencies, run on target (consoles, phones, etc.). + - Efficient runtime and memory consumption. + + Designed primarily for developers and content-creators, not the typical end-user! + Some of the current weaknesses (which we aim to address in the future) includes: + + - Doesn't look fancy. + - Limited layout features, intricate layouts are typically crafted in code. + + + CONTROLS GUIDE + ============== + + - MOUSE CONTROLS + - Mouse wheel: Scroll vertically. + - SHIFT+Mouse wheel: Scroll horizontally. + - Click [X]: Close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click ^, Double-Click title: Collapse window. + - Drag on corner/border: Resize window (double-click to auto fit window to its contents). + - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true). + - Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack). + + - TEXT EDITOR + - Hold SHIFT or Drag Mouse: Select text. + - CTRL+Left/Right: Word jump. + - CTRL+Shift+Left/Right: Select words. + - CTRL+A or Double-Click: Select All. + - CTRL+X, CTRL+C, CTRL+V: Use OS clipboard. + - CTRL+Z Undo. + - CTRL+Y or CTRL+Shift+Z: Redo. + - ESCAPE: Revert text to its original value. + - On OSX, controls are automatically adjusted to match standard OSX text editing 2ts and behaviors. + + - KEYBOARD CONTROLS + - Basic: + - Tab, SHIFT+Tab Cycle through text editable fields. + - CTRL+Tab, CTRL+Shift+Tab Cycle through windows. + - CTRL+Click Input text into a Slider or Drag widget. + - Extended features with `io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard`: + - Tab, SHIFT+Tab: Cycle through every items. + - Arrow keys Move through items using directional navigation. Tweak value. + - Arrow keys + Alt, Shift Tweak slower, tweak faster (when using arrow keys). + - Enter Activate item (prefer text input when possible). + - Space Activate item (prefer tweaking with arrows when possible). + - Escape Deactivate item, leave child window, close popup. + - Page Up, Page Down Previous page, next page. + - Home, End Scroll to top, scroll to bottom. + - Alt Toggle between scrolling layer and menu layer. + - CTRL+Tab then Ctrl+Arrows Move window. Hold SHIFT to resize instead of moving. + - Output when ImGuiConfigFlags_NavEnableKeyboard set, + - io.WantCaptureKeyboard flag is set when keyboard is claimed. + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (usually goes to back false when mouse is used). + + - GAMEPAD CONTROLS + - Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. + - Particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse! + - Download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets + - Backend support: backend needs to: + - Set 'io.BackendFlags |= ImGuiBackendFlags_HasGamepad' + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys. + - For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly. + Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.). + - If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing, + with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved. + + - REMOTE INPUTS SHARING & MOUSE EMULATION + - PS4/PS5 users: Consider emulating a mouse cursor with DualShock touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + run examples/libs/synergy/uSynergy.c (on your console/tablet/phone app) + in order to share your PC mouse/keyboard. + - See https://github.com/ocornut/imgui/wiki/Useful-Extensions#remoting for other remoting solutions. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the io.ConfigNavMoveSetMousePos flag. + Enabling io.ConfigNavMoveSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs Dear ImGui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved. + When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that. + (If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, Dear ImGui will misbehave as it will see your mouse moving back & forth!) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) + + + PROGRAMMER GUIDE + ================ + + READ FIRST + ---------- + - Remember to check the wonderful Wiki (https://github.com/ocornut/imgui/wiki) + - Your code creates the UI every frame of your application loop, if your code doesn't run the UI is gone! + The UI can be highly dynamic, there are no construction or destruction steps, less superfluous + data retention on your side, less state duplication, less state synchronization, fewer bugs. + - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + Or browse https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html for interactive web version. + - The library is designed to be built from sources. Avoid pre-compiled binaries and packaged versions. See imconfig.h to configure your build. + - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori). + You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki. + - Dear ImGui is a "single pass" rasterizing implementation of the IMGUI paradigm, aimed at ease of use and high-performances. + For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI, + where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches. + - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. + - Please make sure you have asserts enabled (IM_ASSERT redirects to assert() by default, but can be redirected). + If you get an assert, read the messages and comments around the assert. + - This codebase aims to be highly optimized: + - A typical idle frame should never call malloc/free. + - We rely on a maximum of constant-time or O(N) algorithms. Limiting searches/scans as much as possible. + - We put particular energy in making sure performances are decent with typical "Debug" build settings as well. + Which mean we tend to avoid over-relying on "zero-cost abstraction" as they aren't zero-cost at all. + - This codebase aims to be both highly opinionated and highly flexible: + - This code works because of the things it choose to solve or not solve. + - C++: this is a pragmatic C-ish codebase: we don't use fancy C++ features, we don't include C++ headers, + and ImGui:: is a namespace. We rarely use member functions (and when we did, I am mostly regretting it now). + This is to increase compatibility, increase maintainability and facilitate use from other languages. + - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. + See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. + We can can optionally export math operators for ImVec2/ImVec4 using IMGUI_DEFINE_MATH_OPERATORS, which we use internally. + - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction + (so don't use ImVector in your code or at our own risk!). + - Building: We don't use nor mandate a build system for the main library. + This is in an effort to ensure that it works in the real world aka with any esoteric build setup. + This is also because providing a build system for the main library would be of little-value. + The build problems are almost never coming from the main library but from specific backends. + + + HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + ---------------------------------------------- + - Update submodule or copy/overwrite every file. + - About imconfig.h: + - You may modify your copy of imconfig.h, in this case don't overwrite it. + - or you may locally branch to modify imconfig.h and merge/rebase latest. + - or you may '#define IMGUI_USER_CONFIG "my_config_file.h"' globally from your build system to + specify a custom path for your imconfig.h file and instead not have to modify the default one. + + - Overwrite all the sources files except for imconfig.h (if you have modified your copy of imconfig.h) + - Or maintain your own branch where you have imconfig.h modified as a top-most commit which you can regularly rebase over "master". + - You can also use '#define IMGUI_USER_CONFIG "my_config_file.h" to redirect configuration to your own file. + - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. + If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed + from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will + likely be a comment about it. Please report any issue to the GitHub page! + - To find out usage of old API, you can add '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in your configuration file. + - Try to keep your copy of Dear ImGui reasonably up to date! + + + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + --------------------------------------------------------------- + - See https://github.com/ocornut/imgui/wiki/Getting-Started. + - Run and study the examples and demo in imgui_demo.cpp to get acquainted with the library. + - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder. + - Add the Dear ImGui source files + selected backend source files to your projects or using your preferred build system. + It is recommended you build and statically link the .cpp files as part of your project and NOT as a shared library (DLL). + - You can later customize the imconfig.h file to tweak some compile-time behavior, such as integrating Dear ImGui types with your own maths types. + - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. + - Dear ImGui never touches or knows about your GPU state. The only function that knows about GPU is the draw function that you provide. + Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" + phases of your own application. All rendering information is stored into command-lists that you will retrieve after calling ImGui::Render(). + - Refer to the backends and demo applications in the examples/ folder for instruction on how to setup your code. + - If you are running over a standard OS with a common graphics API, you should be able to use unmodified imgui_impl_*** files from the examples/ folder. + + + HOW A SIMPLE APPLICATION MAY LOOK LIKE + -------------------------------------- + EXHIBIT 1: USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder). + The sub-folders in examples/ contain examples applications following this structure. + + // Application init: create a dear imgui context, setup some options, load fonts + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. + + // Initialize helper Platform and Renderer backends (here we are using imgui_impl_win32.cpp and imgui_impl_dx11.cpp) + ImGui_ImplWin32_Init(hwnd); + ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); + + // Application main loop + while (true) + { + // Feed inputs to dear imgui, start new frame + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + // Any application code here + ImGui::Text("Hello, world!"); + + // Render dear imgui into screen + ImGui::Render(); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + g_pSwapChain->Present(1, 0); + } + + // Shutdown + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + + EXHIBIT 2: IMPLEMENTING CUSTOM BACKEND / CUSTOM ENGINE + + // Application init: create a dear imgui context, setup some options, load fonts + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + // TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard' to enable keyboard controls. + // TODO: Fill optional fields of the io structure later. + // TODO: Load TTF/OTF fonts if you don't want to use the default font. + + // Build and load the texture atlas into a texture + // (In the examples/ app this is usually done within the ImGui_ImplXXX_Init() function from one of the demo Renderer) + int width, height; + unsigned char* pixels = nullptr; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + + // At this point you've got the texture data and you need to upload that to your graphic system: + // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'. + // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ for details about ImTextureID. + MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA32) + io.Fonts->SetTexID((void*)texture); + + // Application main loop + while (true) + { + // Setup low-level inputs, e.g. on Win32: calling GetKeyboardState(), or write to those fields from your Windows message handlers, etc. + // (In the examples/ app this is usually done within the ImGui_ImplXXX_NewFrame() function from one of the demo Platform Backends) + io.DeltaTime = 1.0f/60.0f; // set the time elapsed since the previous frame (in seconds) + io.DisplaySize.x = 1920.0f; // set the current display width + io.DisplaySize.y = 1280.0f; // set the current display height here + io.AddMousePosEvent(mouse_x, mouse_y); // update mouse position + io.AddMouseButtonEvent(0, mouse_b[0]); // update mouse button states + io.AddMouseButtonEvent(1, mouse_b[1]); // update mouse button states + + // Call NewFrame(), after this point you can use ImGui::* functions anytime + // (So you want to try calling NewFrame() as early as you can in your main loop to be able to use Dear ImGui everywhere) + ImGui::NewFrame(); + + // Most of your application code here + ImGui::Text("Hello, world!"); + MyGameUpdate(); // may use any Dear ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); + MyGameRender(); // may use any Dear ImGui functions as well! + + // Render dear imgui, swap buffers + // (You want to try calling EndFrame/Render as late as you can, to be able to use Dear ImGui in your own game rendering code) + ImGui::EndFrame(); + ImGui::Render(); + ImDrawData* draw_data = ImGui::GetDrawData(); + MyImGuiRenderFunction(draw_data); + SwapBuffers(); + } + + // Shutdown + ImGui::DestroyContext(); + + To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application, + you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this. + + + HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE + --------------------------------------------- + The backends in impl_impl_XXX.cpp files contain many working implementations of a rendering function. + + void MyImGuiRenderFunction(ImDrawData* draw_data) + { + // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + // TODO: Setup texture sampling state: sample with bilinear filtering (NOT point/nearest filtering). Use 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines;' to allow point/nearest filtering. + // TODO: Setup viewport covering draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize + // TODO: Setup orthographic projection matrix cover draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize + // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + ImVec2 clip_off = draw_data->DisplayPos; + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by Dear ImGui + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by Dear ImGui + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + pcmd->UserCallback(cmd_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); + ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // We are using scissoring to clip some objects. All low-level graphics API should support it. + // - If your engine doesn't support scissoring yet, you may ignore this at first. You will get some small glitches + // (some elements visible outside their bounds) but you can fix that once everything else works! + // - Clipping coordinates are provided in imgui coordinates space: + // - For a given viewport, draw_data->DisplayPos == viewport->Pos and draw_data->DisplaySize == viewport->Size + // - In a single viewport application, draw_data->DisplayPos == (0,0) and draw_data->DisplaySize == io.DisplaySize, but always use GetMainViewport()->Pos/Size instead of hardcoding those values. + // - In the interest of supporting multi-viewport applications (see 'docking' branch on github), + // always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space. + // - Note that pcmd->ClipRect contains Min+Max bounds. Some graphics API may use Min+Max, other may use Min+Size (size being Max-Min) + MyEngineSetScissor(clip_min.x, clip_min.y, clip_max.x, clip_max.y); + + // The texture for the draw call is specified by pcmd->GetTexID(). + // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture((MyTexture*)pcmd->GetTexID()); + + // Render 'pcmd->ElemCount/3' indexed triangles. + // By default the indices ImDrawIdx are 16-bit, you can change them to 32-bit in imconfig.h if your engine doesn't support 16-bit indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset, vtx_buffer, pcmd->VtxOffset); + } + } + } + } + + + API BREAKING CHANGES + ==================== + + Occasionally introducing changes that are breaking the API. We try to make the breakage minor and easy to fix. + Below is a change-log of API breaking changes only. If you are using one of the functions listed, expect to have to fix some code. + When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. + You can read releases logs https://github.com/ocornut/imgui/releases for more details. + + - 2025/03/05 (1.91.9) - BeginMenu(): Internals: reworked mangling of menu windows to use "###Menu_00" etc. instead of "##Menu_00", allowing them to also store the menu name before it. This shouldn't affect code unless directly accessing menu window from their mangled name. + - 2025/02/27 (1.91.9) - Image(): removed 'tint_col' and 'border_col' parameter from Image() function. Added ImageWithBg() replacement. (#8131, #8238) + - old: void Image (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1), ImVec4 tint_col = (1,1,1,1), ImVec4 border_col = (0,0,0,0)); + - new: void Image (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1)); + - new: void ImageWithBg(ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1), ImVec4 bg_col = (0,0,0,0), ImVec4 tint_col = (1,1,1,1)); + - TL;DR: 'border_col' had misleading side-effect on layout, 'bg_col' was missing, parameter order couldn't be consistent with ImageButton(). + - new behavior always use ImGuiCol_Border color + style.ImageBorderSize / ImGuiStyleVar_ImageBorderSize. + - old behavior altered border size (and therefore layout) based on border color's alpha, which caused variety of problems + old behavior a fixed 1.0f for border size which was not tweakable. + - kept legacy signature (will obsolete), which mimics the old behavior, but uses Max(1.0f, style.ImageBorderSize) when border_col is specified. + - added ImageWithBg() function which has both 'bg_col' (which was missing) and 'tint_col'. It was impossible to add 'bg_col' to Image() with a parameter order consistent with other functions, so we decided to remove 'tint_col' and introduce ImageWithBg(). + - 2025/02/25 (1.91.9) - internals: fonts: ImFontAtlas::ConfigData[] has been renamed to ImFontAtlas::Sources[]. ImFont::ConfigData[], ConfigDataCount has been renamed to Sources[], SourcesCount. + - 2025/02/06 (1.91.9) - renamed ImFontConfig::GlyphExtraSpacing.x to ImFontConfig::GlyphExtraAdvanceX. + - 2025/01/22 (1.91.8) - removed ImGuiColorEditFlags_AlphaPreview (made value 0): it is now the default behavior. + prior to 1.91.8: alpha was made opaque in the preview by default _unless_ using ImGuiColorEditFlags_AlphaPreview. We now display the preview as transparent by default. You can use ImGuiColorEditFlags_AlphaOpaque to use old behavior. + the new flags (ImGuiColorEditFlags_AlphaOpaque, ImGuiColorEditFlags_AlphaNoBg + existing ImGuiColorEditFlags_AlphaPreviewHalf) may be combined better and allow finer controls: + - 2025/01/14 (1.91.7) - renamed ImGuiTreeNodeFlags_SpanTextWidth to ImGuiTreeNodeFlags_SpanLabelWidth for consistency with other names. Kept redirection enum (will obsolete). (#6937) + - 2024/11/27 (1.91.6) - changed CRC32 table from CRC32-adler to CRC32c polynomial in order to be compatible with the result of SSE 4.2 instructions. + As a result, old .ini data may be partially lost (docking and tables information particularly). + Because some users have crafted and storing .ini data as a way to workaround limitations of the docking API, we are providing a '#define IMGUI_USE_LEGACY_CRC32_ADLER' compile-time option to keep using old CRC32 tables if you cannot afford invalidating old .ini data. + - 2024/11/06 (1.91.5) - commented/obsoleted out pre-1.87 IO system (equivalent to using IMGUI_DISABLE_OBSOLETE_KEYIO or IMGUI_DISABLE_OBSOLETE_FUNCTIONS before) + - io.KeyMap[] and io.KeysDown[] are removed (obsoleted February 2022). + - io.NavInputs[] and ImGuiNavInput are removed (obsoleted July 2022). + - pre-1.87 backends are not supported: + - backends need to call io.AddKeyEvent(), io.AddMouseEvent() instead of writing to io.KeysDown[], io.MouseDown[] fields. + - backends need to call io.AddKeyAnalogEvent() for gamepad values instead of writing to io.NavInputs[] fields. + - for more reference: + - read 1.87 and 1.88 part of this section or read Changelog for 1.87 and 1.88. + - read https://github.com/ocornut/imgui/issues/4921 + - if you have trouble updating a very old codebase using legacy backend-specific key codes: consider updating to 1.91.4 first, then #define IMGUI_DISABLE_OBSOLETE_KEYIO, then update to latest. + - obsoleted ImGuiKey_COUNT (it is unusually error-prone/misleading since valid keys don't start at 0). probably use ImGuiKey_NamedKey_BEGIN/ImGuiKey_NamedKey_END? + - fonts: removed const qualifiers from most font functions in prevision for upcoming font improvements. + - 2024/10/18 (1.91.4) - renamed ImGuiCol_NavHighlight to ImGuiCol_NavCursor (for consistency with newly exposed and reworked features). Kept inline redirection enum (will obsolete). + - 2024/10/14 (1.91.4) - moved ImGuiConfigFlags_NavEnableSetMousePos to standalone io.ConfigNavMoveSetMousePos bool. + moved ImGuiConfigFlags_NavNoCaptureKeyboard to standalone io.ConfigNavCaptureKeyboard bool (note the inverted value!). + kept legacy names (will obsolete) + code that copies settings once the first time. Dynamically changing the old value won't work. Switch to using the new value! + - 2024/10/10 (1.91.4) - the typedef for ImTextureID now defaults to ImU64 instead of void*. (#1641) + this removes the requirement to redefine it for backends which are e.g. storing descriptor sets or other 64-bits structures when building on 32-bits archs. It therefore simplify various building scripts/helpers. + you may have compile-time issues if you were casting to 'void*' instead of 'ImTextureID' when passing your types to functions taking ImTextureID values, e.g. ImGui::Image(). + in doubt it is almost always better to do an intermediate intptr_t cast, since it allows casting any pointer/integer type without warning: + - May warn: ImGui::Image((void*)MyTextureData, ...); + - May warn: ImGui::Image((void*)(intptr_t)MyTextureData, ...); + - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData), ...); + - note that you can always define ImTextureID to be your own high-level structures (with dedicated constructors) if you like. + - 2024/10/03 (1.91.3) - drags: treat v_min==v_max as a valid clamping range when != 0.0f. Zero is a still special value due to legacy reasons, unless using ImGuiSliderFlags_ClampZeroRange. (#7968, #3361, #76) + - drags: extended behavior of ImGuiSliderFlags_AlwaysClamp to include _ClampZeroRange. It considers v_min==v_max==0.0f as a valid clamping range (aka edits not allowed). + although unlikely, it you wish to only clamp on text input but want v_min==v_max==0.0f to mean unclamped drags, you can use _ClampOnInput instead of _AlwaysClamp. (#7968, #3361, #76) + - 2024/09/10 (1.91.2) - internals: using multiple overlayed ButtonBehavior() with same ID will now have io.ConfigDebugHighlightIdConflicts=true feature emit a warning. (#8030) + it was one of the rare case where using same ID is legal. workarounds: (1) use single ButtonBehavior() call with multiple _MouseButton flags, or (2) surround the calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag() + - 2024/08/23 (1.91.1) - renamed ImGuiChildFlags_Border to ImGuiChildFlags_Borders for consistency. kept inline redirection flag. + - 2024/08/22 (1.91.1) - moved some functions from ImGuiIO to ImGuiPlatformIO structure: + - io.GetClipboardTextFn -> platform_io.Platform_GetClipboardTextFn + changed 'void* user_data' to 'ImGuiContext* ctx'. Pull your user data from platform_io.ClipboardUserData. + - io.SetClipboardTextFn -> platform_io.Platform_SetClipboardTextFn + same as above line. + - io.PlatformOpenInShellFn -> platform_io.Platform_OpenInShellFn (#7660) + - io.PlatformSetImeDataFn -> platform_io.Platform_SetImeDataFn + - io.PlatformLocaleDecimalPoint -> platform_io.Platform_LocaleDecimalPoint (#7389, #6719, #2278) + - access those via GetPlatformIO() instead of GetIO(). + some were introduced very recently and often automatically setup by core library and backends, so for those we are exceptionally not maintaining a legacy redirection symbol. + - commented the old ImageButton() signature obsoleted in 1.89 (~August 2022). As a reminder: + - old ImageButton() before 1.89 used ImTextureId as item id (created issue with e.g. multiple buttons in same scope, transient texture id values, opaque computation of ID) + - new ImageButton() since 1.89 requires an explicit 'const char* str_id' + - old ImageButton() before 1.89 had frame_padding' override argument. + - new ImageButton() since 1.89 always use style.FramePadding, which you can freely override with PushStyleVar()/PopStyleVar(). + - 2024/07/25 (1.91.0) - obsoleted GetContentRegionMax(), GetWindowContentRegionMin() and GetWindowContentRegionMax(). (see #7838 on GitHub for more info) + you should never need those functions. you can do everything with GetCursorScreenPos() and GetContentRegionAvail() in a more simple way. + - instead of: GetWindowContentRegionMax().x - GetCursorPos().x + - you can use: GetContentRegionAvail().x + - instead of: GetWindowContentRegionMax().x + GetWindowPos().x + - you can use: GetCursorScreenPos().x + GetContentRegionAvail().x // when called from left edge of window + - instead of: GetContentRegionMax() + - you can use: GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos() // right edge in local coordinates + - instead of: GetWindowContentRegionMax().x - GetWindowContentRegionMin().x + - you can use: GetContentRegionAvail() // when called from left edge of window + - 2024/07/15 (1.91.0) - renamed ImGuiSelectableFlags_DontClosePopups to ImGuiSelectableFlags_NoAutoClosePopups. (#1379, #1468, #2200, #4936, #5216, #7302, #7573) + (internals: also renamed ImGuiItemFlags_SelectableDontClosePopup into ImGuiItemFlags_AutoClosePopups with inverted behaviors) + - 2024/07/15 (1.91.0) - obsoleted PushButtonRepeat()/PopButtonRepeat() in favor of using new PushItemFlag(ImGuiItemFlags_ButtonRepeat, ...)/PopItemFlag(). + - 2024/07/02 (1.91.0) - commented out obsolete ImGuiModFlags (renamed to ImGuiKeyChord in 1.89). (#4921, #456) + - commented out obsolete ImGuiModFlags_XXX values (renamed to ImGuiMod_XXX in 1.89). (#4921, #456) + - ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl, ImGuiModFlags_Shift -> ImGuiMod_Shift etc. + - 2024/07/02 (1.91.0) - IO, IME: renamed platform IME hook and added explicit context for consistency and future-proofness. + - old: io.SetPlatformImeDataFn(ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - new: io.PlatformSetImeDataFn(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - 2024/06/21 (1.90.9) - BeginChild: added ImGuiChildFlags_NavFlattened as a replacement for the window flag ImGuiWindowFlags_NavFlattened: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_NavFlattened); + - new: BeginChild("Name", size, ImGuiChildFlags_NavFlattened, 0); + - 2024/06/21 (1.90.9) - io: ClearInputKeys() (first exposed in 1.89.8) doesn't clear mouse data, newly added ClearInputMouse() does. + - 2024/06/20 (1.90.9) - renamed ImGuiDragDropFlags_SourceAutoExpirePayload to ImGuiDragDropFlags_PayloadAutoExpire. + - 2024/06/18 (1.90.9) - style: renamed ImGuiCol_TabActive -> ImGuiCol_TabSelected, ImGuiCol_TabUnfocused -> ImGuiCol_TabDimmed, ImGuiCol_TabUnfocusedActive -> ImGuiCol_TabDimmedSelected. + - 2024/06/10 (1.90.9) - removed old nested structure: renaming ImGuiStorage::ImGuiStoragePair type to ImGuiStoragePair (simpler for many languages). + - 2024/06/06 (1.90.8) - reordered ImGuiInputTextFlags values. This should not be breaking unless you are using generated headers that have values not matching the main library. + - 2024/06/06 (1.90.8) - removed 'ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft', was mostly unused and misleading. + - 2024/05/27 (1.90.7) - commented out obsolete symbols marked obsolete in 1.88 (May 2022): + - old: CaptureKeyboardFromApp(bool) + - new: SetNextFrameWantCaptureKeyboard(bool) + - old: CaptureMouseFromApp(bool) + - new: SetNextFrameWantCaptureMouse(bool) + - 2024/05/22 (1.90.7) - inputs (internals): renamed ImGuiKeyOwner_None to ImGuiKeyOwner_NoOwner, to make use more explicit and reduce confusion with the default it is a non-zero value and cannot be the default value (never made public, but disclosing as I expect a few users caught on owner-aware inputs). + - inputs (internals): renamed ImGuiInputFlags_RouteGlobalLow -> ImGuiInputFlags_RouteGlobal, ImGuiInputFlags_RouteGlobal -> ImGuiInputFlags_RouteGlobalOverFocused, ImGuiInputFlags_RouteGlobalHigh -> ImGuiInputFlags_RouteGlobalHighest. + - inputs (internals): Shortcut(), SetShortcutRouting(): swapped last two parameters order in function signatures: + - old: Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id = 0, ImGuiInputFlags flags = 0); + - new: Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0, ImGuiID owner_id = 0); + - inputs (internals): owner-aware versions of IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(): swapped last two parameters order in function signatures. + - old: IsKeyPressed(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); + - old: IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + for various reasons those changes makes sense. They are being made because making some of those API public. + only past users of imgui_internal.h with the extra parameters will be affected. Added asserts for valid flags in various functions to detect _some_ misuses, BUT NOT ALL. + - 2024/05/16 (1.90.7) - inputs: on macOS X, Cmd and Ctrl keys are now automatically swapped by io.AddKeyEvent() as this naturally align with how macOS X uses those keys. + - it shouldn't really affect you unless you had custom shortcut swapping in place for macOS X apps. + - removed ImGuiMod_Shortcut which was previously dynamically remapping to Ctrl or Cmd/Super. It is now unnecessary to specific cross-platform idiomatic shortcuts. (#2343, #4084, #5923, #456) + - 2024/05/14 (1.90.7) - backends: SDL_Renderer2 and SDL_Renderer3 backend now take a SDL_Renderer* in their RenderDrawData() functions. + - 2024/04/18 (1.90.6) - TreeNode: Fixed a layout inconsistency when using an empty/hidden label followed by a SameLine() call. (#7505, #282) + - old: TreeNode("##Hidden"); SameLine(); Text("Hello"); // <-- This was actually incorrect! BUT appeared to look ok with the default style where ItemSpacing.x == FramePadding.x * 2 (it didn't look aligned otherwise). + - new: TreeNode("##Hidden"); SameLine(0, 0); Text("Hello"); // <-- This is correct for all styles values. + with the fix, IF you were successfully using TreeNode("")+SameLine(); you will now have extra spacing between your TreeNode and the following item. + You'll need to change the SameLine() call to SameLine(0,0) to remove this extraneous spacing. This seemed like the more sensible fix that's not making things less consistent. + (Note: when using this idiom you are likely to also use ImGuiTreeNodeFlags_SpanAvailWidth). + - 2024/03/18 (1.90.5) - merged the radius_x/radius_y parameters in ImDrawList::AddEllipse(), AddEllipseFilled() and PathEllipticalArcTo() into a single ImVec2 parameter. Exceptionally, because those functions were added in 1.90, we are not adding inline redirection functions. The transition is easy and should affect few users. (#2743, #7417) + - 2024/03/08 (1.90.5) - inputs: more formally obsoleted GetKeyIndex() when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is set. It has been unnecessary and a no-op since 1.87 (it returns the same value as passed when used with a 1.87+ backend using io.AddKeyEvent() function). (#4921) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - 2024/01/15 (1.90.2) - commented out obsolete ImGuiIO::ImeWindowHandle marked obsolete in 1.87, favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2023/12/19 (1.90.1) - commented out obsolete ImGuiKey_KeyPadEnter redirection to ImGuiKey_KeypadEnter. + - 2023/11/06 (1.90.1) - removed CalcListClipping() marked obsolete in 1.86. Prefer using ImGuiListClipper which can return non-contiguous ranges. + - 2023/11/05 (1.90.1) - imgui_freetype: commented out ImGuiFreeType::BuildFontAtlas() obsoleted in 1.81. prefer using #define IMGUI_ENABLE_FREETYPE or see commented code for manual calls. + - 2023/11/05 (1.90.1) - internals,columns: commented out legacy ImGuiColumnsFlags_XXX symbols redirecting to ImGuiOldColumnsFlags_XXX, obsoleted from imgui_internal.h in 1.80. + - 2023/11/09 (1.90.0) - removed IM_OFFSETOF() macro in favor of using offsetof() available in C++11. Kept redirection define (will obsolete). + - 2023/11/07 (1.90.0) - removed BeginChildFrame()/EndChildFrame() in favor of using BeginChild() with the ImGuiChildFlags_FrameStyle flag. kept inline redirection function (will obsolete). + those functions were merely PushStyle/PopStyle helpers, the removal isn't so much motivated by needing to add the feature in BeginChild(), but by the necessity to avoid BeginChildFrame() signature mismatching BeginChild() signature and features. + - 2023/11/02 (1.90.0) - BeginChild: upgraded 'bool border = true' parameter to 'ImGuiChildFlags flags' type, added ImGuiChildFlags_Border equivalent. As with our prior "bool-to-flags" API updates, the ImGuiChildFlags_Border value is guaranteed to be == true forever to ensure a smoother transition, meaning all existing calls will still work. + - old: BeginChild("Name", size, true) + - new: BeginChild("Name", size, ImGuiChildFlags_Border) + - old: BeginChild("Name", size, false) + - new: BeginChild("Name", size) or BeginChild("Name", 0) or BeginChild("Name", size, ImGuiChildFlags_None) + **AMEND FROM THE FUTURE: from 1.91.1, 'ImGuiChildFlags_Border' is called 'ImGuiChildFlags_Borders'** + - 2023/11/02 (1.90.0) - BeginChild: added child-flag ImGuiChildFlags_AlwaysUseWindowPadding as a replacement for the window-flag ImGuiWindowFlags_AlwaysUseWindowPadding: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_AlwaysUseWindowPadding); + - new: BeginChild("Name", size, ImGuiChildFlags_AlwaysUseWindowPadding, 0); + - 2023/09/27 (1.90.0) - io: removed io.MetricsActiveAllocations introduced in 1.63. Same as 'g.DebugMemAllocCount - g.DebugMemFreeCount' (still displayed in Metrics, unlikely to be accessed by end-user). + - 2023/09/26 (1.90.0) - debug tools: Renamed ShowStackToolWindow() ("Stack Tool") to ShowIDStackToolWindow() ("ID Stack Tool"), as earlier name was misleading. Kept inline redirection function. (#4631) + - 2023/09/15 (1.90.0) - ListBox, Combo: changed signature of "name getter" callback in old one-liner ListBox()/Combo() apis. kept inline redirection function (will obsolete). + - old: bool Combo(const char* label, int* current_item, bool (*getter)(void* user_data, int idx, const char** out_text), ...) + - new: bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - old: bool ListBox(const char* label, int* current_item, bool (*getting)(void* user_data, int idx, const char** out_text), ...); + - new: bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - 2023/09/08 (1.90.0) - commented out obsolete redirecting functions: + - GetWindowContentRegionWidth() -> use GetWindowContentRegionMax().x - GetWindowContentRegionMin().x. Consider that generally 'GetContentRegionAvail().x' is more useful. + - ImDrawCornerFlags_XXX -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + grep commented names in sources. + - commented out runtime support for hardcoded ~0 or 0x01..0x0F rounding flags values for AddRect()/AddRectFilled()/PathRect()/AddImageRounded() -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + - 2023/08/25 (1.89.9) - clipper: Renamed IncludeRangeByIndices() (also called ForceDisplayRangeByIndices() before 1.89.6) to IncludeItemsByIndex(). Kept inline redirection function. Sorry! + - 2023/07/12 (1.89.8) - ImDrawData: CmdLists now owned, changed from ImDrawList** to ImVector. Majority of users shouldn't be affected, but you cannot compare to NULL nor reassign manually anymore. Instead use AddDrawList(). (#6406, #4879, #1878) + - 2023/06/28 (1.89.7) - overlapping items: obsoleted 'SetItemAllowOverlap()' (called after item) in favor of calling 'SetNextItemAllowOverlap()' (called before item). 'SetItemAllowOverlap()' didn't and couldn't work reliably since 1.89 (2022-11-15). + - 2023/06/28 (1.89.7) - overlapping items: renamed 'ImGuiTreeNodeFlags_AllowItemOverlap' to 'ImGuiTreeNodeFlags_AllowOverlap', 'ImGuiSelectableFlags_AllowItemOverlap' to 'ImGuiSelectableFlags_AllowOverlap'. Kept redirecting enums (will obsolete). + - 2023/06/28 (1.89.7) - overlapping items: IsItemHovered() now by default return false when querying an item using AllowOverlap mode which is being overlapped. Use ImGuiHoveredFlags_AllowWhenOverlappedByItem to revert to old behavior. + - 2023/06/28 (1.89.7) - overlapping items: Selectable and TreeNode don't allow overlap when active so overlapping widgets won't appear as hovered. While this fixes a common small visual issue, it also means that calling IsItemHovered() after a non-reactive elements - e.g. Text() - overlapping an active one may fail if you don't use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem). (#6610) + - 2023/06/20 (1.89.7) - moved io.HoverDelayShort/io.HoverDelayNormal to style.HoverDelayShort/style.HoverDelayNormal. As the fields were added in 1.89 and expected to be left unchanged by most users, or only tweaked once during app initialization, we are exceptionally accepting the breakage. + - 2023/05/30 (1.89.6) - backends: renamed "imgui_impl_sdlrenderer.cpp" to "imgui_impl_sdlrenderer2.cpp" and "imgui_impl_sdlrenderer.h" to "imgui_impl_sdlrenderer2.h". This is in prevision for the future release of SDL3. + - 2023/05/22 (1.89.6) - listbox: commented out obsolete/redirecting functions that were marked obsolete more than two years ago: + - ListBoxHeader() -> use BeginListBox() (note how two variants of ListBoxHeader() existed. Check commented versions in imgui.h for reference) + - ListBoxFooter() -> use EndListBox() + - 2023/05/15 (1.89.6) - clipper: commented out obsolete redirection constructor 'ImGuiListClipper(int items_count, float items_height = -1.0f)' that was marked obsolete in 1.79. Use default constructor + clipper.Begin(). + - 2023/05/15 (1.89.6) - clipper: renamed ImGuiListClipper::ForceDisplayRangeByIndices() to ImGuiListClipper::IncludeRangeByIndices(). + - 2023/03/14 (1.89.4) - commented out redirecting enums/functions names that were marked obsolete two years ago: + - ImGuiSliderFlags_ClampOnInput -> use ImGuiSliderFlags_AlwaysClamp + - ImGuiInputTextFlags_AlwaysInsertMode -> use ImGuiInputTextFlags_AlwaysOverwrite + - ImDrawList::AddBezierCurve() -> use ImDrawList::AddBezierCubic() + - ImDrawList::PathBezierCurveTo() -> use ImDrawList::PathBezierCubicCurveTo() + - 2023/03/09 (1.89.4) - renamed PushAllowKeyboardFocus()/PopAllowKeyboardFocus() to PushTabStop()/PopTabStop(). Kept inline redirection functions (will obsolete). + - 2023/03/09 (1.89.4) - tooltips: Added 'bool' return value to BeginTooltip() for API consistency. Please only submit contents and call EndTooltip() if BeginTooltip() returns true. In reality the function will _currently_ always return true, but further changes down the line may change this, best to clarify API sooner. + - 2023/02/15 (1.89.4) - moved the optional "courtesy maths operators" implementation from imgui_internal.h in imgui.h. + Even though we encourage using your own maths types and operators by setting up IM_VEC2_CLASS_EXTRA, + it has been frequently requested by people to use our own. We had an opt-in define which was + previously fulfilled in imgui_internal.h. It is now fulfilled in imgui.h. (#6164) + - OK: #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui.h" / #include "imgui_internal.h" + - Error: #include "imgui.h" / #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui_internal.h" + - 2023/02/07 (1.89.3) - backends: renamed "imgui_impl_sdl.cpp" to "imgui_impl_sdl2.cpp" and "imgui_impl_sdl.h" to "imgui_impl_sdl2.h". (#6146) This is in prevision for the future release of SDL3. + - 2022/10/26 (1.89) - commented out redirecting OpenPopupContextItem() which was briefly the name of OpenPopupOnItemClick() from 1.77 to 1.79. + - 2022/10/12 (1.89) - removed runtime patching of invalid "%f"/"%0.f" format strings for DragInt()/SliderInt(). This was obsoleted in 1.61 (May 2018). See 1.61 changelog for details. + - 2022/09/26 (1.89) - renamed and merged keyboard modifiers key enums and flags into a same set. Kept inline redirection enums (will obsolete). + - ImGuiKey_ModCtrl and ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl + - ImGuiKey_ModShift and ImGuiModFlags_Shift -> ImGuiMod_Shift + - ImGuiKey_ModAlt and ImGuiModFlags_Alt -> ImGuiMod_Alt + - ImGuiKey_ModSuper and ImGuiModFlags_Super -> ImGuiMod_Super + the ImGuiKey_ModXXX were introduced in 1.87 and mostly used by backends. + the ImGuiModFlags_XXX have been exposed in imgui.h but not really used by any public api only by third-party extensions. + exceptionally commenting out the older ImGuiKeyModFlags_XXX names ahead of obsolescence schedule to reduce confusion and because they were not meant to be used anyway. + - 2022/09/20 (1.89) - ImGuiKey is now a typed enum, allowing ImGuiKey_XXX symbols to be named in debuggers. + this will require uses of legacy backend-dependent indices to be casted, e.g. + - with imgui_impl_glfw: IsKeyPressed(GLFW_KEY_A) -> IsKeyPressed((ImGuiKey)GLFW_KEY_A); + - with imgui_impl_win32: IsKeyPressed('A') -> IsKeyPressed((ImGuiKey)'A') + - etc. However if you are upgrading code you might well use the better, backend-agnostic IsKeyPressed(ImGuiKey_A) now! + - 2022/09/12 (1.89) - removed the bizarre legacy default argument for 'TreePush(const void* ptr = NULL)', always pass a pointer value explicitly. NULL/nullptr is ok but require cast, e.g. TreePush((void*)nullptr); + - 2022/09/05 (1.89) - commented out redirecting functions/enums names that were marked obsolete in 1.77 and 1.78 (June 2020): + - DragScalar(), DragScalarN(), DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - SliderScalar(), SliderScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - BeginPopupContextWindow(const char*, ImGuiMouseButton, bool) -> use BeginPopupContextWindow(const char*, ImGuiPopupFlags) + - 2022/09/02 (1.89) - obsoleted using SetCursorPos()/SetCursorScreenPos() to extend parent window/cell boundaries. + this relates to when moving the cursor position beyond current boundaries WITHOUT submitting an item. + - previously this would make the window content size ~200x200: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); + - instead, please submit an item: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); + - alternative: + Begin(...) + Dummy(ImVec2(200,200)) + End(); + - content size is now only extended when submitting an item! + - with '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will now be detected and assert. + - without '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will silently be fixed until we obsolete it. + - 2022/08/03 (1.89) - changed signature of ImageButton() function. Kept redirection function (will obsolete). + - added 'const char* str_id' parameter + removed 'int frame_padding = -1' parameter. + - old signature: bool ImageButton(ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), int frame_padding = -1, ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - used the ImTextureID value to create an ID. This was inconsistent with other functions, led to ID conflicts, and caused problems with engines using transient ImTextureID values. + - had a FramePadding override which was inconsistent with other functions and made the already-long signature even longer. + - new signature: bool ImageButton(const char* str_id, ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - requires an explicit identifier. You may still use e.g. PushID() calls and then pass an empty identifier. + - always uses style.FramePadding for padding, to be consistent with other buttons. You may use PushStyleVar() to alter this. + - 2022/07/08 (1.89) - inputs: removed io.NavInputs[] and ImGuiNavInput enum (following 1.87 changes). + - Official backends from 1.87+ -> no issue. + - Official backends from 1.60 to 1.86 -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need updating! + - Custom backends not writing to io.NavInputs[] -> no issue. + - Custom backends writing to io.NavInputs[] -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need fixing! + - TL;DR: Backends should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values instead of filling io.NavInput[]. + - 2022/06/15 (1.88) - renamed IMGUI_DISABLE_METRICS_WINDOW to IMGUI_DISABLE_DEBUG_TOOLS for correctness. kept support for old define (will obsolete). + - 2022/05/03 (1.88) - backends: osx: removed ImGui_ImplOSX_HandleEvent() from backend API in favor of backend automatically handling event capture. All ImGui_ImplOSX_HandleEvent() calls should be removed as they are now unnecessary. + - 2022/04/05 (1.88) - inputs: renamed ImGuiKeyModFlags to ImGuiModFlags. Kept inline redirection enums (will obsolete). This was never used in public API functions but technically present in imgui.h and ImGuiIO. + - 2022/01/20 (1.87) - inputs: reworded gamepad IO. + - Backend writing to io.NavInputs[] -> backend should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values. + - 2022/01/19 (1.87) - sliders, drags: removed support for legacy arithmetic operators (+,+-,*,/) when inputting text. This doesn't break any api/code but a feature that used to be accessible by end-users (which seemingly no one used). + - 2022/01/17 (1.87) - inputs: reworked mouse IO. + - Backend writing to io.MousePos -> backend should call io.AddMousePosEvent() + - Backend writing to io.MouseDown[] -> backend should call io.AddMouseButtonEvent() + - Backend writing to io.MouseWheel -> backend should call io.AddMouseWheelEvent() + - Backend writing to io.MouseHoveredViewport -> backend should call io.AddMouseViewportEvent() [Docking branch w/ multi-viewports only] + note: for all calls to IO new functions, the Dear ImGui context should be bound/current. + read https://github.com/ocornut/imgui/issues/4921 for details. + - 2022/01/10 (1.87) - inputs: reworked keyboard IO. Removed io.KeyMap[], io.KeysDown[] in favor of calling io.AddKeyEvent(), ImGui::IsKeyDown(). Removed GetKeyIndex(), now unnecessary. All IsKeyXXX() functions now take ImGuiKey values. All features are still functional until IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Read Changelog and Release Notes for details. + - IsKeyPressed(MY_NATIVE_KEY_XXX) -> use IsKeyPressed(ImGuiKey_XXX) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() (+ call io.SetKeyEventNativeData() if you want legacy user code to stil function with legacy key codes). + - Backend writing to io.KeyCtrl, io.KeyShift.. -> backend should call io.AddKeyEvent() with ImGuiMod_XXX values. *IF YOU PULLED CODE BETWEEN 2021/01/10 and 2021/01/27: We used to have a io.AddKeyModsEvent() function which was now replaced by io.AddKeyEvent() with ImGuiMod_XXX values.* + - one case won't work with backward compatibility: if your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") because those values are now larger than the legacy KeyDown[] array. Will assert. + - inputs: added ImGuiKey_ModCtrl/ImGuiKey_ModShift/ImGuiKey_ModAlt/ImGuiKey_ModSuper values to submit keyboard modifiers using io.AddKeyEvent(), instead of writing directly to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper. + - 2022/01/05 (1.87) - inputs: renamed ImGuiKey_KeyPadEnter to ImGuiKey_KeypadEnter to align with new symbols. Kept redirection enum. + - 2022/01/05 (1.87) - removed io.ImeSetInputScreenPosFn() in favor of more flexible io.SetPlatformImeDataFn(). Removed 'void* io.ImeWindowHandle' in favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2022/01/01 (1.87) - commented out redirecting functions/enums names that were marked obsolete in 1.69, 1.70, 1.71, 1.72 (March-July 2019) + - ImGui::SetNextTreeNodeOpen() -> use ImGui::SetNextItemOpen() + - ImGui::GetContentRegionAvailWidth() -> use ImGui::GetContentRegionAvail().x + - ImGui::TreeAdvanceToLabelPos() -> use ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetTreeNodeToLabelSpacing()); + - ImFontAtlas::CustomRect -> use ImFontAtlasCustomRect + - ImGuiColorEditFlags_RGB/HSV/HEX -> use ImGuiColorEditFlags_DisplayRGB/HSV/Hex + - 2021/12/20 (1.86) - backends: removed obsolete Marmalade backend (imgui_impl_marmalade.cpp) + example. Find last supported version at https://github.com/ocornut/imgui/wiki/Bindings + - 2021/11/04 (1.86) - removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. Please open an issue if you think you really need this function. + - 2021/08/23 (1.85) - removed GetWindowContentRegionWidth() function. keep inline redirection helper. can use 'GetWindowContentRegionMax().x - GetWindowContentRegionMin().x' instead for generally 'GetContentRegionAvail().x' is more useful. + - 2021/07/26 (1.84) - commented out redirecting functions/enums names that were marked obsolete in 1.67 and 1.69 (March 2019): + - ImGui::GetOverlayDrawList() -> use ImGui::GetForegroundDrawList() + - ImFont::GlyphRangesBuilder -> use ImFontGlyphRangesBuilder + - 2021/05/19 (1.83) - backends: obsoleted direct access to ImDrawCmd::TextureId in favor of calling ImDrawCmd::GetTexID(). + - if you are using official backends from the source tree: you have nothing to do. + - if you have copied old backend code or using your own: change access to draw_cmd->TextureId to draw_cmd->GetTexID(). + - 2021/03/12 (1.82) - upgraded ImDrawList::AddRect(), AddRectFilled(), PathRect() to use ImDrawFlags instead of ImDrawCornersFlags. + - ImDrawCornerFlags_TopLeft -> use ImDrawFlags_RoundCornersTopLeft + - ImDrawCornerFlags_BotRight -> use ImDrawFlags_RoundCornersBottomRight + - ImDrawCornerFlags_None -> use ImDrawFlags_RoundCornersNone etc. + flags now sanely defaults to 0 instead of 0x0F, consistent with all other flags in the API. + breaking: the default with rounding > 0.0f is now "round all corners" vs old implicit "round no corners": + - rounding == 0.0f + flags == 0 --> meant no rounding --> unchanged (common use) + - rounding > 0.0f + flags != 0 --> meant rounding --> unchanged (common use) + - rounding == 0.0f + flags != 0 --> meant no rounding --> unchanged (unlikely use) + - rounding > 0.0f + flags == 0 --> meant no rounding --> BREAKING (unlikely use): will now round all corners --> use ImDrawFlags_RoundCornersNone or rounding == 0.0f. + this ONLY matters for hard coded use of 0 + rounding > 0.0f. Use of named ImDrawFlags_RoundCornersNone (new) or ImDrawCornerFlags_None (old) are ok. + the old ImDrawCornersFlags used awkward default values of ~0 or 0xF (4 lower bits set) to signify "round all corners" and we sometimes encouraged using them as shortcuts. + legacy path still support use of hard coded ~0 or any value from 0x1 or 0xF. They will behave the same with legacy paths enabled (will assert otherwise). + - 2021/03/11 (1.82) - removed redirecting functions/enums names that were marked obsolete in 1.66 (September 2018): + - ImGui::SetScrollHere() -> use ImGui::SetScrollHereY() + - 2021/03/11 (1.82) - clarified that ImDrawList::PathArcTo(), ImDrawList::PathArcToFast() won't render with radius < 0.0f. Previously it sorts of accidentally worked but would generally lead to counter-clockwise paths and have an effect on anti-aliasing. + - 2021/03/10 (1.82) - upgraded ImDrawList::AddPolyline() and PathStroke() "bool closed" parameter to "ImDrawFlags flags". The matching ImDrawFlags_Closed value is guaranteed to always stay == 1 in the future. + - 2021/02/22 (1.82) - (*undone in 1.84*) win32+mingw: Re-enabled IME functions by default even under MinGW. In July 2016, issue #738 had me incorrectly disable those default functions for MinGW. MinGW users should: either link with -limm32, either set their imconfig file with '#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS'. + - 2021/02/17 (1.82) - renamed rarely used style.CircleSegmentMaxError (old default = 1.60f) to style.CircleTessellationMaxError (new default = 0.30f) as the meaning of the value changed. + - 2021/02/03 (1.81) - renamed ListBoxHeader(const char* label, ImVec2 size) to BeginListBox(). Kept inline redirection function (will obsolete). + - removed ListBoxHeader(const char* label, int items_count, int height_in_items = -1) in favor of specifying size. Kept inline redirection function (will obsolete). + - renamed ListBoxFooter() to EndListBox(). Kept inline redirection function (will obsolete). + - 2021/01/26 (1.81) - removed ImGuiFreeType::BuildFontAtlas(). Kept inline redirection function. Prefer using '#define IMGUI_ENABLE_FREETYPE', but there's a runtime selection path available too. The shared extra flags parameters (very rarely used) are now stored in ImFontAtlas::FontBuilderFlags. + - renamed ImFontConfig::RasterizerFlags (used by FreeType) to ImFontConfig::FontBuilderFlags. + - renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.63 (August 2018): + - ImGui::IsItemDeactivatedAfterChange() -> use ImGui::IsItemDeactivatedAfterEdit(). + - ImGuiCol_ModalWindowDarkening -> use ImGuiCol_ModalWindowDimBg + - ImGuiInputTextCallback -> use ImGuiTextEditCallback + - ImGuiInputTextCallbackData -> use ImGuiTextEditCallbackData + - 2020/12/21 (1.80) - renamed ImDrawList::AddBezierCurve() to AddBezierCubic(), and PathBezierCurveTo() to PathBezierCubicCurveTo(). Kept inline redirection function (will obsolete). + - 2020/12/04 (1.80) - added imgui_tables.cpp file! Manually constructed project files will need the new file added! + - 2020/11/18 (1.80) - renamed undocumented/internals ImGuiColumnsFlags_* to ImGuiOldColumnFlags_* in prevision of incoming Tables API. + - 2020/11/03 (1.80) - renamed io.ConfigWindowsMemoryCompactTimer to io.ConfigMemoryCompactTimer as the feature will apply to other data structures + - 2020/10/14 (1.80) - backends: moved all backends files (imgui_impl_XXXX.cpp, imgui_impl_XXXX.h) from examples/ to backends/. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.60 (April 2018): + - io.RenderDrawListsFn pointer -> use ImGui::GetDrawData() value and call the render function of your backend + - ImGui::IsAnyWindowFocused() -> use ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) + - ImGui::IsAnyWindowHovered() -> use ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + - ImGuiStyleVar_Count_ -> use ImGuiStyleVar_COUNT + - ImGuiMouseCursor_Count_ -> use ImGuiMouseCursor_COUNT + - removed redirecting functions names that were marked obsolete in 1.61 (May 2018): + - InputFloat (... int decimal_precision ...) -> use InputFloat (... const char* format ...) with format = "%.Xf" where X is your value for decimal_precision. + - same for InputFloat2()/InputFloat3()/InputFloat4() variants taking a `int decimal_precision` parameter. + - 2020/10/05 (1.79) - removed ImGuiListClipper: Renamed constructor parameters which created an ambiguous alternative to using the ImGuiListClipper::Begin() function, with misleading edge cases (note: imgui_memory_editor <0.40 from imgui_club/ used this old clipper API. Update your copy if needed). + - 2020/09/25 (1.79) - renamed ImGuiSliderFlags_ClampOnInput to ImGuiSliderFlags_AlwaysClamp. Kept redirection enum (will obsolete sooner because previous name was added recently). + - 2020/09/25 (1.79) - renamed style.TabMinWidthForUnselectedCloseButton to style.TabMinWidthForCloseButton. + - 2020/09/21 (1.79) - renamed OpenPopupContextItem() back to OpenPopupOnItemClick(), reverting the change from 1.77. For varieties of reason this is more self-explanatory. + - 2020/09/21 (1.79) - removed return value from OpenPopupOnItemClick() - returned true on mouse release on an item - because it is inconsistent with other popup APIs and makes others misleading. It's also and unnecessary: you can use IsWindowAppearing() after BeginPopup() for a similar result. + - 2020/09/17 (1.79) - removed ImFont::DisplayOffset in favor of ImFontConfig::GlyphOffset. DisplayOffset was applied after scaling and not very meaningful/useful outside of being needed by the default ProggyClean font. If you scaled this value after calling AddFontDefault(), this is now done automatically. It was also getting in the way of better font scaling, so let's get rid of it now! + - 2020/08/17 (1.78) - obsoleted use of the trailing 'float power=1.0f' parameter for DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(), DragFloatRange2(), DragScalar(), DragScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(), SliderScalar(), SliderScalarN(), VSliderFloat() and VSliderScalar(). + replaced the 'float power=1.0f' argument with integer-based flags defaulting to 0 (as with all our flags). + worked out a backward-compatibility scheme so hopefully most C++ codebase should not be affected. in short, when calling those functions: + - if you omitted the 'power' parameter (likely!), you are not affected. + - if you set the 'power' parameter to 1.0f (same as previous default value): 1/ your compiler may warn on float>int conversion, 2/ everything else will work. 3/ you can replace the 1.0f value with 0 to fix the warning, and be technically correct. + - if you set the 'power' parameter to >1.0f (to enable non-linear editing): 1/ your compiler may warn on float>int conversion, 2/ code will assert at runtime, 3/ in case asserts are disabled, the code will not crash and enable the _Logarithmic flag. 4/ you can replace the >1.0f value with ImGuiSliderFlags_Logarithmic to fix the warning/assert and get a _similar_ effect as previous uses of power >1.0f. + see https://github.com/ocornut/imgui/issues/3361 for all details. + kept inline redirection functions (will obsolete) apart for: DragFloatRange2(), VSliderFloat(), VSliderScalar(). For those three the 'float power=1.0f' version was removed directly as they were most unlikely ever used. + for shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + - obsoleted use of v_min > v_max in DragInt, DragFloat, DragScalar to lock edits (introduced in 1.73, was not demoed nor documented very), will be replaced by a more generic ReadOnly feature. You may use the ImGuiSliderFlags_ReadOnly internal flag in the meantime. + - 2020/06/23 (1.77) - removed BeginPopupContextWindow(const char*, int mouse_button, bool also_over_items) in favor of BeginPopupContextWindow(const char*, ImGuiPopupFlags flags) with ImGuiPopupFlags_NoOverItems. + - 2020/06/15 (1.77) - renamed OpenPopupOnItemClick() to OpenPopupContextItem(). Kept inline redirection function (will obsolete). [NOTE: THIS WAS REVERTED IN 1.79] + - 2020/06/15 (1.77) - removed CalcItemRectClosestPoint() entry point which was made obsolete and asserting in December 2017. + - 2020/04/23 (1.77) - removed unnecessary ID (first arg) of ImFontAtlas::AddCustomRectRegular(). + - 2020/01/22 (1.75) - ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius any more. + - 2019/12/17 (1.75) - [undid this change in 1.76] made Columns() limited to 64 columns by asserting above that limit. While the current code technically supports it, future code may not so we're putting the restriction ahead. + - 2019/12/13 (1.75) - [imgui_internal.h] changed ImRect() default constructor initializes all fields to 0.0f instead of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by adding multiple points into it, you may need to fix your initial value. + - 2019/12/08 (1.75) - removed redirecting functions/enums that were marked obsolete in 1.53 (December 2017): + - ShowTestWindow() -> use ShowDemoWindow() + - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) + - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() + - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg + - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding + - ImGuiTreeNodeFlags_AllowOverlapMode -> use ImGuiTreeNodeFlags_AllowItemOverlap + - IMGUI_DISABLE_TEST_WINDOWS -> use IMGUI_DISABLE_DEMO_WINDOWS + - 2019/12/08 (1.75) - obsoleted calling ImDrawList::PrimReserve() with a negative count (which was vaguely documented and rarely if ever used). Instead, we added an explicit PrimUnreserve() API. + - 2019/12/06 (1.75) - removed implicit default parameter to IsMouseDragging(int button = 0) to be consistent with other mouse functions (none of the other functions have it). + - 2019/11/21 (1.74) - ImFontAtlas::AddCustomRectRegular() now requires an ID larger than 0x110000 (instead of 0x10000) to conform with supporting Unicode planes 1-16 in a future update. ID below 0x110000 will now assert. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS to IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS for consistency. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_MATH_FUNCTIONS to IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS for consistency. + - 2019/10/22 (1.74) - removed redirecting functions/enums that were marked obsolete in 1.52 (October 2017): + - Begin() [old 5 args version] -> use Begin() [3 args], use SetNextWindowSize() SetNextWindowBgAlpha() if needed + - IsRootWindowOrAnyChildHovered() -> use IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) + - AlignFirstTextHeightToWidgets() -> use AlignTextToFramePadding() + - SetNextWindowPosCenter() -> use SetNextWindowPos() with a pivot of (0.5f, 0.5f) + - ImFont::Glyph -> use ImFontGlyph + - 2019/10/14 (1.74) - inputs: Fixed a miscalculation in the keyboard/mouse "typematic" repeat delay/rate calculation, used by keys and e.g. repeating mouse buttons as well as the GetKeyPressedAmount() function. + if you were using a non-default value for io.KeyRepeatRate (previous default was 0.250), you can add +io.KeyRepeatDelay to it to compensate for the fix. + The function was triggering on: 0.0 and (delay+rate*N) where (N>=1). Fixed formula responds to (N>=0). Effectively it made io.KeyRepeatRate behave like it was set to (io.KeyRepeatRate + io.KeyRepeatDelay). + If you never altered io.KeyRepeatRate nor used GetKeyPressedAmount() this won't affect you. + - 2019/07/15 (1.72) - removed TreeAdvanceToLabelPos() which is rarely used and only does SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()). Kept redirection function (will obsolete). + - 2019/07/12 (1.72) - renamed ImFontAtlas::CustomRect to ImFontAtlasCustomRect. Kept redirection typedef (will obsolete). + - 2019/06/14 (1.72) - removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): ImGuiCol_Column*, ImGuiSetCond_*, IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow(), IMGUI_ONCE_UPON_A_FRAME. Grep this log for details and new names, or see how they were implemented until 1.71. + - 2019/06/07 (1.71) - rendering of child window outer decorations (bg color, border, scrollbars) is now performed as part of the parent window. If you have + overlapping child windows in a same parent, and relied on their relative z-order to be mapped to their submission order, this will affect your rendering. + This optimization is disabled if the parent window has no visual output, because it appears to be the most common situation leading to the creation of overlapping child windows. + Please reach out if you are affected. + - 2019/05/13 (1.71) - renamed SetNextTreeNodeOpen() to SetNextItemOpen(). Kept inline redirection function (will obsolete). + - 2019/05/11 (1.71) - changed io.AddInputCharacter(unsigned short c) signature to io.AddInputCharacter(unsigned int c). + - 2019/04/29 (1.70) - improved ImDrawList thick strokes (>1.0f) preserving correct thickness up to 90 degrees angles (e.g. rectangles). If you have custom rendering using thick lines, they will appear thicker now. + - 2019/04/29 (1.70) - removed GetContentRegionAvailWidth(), use GetContentRegionAvail().x instead. Kept inline redirection function (will obsolete). + - 2019/03/04 (1.69) - renamed GetOverlayDrawList() to GetForegroundDrawList(). Kept redirection function (will obsolete). + - 2019/02/26 (1.69) - renamed ImGuiColorEditFlags_RGB/ImGuiColorEditFlags_HSV/ImGuiColorEditFlags_HEX to ImGuiColorEditFlags_DisplayRGB/ImGuiColorEditFlags_DisplayHSV/ImGuiColorEditFlags_DisplayHex. Kept redirection enums (will obsolete). + - 2019/02/14 (1.68) - made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). If for some reason your time step calculation gives you a zero value, replace it with an arbitrarily small value! + - 2019/02/01 (1.68) - removed io.DisplayVisibleMin/DisplayVisibleMax (which were marked obsolete and removed from viewport/docking branch already). + - 2019/01/06 (1.67) - renamed io.InputCharacters[], marked internal as was always intended. Please don't access directly, and use AddInputCharacter() instead! + - 2019/01/06 (1.67) - renamed ImFontAtlas::GlyphRangesBuilder to ImFontGlyphRangesBuilder. Kept redirection typedef (will obsolete). + - 2018/12/20 (1.67) - made it illegal to call Begin("") with an empty string. This somehow half-worked before but had various undesirable side-effects. + - 2018/12/10 (1.67) - renamed io.ConfigResizeWindowsFromEdges to io.ConfigWindowsResizeFromEdges as we are doing a large pass on configuration flags. + - 2018/10/12 (1.66) - renamed misc/stl/imgui_stl.* to misc/cpp/imgui_stdlib.* in prevision for other C++ helper files. + - 2018/09/28 (1.66) - renamed SetScrollHere() to SetScrollHereY(). Kept redirection function (will obsolete). + - 2018/09/06 (1.65) - renamed stb_truetype.h to imstb_truetype.h, stb_textedit.h to imstb_textedit.h, and stb_rect_pack.h to imstb_rectpack.h. + If you were conveniently using the imgui copy of those STB headers in your project you will have to update your include paths. + - 2018/09/05 (1.65) - renamed io.OptCursorBlink/io.ConfigCursorBlink to io.ConfigInputTextCursorBlink. (#1427) + - 2018/08/31 (1.64) - added imgui_widgets.cpp file, extracted and moved widgets code out of imgui.cpp into imgui_widgets.cpp. Re-ordered some of the code remaining in imgui.cpp. + NONE OF THE FUNCTIONS HAVE CHANGED. THE CODE IS SEMANTICALLY 100% IDENTICAL, BUT _EVERY_ FUNCTION HAS BEEN MOVED. + Because of this, any local modifications to imgui.cpp will likely conflict when you update. Read docs/CHANGELOG.txt for suggestions. + - 2018/08/22 (1.63) - renamed IsItemDeactivatedAfterChange() to IsItemDeactivatedAfterEdit() for consistency with new IsItemEdited() API. Kept redirection function (will obsolete soonish as IsItemDeactivatedAfterChange() is very recent). + - 2018/08/21 (1.63) - renamed ImGuiTextEditCallback to ImGuiInputTextCallback, ImGuiTextEditCallbackData to ImGuiInputTextCallbackData for consistency. Kept redirection types (will obsolete). + - 2018/08/21 (1.63) - removed ImGuiInputTextCallbackData::ReadOnly since it is a duplication of (ImGuiInputTextCallbackData::Flags & ImGuiInputTextFlags_ReadOnly). + - 2018/08/01 (1.63) - removed per-window ImGuiWindowFlags_ResizeFromAnySide beta flag in favor of a global io.ConfigResizeWindowsFromEdges [update 1.67 renamed to ConfigWindowsResizeFromEdges] to enable the feature. + - 2018/08/01 (1.63) - renamed io.OptCursorBlink to io.ConfigCursorBlink [-> io.ConfigInputTextCursorBlink in 1.65], io.OptMacOSXBehaviors to ConfigMacOSXBehaviors for consistency. + - 2018/07/22 (1.63) - changed ImGui::GetTime() return value from float to double to avoid accumulating floating point imprecisions over time. + - 2018/07/08 (1.63) - style: renamed ImGuiCol_ModalWindowDarkening to ImGuiCol_ModalWindowDimBg for consistency with other features. Kept redirection enum (will obsolete). + - 2018/06/08 (1.62) - examples: the imgui_impl_XXX files have been split to separate platform (Win32, GLFW, SDL2, etc.) from renderer (DX11, OpenGL, Vulkan, etc.). + old backends will still work as is, however prefer using the separated backends as they will be updated to support multi-viewports. + when adopting new backends follow the main.cpp code of your preferred examples/ folder to know which functions to call. + in particular, note that old backends called ImGui::NewFrame() at the end of their ImGui_ImplXXXX_NewFrame() function. + - 2018/06/06 (1.62) - renamed GetGlyphRangesChinese() to GetGlyphRangesChineseFull() to distinguish other variants and discourage using the full set. + - 2018/06/06 (1.62) - TreeNodeEx()/TreeNodeBehavior(): the ImGuiTreeNodeFlags_CollapsingHeader helper now include the ImGuiTreeNodeFlags_NoTreePushOnOpen flag. See Changelog for details. + - 2018/05/03 (1.61) - DragInt(): the default compile-time format string has been changed from "%.0f" to "%d", as we are not using integers internally any more. + If you used DragInt() with custom format strings, make sure you change them to use %d or an integer-compatible format. + To honor backward-compatibility, the DragInt() code will currently parse and modify format strings to replace %*f with %d, giving time to users to upgrade their code. + If you have IMGUI_DISABLE_OBSOLETE_FUNCTIONS enabled, the code will instead assert! You may run a reg-exp search on your codebase for e.g. "DragInt.*%f" to help you find them. + - 2018/04/28 (1.61) - obsoleted InputFloat() functions taking an optional "int decimal_precision" in favor of an equivalent and more flexible "const char* format", + consistent with other functions. Kept redirection functions (will obsolete). + - 2018/04/09 (1.61) - IM_DELETE() helper function added in 1.60 doesn't clear the input _pointer_ reference, more consistent with expectation and allows passing r-value. + - 2018/03/20 (1.60) - renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, _not_ used by core and only honored by some backend ahead of merging the Nav branch). + - 2018/03/12 (1.60) - removed ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered as the closing cross uses regular button colors now. + - 2018/03/08 (1.60) - changed ImFont::DisplayOffset.y to default to 0 instead of +1. Fixed rounding of Ascent/Descent to match TrueType renderer. If you were adding or subtracting to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. + - 2018/03/03 (1.60) - renamed ImGuiStyleVar_Count_ to ImGuiStyleVar_COUNT and ImGuiMouseCursor_Count_ to ImGuiMouseCursor_COUNT for consistency with other public enums. + - 2018/02/18 (1.60) - BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. + - 2018/02/16 (1.60) - obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). Use ImGui::GetDrawData() to retrieve the ImDrawData* to display. + - 2018/02/07 (1.60) - reorganized context handling to be more explicit, + - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. + - removed Shutdown() function, as DestroyContext() serve this purpose. + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwise CreateContext() will create its own font atlas instance. + - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. + - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. + - 2018/01/31 (1.60) - moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. + - 2018/01/11 (1.60) - obsoleted IsAnyWindowHovered() in favor of IsWindowHovered(ImGuiHoveredFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/11 (1.60) - obsoleted IsAnyWindowFocused() in favor of IsWindowFocused(ImGuiFocusedFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/03 (1.60) - renamed ImGuiSizeConstraintCallback to ImGuiSizeCallback, ImGuiSizeConstraintCallbackData to ImGuiSizeCallbackData. + - 2017/12/29 (1.60) - removed CalcItemRectClosestPoint() which was weird and not really used by anyone except demo code. If you need it it's easy to replicate on your side. + - 2017/12/24 (1.53) - renamed the emblematic ShowTestWindow() function to ShowDemoWindow(). Kept redirection function (will obsolete). + - 2017/12/21 (1.53) - ImDrawList: renamed style.AntiAliasedShapes to style.AntiAliasedFill for consistency and as a way to explicitly break code that manipulate those flag at runtime. You can now manipulate ImDrawList::Flags + - 2017/12/21 (1.53) - ImDrawList: removed 'bool anti_aliased = true' final parameter of ImDrawList::AddPolyline() and ImDrawList::AddConvexPolyFilled(). Prefer manipulating ImDrawList::Flags if you need to toggle them during the frame. + - 2017/12/14 (1.53) - using the ImGuiWindowFlags_NoScrollWithMouse flag on a child window forwards the mouse wheel event to the parent window, unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set. + - 2017/12/13 (1.53) - renamed GetItemsLineHeightWithSpacing() to GetFrameHeightWithSpacing(). Kept redirection function (will obsolete). + - 2017/12/13 (1.53) - obsoleted IsRootWindowFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootWindow). Kept redirection function (will obsolete). + - obsoleted IsRootWindowOrAnyChildFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows). Kept redirection function (will obsolete). + - 2017/12/12 (1.53) - renamed ImGuiTreeNodeFlags_AllowOverlapMode to ImGuiTreeNodeFlags_AllowItemOverlap. Kept redirection enum (will obsolete). + - 2017/12/10 (1.53) - removed SetNextWindowContentWidth(), prefer using SetNextWindowContentSize(). Kept redirection function (will obsolete). + - 2017/11/27 (1.53) - renamed ImGuiTextBuffer::append() helper to appendf(), appendv() to appendfv(). If you copied the 'Log' demo in your code, it uses appendv() so that needs to be renamed. + - 2017/11/18 (1.53) - Style, Begin: removed ImGuiWindowFlags_ShowBorders window flag. Borders are now fully set up in the ImGuiStyle structure (see e.g. style.FrameBorderSize, style.WindowBorderSize). Use ImGui::ShowStyleEditor() to look them up. + Please note that the style system will keep evolving (hopefully stabilizing in Q1 2018), and so custom styles will probably subtly break over time. It is recommended you use the StyleColorsClassic(), StyleColorsDark(), StyleColorsLight() functions. + - 2017/11/18 (1.53) - Style: removed ImGuiCol_ComboBg in favor of combo boxes using ImGuiCol_PopupBg for consistency. + - 2017/11/18 (1.53) - Style: renamed ImGuiCol_ChildWindowBg to ImGuiCol_ChildBg. + - 2017/11/18 (1.53) - Style: renamed style.ChildWindowRounding to style.ChildRounding, ImGuiStyleVar_ChildWindowRounding to ImGuiStyleVar_ChildRounding. + - 2017/11/02 (1.53) - obsoleted IsRootWindowOrAnyChildHovered() in favor of using IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + - 2017/10/24 (1.52) - renamed IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS to IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS for consistency. + - 2017/10/20 (1.52) - changed IsWindowHovered() default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. + - 2017/10/20 (1.52) - marked IsItemHoveredRect()/IsMouseHoveringWindow() as obsolete, in favor of using the newly introduced flags for IsItemHovered() and IsWindowHovered(). See https://github.com/ocornut/imgui/issues/1382 for details. + removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. + IsItemHoveredRect() --> IsItemHovered(ImGuiHoveredFlags_RectOnly) + IsMouseHoveringAnyWindow() --> IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + IsMouseHoveringWindow() --> IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) [weird, old behavior] + - 2017/10/17 (1.52) - marked the old 5-parameters version of Begin() as obsolete (still available). Use SetNextWindowSize()+Begin() instead! + - 2017/10/11 (1.52) - renamed AlignFirstTextHeightToWidgets() to AlignTextToFramePadding(). Kept inline redirection function (will obsolete). + - 2017/09/26 (1.52) - renamed ImFont::Glyph to ImFontGlyph. Kept redirection typedef (will obsolete). + - 2017/09/25 (1.52) - removed SetNextWindowPosCenter() because SetNextWindowPos() now has the optional pivot information to do the same and more. Kept redirection function (will obsolete). + - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your backend if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". + - 2017/08/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! + - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). + - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). + - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency. + - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix. + - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame type. + - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely. + - 2017/08/13 (1.51) - renamed ImGuiCol_Column to ImGuiCol_Separator, ImGuiCol_ColumnHovered to ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive to ImGuiCol_SeparatorActive. Kept redirection enums (will obsolete). + - 2017/08/11 (1.51) - renamed ImGuiSetCond_Always to ImGuiCond_Always, ImGuiSetCond_Once to ImGuiCond_Once, ImGuiSetCond_FirstUseEver to ImGuiCond_FirstUseEver, ImGuiSetCond_Appearing to ImGuiCond_Appearing. Kept redirection enums (will obsolete). + - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). + - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. + - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0))' + - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse + - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. + - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetID() and use it instead of passing string to BeginChild(). + - 2016/10/15 (1.50) - avoid 'void* user_data' parameter to io.SetClipboardTextFn/io.GetClipboardTextFn pointers. We pass io.ClipboardUserData to it. + - 2016/09/25 (1.50) - style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. + - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully, breakage should be minimal. + - 2016/05/12 (1.49) - title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore. + If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you, otherwise if <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color: + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) { float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); } + If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. + - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). + - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. + - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). + - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDrawList::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref GitHub issue #337). + - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) + - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). + - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. + - 2016/01/23 (1.48) - fixed not honoring exact width passed to PushItemWidth(), previously it would add extra FramePadding.x*2 over that width. if you had manual pixel-perfect alignment in place it might affect you. + - 2015/12/27 (1.48) - fixed ImDrawList::AddRect() which used to render a rectangle 1 px too large on each axis. + - 2015/12/04 (1.47) - renamed Color() helpers to ValueColor() - dangerously named, rarely used and probably to be made obsolete. + - 2015/08/29 (1.45) - with the addition of horizontal scrollbar we made various fixes to inconsistencies with dealing with cursor position. + GetCursorPos()/SetCursorPos() functions now include the scrolled amount. It shouldn't affect the majority of users, but take note that SetCursorPosX(100.0f) puts you at +100 from the starting x position which may include scrolling, not at +100 from the window left side. + GetContentRegionMax()/GetWindowContentRegionMin()/GetWindowContentRegionMax() functions allow include the scrolled amount. Typically those were used in cases where no scrolling would happen so it may not be a problem, but watch out! + - 2015/08/29 (1.45) - renamed style.ScrollbarWidth to style.ScrollbarSize + - 2015/08/05 (1.44) - split imgui.cpp into extra files: imgui_demo.cpp imgui_draw.cpp imgui_internal.h that you need to add to your project. + - 2015/07/18 (1.44) - fixed angles in ImDrawList::PathArcTo(), PathArcToFast() (introduced in 1.43) being off by an extra PI for no justifiable reason + - 2015/07/14 (1.43) - add new ImFontAtlas::AddFont() API. For the old AddFont***, moved the 'font_no' parameter of ImFontAtlas::AddFont** functions to the ImFontConfig structure. + you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. + - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. + this necessary change will break your rendering function! the fix should be very easy. sorry for that :( + - if you are using a vanilla copy of one of the imgui_impl_XXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - the signature of the io.RenderDrawListsFn handler has changed! + old: ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + new: ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). + parameters: 'cmd_lists' becomes 'draw_data->CmdLists', 'cmd_lists_count' becomes 'draw_data->CmdListsCount' + ImDrawList: 'commands' becomes 'CmdBuffer', 'vtx_buffer' becomes 'VtxBuffer', 'IdxBuffer' is new. + ImDrawCmd: 'vtx_count' becomes 'ElemCount', 'clip_rect' becomes 'ClipRect', 'user_callback' becomes 'UserCallback', 'texture_id' becomes 'TextureId'. + - each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles using indices from the index buffer. + - if you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! + - refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. please upgrade! + - 2015/07/10 (1.43) - changed SameLine() parameters from int to float. + - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). + - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. + - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely used. Sorry! + - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). + - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). + - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. + - 2015/05/11 (1.40) - changed BeginPopup() API, takes a string identifier instead of a bool. ImGui needs to manage the open/closed state of popups. Call OpenPopup() to actually set the "open" state of a popup. BeginPopup() returns true if the popup is opened. + - 2015/05/03 (1.40) - removed style.AutoFitPadding, using style.WindowPadding makes more sense (the default values were already the same). + - 2015/04/13 (1.38) - renamed IsClipped() to IsRectClipped(). Kept inline redirection function until 1.50. + - 2015/04/09 (1.38) - renamed ImDrawList::AddArc() to ImDrawList::AddArcFast() for compatibility with future API + - 2015/04/03 (1.38) - removed ImGuiCol_CheckHovered, ImGuiCol_CheckActive, replaced with the more general ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive. + - 2014/04/03 (1.38) - removed support for passing -FLT_MAX..+FLT_MAX as the range for a SliderFloat(). Use DragFloat() or Inputfloat() instead. + - 2015/03/17 (1.36) - renamed GetItemBoxMin()/GetItemBoxMax()/IsMouseHoveringBox() to GetItemRectMin()/GetItemRectMax()/IsMouseHoveringRect(). Kept inline redirection function until 1.50. + - 2015/03/15 (1.36) - renamed style.TreeNodeSpacing to style.IndentSpacing, ImGuiStyleVar_TreeNodeSpacing to ImGuiStyleVar_IndentSpacing + - 2015/03/13 (1.36) - renamed GetWindowIsFocused() to IsWindowFocused(). Kept inline redirection function until 1.50. + - 2015/03/08 (1.35) - renamed style.ScrollBarWidth to style.ScrollbarWidth (casing) + - 2015/02/27 (1.34) - renamed OpenNextNode(bool) to SetNextTreeNodeOpened(bool, ImGuiSetCond). Kept inline redirection function until 1.50. + - 2015/02/27 (1.34) - renamed ImGuiSetCondition_*** to ImGuiSetCond_***, and _FirstUseThisSession becomes _Once. + - 2015/02/11 (1.32) - changed text input callback ImGuiTextEditCallback return type from void-->int. reserved for future use, return 0 for now. + - 2015/02/10 (1.32) - renamed GetItemWidth() to CalcItemWidth() to clarify its evolving behavior + - 2015/02/08 (1.31) - renamed GetTextLineSpacing() to GetTextLineHeightWithSpacing() + - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) + - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. + - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. + - 2015/01/11 (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + - old: const void* png_data; unsigned int png_size; ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); [..Upload texture to GPU..]; + - new: unsigned char* pixels; int width, height; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); [..Upload texture to GPU..]; io.Fonts->SetTexID(YourTexIdentifier); + you now have more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. It is now recommended that you sample the font texture with bilinear interpolation. + - 2015/01/11 (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to call io.Fonts->SetTexID() + - 2015/01/11 (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + - 2015/01/11 (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) + - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) + - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility + - 2014/11/07 (1.15) - renamed IsHovered() to IsItemHovered() + - 2014/10/02 (1.14) - renamed IMGUI_INCLUDE_IMGUI_USER_CPP to IMGUI_INCLUDE_IMGUI_USER_INL and imgui_user.cpp to imgui_user.inl (more IDE friendly) + - 2014/09/25 (1.13) - removed 'text_end' parameter from IO.SetClipboardTextFn (the string is now always zero-terminated for simplicity) + - 2014/09/24 (1.12) - renamed SetFontScale() to SetWindowFontScale() + - 2014/09/24 (1.12) - moved IM_MALLOC/IM_REALLOC/IM_FREE preprocessor defines to IO.MemAllocFn/IO.MemReallocFn/IO.MemFreeFn + - 2014/08/30 (1.09) - removed IO.FontHeight (now computed automatically) + - 2014/08/30 (1.09) - moved IMGUI_FONT_TEX_UV_FOR_WHITE preprocessor define to IO.FontTexUvForWhite + - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes + + + FREQUENTLY ASKED QUESTIONS (FAQ) + ================================ + + Read all answers online: + https://www.dearimgui.com/faq or https://github.com/ocornut/imgui/blob/master/docs/FAQ.md (same url) + Read all answers locally (with a text editor or ideally a Markdown viewer): + docs/FAQ.md + Some answers are copied down here to facilitate searching in code. + + Q&A: Basics + =========== + + Q: Where is the documentation? + A: This library is poorly documented at the moment and expects the user to be acquainted with C/C++. + - Run the examples/ applications and explore them. + - Read Getting Started (https://github.com/ocornut/imgui/wiki/Getting-Started) guide. + - See demo code in imgui_demo.cpp and particularly the ImGui::ShowDemoWindow() function. + - The demo covers most features of Dear ImGui, so you can read the code and see its output. + - See documentation and comments at the top of imgui.cpp + effectively imgui.h. + - 20+ standalone example applications using e.g. OpenGL/DirectX are provided in the + examples/ folder to explain how to integrate Dear ImGui with your own engine/application. + - The Wiki (https://github.com/ocornut/imgui/wiki) has many resources and links. + - The Glossary (https://github.com/ocornut/imgui/wiki/Glossary) page also may be useful. + - Your programming IDE is your friend, find the type or function declaration to find comments + associated with it. + + Q: What is this library called? + Q: Which version should I get? + >> This library is called "Dear ImGui", please don't call it "ImGui" :) + >> See https://www.dearimgui.com/faq for details. + + Q&A: Integration + ================ + + Q: How to get started? + A: Read https://github.com/ocornut/imgui/wiki/Getting-Started. Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. + + Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? + A: You should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + >> See https://www.dearimgui.com/faq for a fully detailed answer. You really want to read this. + + Q. How can I enable keyboard or gamepad controls? + Q: How can I use this on a machine without mouse, keyboard or screen? (input share, remote display) + Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... + Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... + >> See https://www.dearimgui.com/faq + + Q&A: Usage + ---------- + + Q: About the ID Stack system.. + - Why is my widget not reacting when I click on it? + - How can I have widgets with an empty label? + - How can I have multiple widgets with the same label? + - How can I have multiple windows with the same label? + Q: How can I display an image? What is ImTextureID, how does it work? + Q: How can I use my own math types instead of ImVec2? + Q: How can I interact with standard C++ types (such as std::string and std::vector)? + Q: How can I display custom shapes? (using low-level ImDrawList API) + >> See https://www.dearimgui.com/faq + + Q&A: Fonts, Text + ================ + + Q: How should I handle DPI in my application? + Q: How can I load a different font than the default? + Q: How can I easily use icons in my application? + Q: How can I load multiple fonts? + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + >> See https://www.dearimgui.com/faq and https://github.com/ocornut/imgui/blob/master/docs/FONTS.md + + Q&A: Concerns + ============= + + Q: Who uses Dear ImGui? + Q: Can you create elaborate/serious tools with Dear ImGui? + Q: Can you reskin the look of Dear ImGui? + Q: Why using C++ (as opposed to C)? + >> See https://www.dearimgui.com/faq + + Q&A: Community + ============== + + Q: How can I help? + A: - Businesses: please reach out to "omar AT dearimgui DOT com" if you work in a place using Dear ImGui! + We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. + This is among the most useful thing you can do for Dear ImGui. With increased funding, we sustain and grow work on this project. + >>> See https://github.com/ocornut/imgui/wiki/Funding + - Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, and see how you want to help and can help! + - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads. Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui helps the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on GitHub or privately). + +*/ + +//------------------------------------------------------------------------- +// [SECTION] INCLUDES +//------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" + +// System includes +#include // vsnprintf, sscanf, printf +#include // intptr_t + +// [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#endif + +// [Windows] OS specific includes (optional) +#if defined(_WIN32) && defined(IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) && defined(IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_FUNCTIONS +#endif +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef __MINGW32__ +#include // _wfopen, OpenClipboard +#else +#include +#endif +#if defined(WINAPI_FAMILY) && ((defined(WINAPI_FAMILY_APP) && WINAPI_FAMILY == WINAPI_FAMILY_APP) || (defined(WINAPI_FAMILY_GAMES) && WINAPI_FAMILY == WINAPI_FAMILY_GAMES)) +// The UWP and GDK Win32 API subsets don't support clipboard nor IME functions +#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#endif + +// [Apple] OS specific includes +#if defined(__APPLE__) +#include +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to an 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning: format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type 'int' +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +// We disable -Wpragmas because GCC doesn't provide a has_warning equivalent and some forks/patches may not follow the warning/version association. +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +// Debug options +#define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Hold CTRL to display for all candidates. CTRL+Arrow to change last direction. +#define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window + +// When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch. +static const float NAV_WINDOWING_HIGHLIGHT_DELAY = 0.20f; // Time before the highlight and screen dimming starts fading in +static const float NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15f; // Time before the window list starts to appear +static const float NAV_ACTIVATE_HIGHLIGHT_TIMER = 0.10f; // Time to highlight an item activated by a shortcut. +static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. +static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 0.70f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved. + +// Tooltip offset +static const ImVec2 TOOLTIP_DEFAULT_OFFSET_MOUSE = ImVec2(16, 10); // Multiplied by g.Style.MouseCursorScale +static const ImVec2 TOOLTIP_DEFAULT_OFFSET_TOUCH = ImVec2(0, -20); // Multiplied by g.Style.MouseCursorScale +static const ImVec2 TOOLTIP_DEFAULT_PIVOT_TOUCH = ImVec2(0.5f, 1.0f); // Multiplied by g.Style.MouseCursorScale + +//------------------------------------------------------------------------- +// [SECTION] FORWARD DECLARATIONS +//------------------------------------------------------------------------- + +static void SetCurrentWindow(ImGuiWindow* window); +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); + +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); + +// Settings +static void WindowSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); +static void WindowSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); +static void WindowSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler*, ImGuiTextBuffer* buf); + +// Platform Dependents default implementation for ImGuiPlatformIO functions +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx); +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text); +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext* ctx, const char* path); + +namespace ImGui +{ +// Item +static void ItemHandleShortcut(ImGuiID id); + +// Window Focus +static int FindWindowFocusIndex(ImGuiWindow* window); +static void UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags); + +// Navigation +static void NavUpdate(); +static void NavUpdateWindowing(); +static void NavUpdateWindowingApplyFocus(ImGuiWindow* window); +static void NavUpdateWindowingOverlay(); +static void NavUpdateCancelRequest(); +static void NavUpdateCreateMoveRequest(); +static void NavUpdateCreateTabbingRequest(); +static float NavUpdatePageUpPageDown(); +static inline void NavUpdateAnyRequestFlag(); +static void NavUpdateCreateWrappingRequest(); +static void NavEndFrame(); +static bool NavScoreItem(ImGuiNavItemData* result); +static void NavApplyItemToResult(ImGuiNavItemData* result); +static void NavProcessItem(); +static void NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags); +static ImGuiInputSource NavCalcPreferredRefPosSource(); +static ImVec2 NavCalcPreferredRefPos(); +static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window); +static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window); +static void NavRestoreLayer(ImGuiNavLayer layer); + +// Error Checking and Debug Tools +static void ErrorCheckNewFrameSanityChecks(); +static void ErrorCheckEndFrameSanityChecks(); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +static void UpdateDebugToolItemPicker(); +static void UpdateDebugToolStackQueries(); +static void UpdateDebugToolFlashStyleColor(); +#endif + +// Inputs +static void UpdateKeyboardInputs(); +static void UpdateMouseInputs(); +static void UpdateMouseWheel(); +static void UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt); + +// Misc +static void UpdateSettings(); +static int UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); +static void RenderWindowOuterBorders(ImGuiWindow* window); +static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); +static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); +static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col); +static void RenderDimmedBackgrounds(); +static void SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect); +static void SetLastItemDataForChildWindowItem(ImGuiWindow* window, const ImRect& rect); + +// Viewports +const ImGuiID IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111; // Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter. +static void UpdateViewportsNewFrame(); + +} + +//----------------------------------------------------------------------------- +// [SECTION] CONTEXT AND MEMORY ALLOCATORS +//----------------------------------------------------------------------------- + +// DLL users: +// - Heaps and globals are not shared across DLL boundaries! +// - You will need to call SetCurrentContext() + SetAllocatorFunctions() for each static/DLL boundary you are calling from. +// - Same applies for hot-reloading mechanisms that are reliant on reloading DLL (note that many hot-reloading mechanisms work without DLL). +// - Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Confused? In a debugger: add GImGui to your watch window and notice how its value changes depending on your current location (which DLL boundary you are in). + +// Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be != NULL. +// - ImGui::CreateContext() will automatically set this pointer if it is NULL. +// Change to a different context by calling ImGui::SetCurrentContext(). +// - Important: Dear ImGui functions are not thread-safe because of this pointer. +// If you want thread-safety to allow N threads to access N different contexts: +// - Change this variable to use thread local storage so each thread can refer to a different context, in your imconfig.h: +// struct ImGuiContext; +// extern thread_local ImGuiContext* MyImGuiTLS; +// #define GImGui MyImGuiTLS +// And then define MyImGuiTLS in one of your cpp files. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword. +// - Future development aims to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 +// - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from a different namespace. +// - DLL users: read comments above. +#ifndef GImGui +ImGuiContext* GImGui = NULL; +#endif + +// Memory Allocator functions. Use SetAllocatorFunctions() to change them. +// - You probably don't want to modify that mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - DLL users: read comments above. +#ifndef IMGUI_DISABLE_DEFAULT_ALLOCATORS +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); return malloc(size); } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); free(ptr); } +#else +static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(size); IM_ASSERT(0); return NULL; } +static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(ptr); IM_ASSERT(0); } +#endif +static ImGuiMemAllocFunc GImAllocatorAllocFunc = MallocWrapper; +static ImGuiMemFreeFunc GImAllocatorFreeFunc = FreeWrapper; +static void* GImAllocatorUserData = NULL; + +//----------------------------------------------------------------------------- +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) +//----------------------------------------------------------------------------- + +ImGuiStyle::ImGuiStyle() +{ + Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. + DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowBorderHoverPadding = 4.0f; // Hit-testing extent outside/inside resizing border. Also extend determination of hovered window. Generally meaningfully larger than WindowBorderSize to make it easy to reach borders. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + WindowMenuButtonPosition = ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 12.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + ImageBorderSize = 0.0f; // Thickness of border around tabs. + TabRounding = 5.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + TabBorderSize = 0.0f; // Thickness of border around tabs. + TabCloseButtonMinWidthSelected = -1.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. + TabCloseButtonMinWidthUnselected = 0.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. + TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + TabBarOverlineSize = 1.0f; // Thickness of tab-bar overline, which highlights the selected tab-bar. + TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). + TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell + ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + SeparatorTextBorderSize = 3.0f; // Thickness of border in SeparatorText() + SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. + AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Behaviors + HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + + // Default theme + ImGui::StyleColorsDark(this); +} + +// To scale your entire UI (e.g. if you want your app to use High DPI or generally be DPI aware) you may use this helper function. Scaling the fonts is done separately and is up to you. +// Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. +void ImGuiStyle::ScaleAllSizes(float scale_factor) +{ + WindowPadding = ImTrunc(WindowPadding * scale_factor); + WindowRounding = ImTrunc(WindowRounding * scale_factor); + WindowMinSize = ImTrunc(WindowMinSize * scale_factor); + WindowBorderHoverPadding = ImTrunc(WindowBorderHoverPadding * scale_factor); + ChildRounding = ImTrunc(ChildRounding * scale_factor); + PopupRounding = ImTrunc(PopupRounding * scale_factor); + FramePadding = ImTrunc(FramePadding * scale_factor); + FrameRounding = ImTrunc(FrameRounding * scale_factor); + ItemSpacing = ImTrunc(ItemSpacing * scale_factor); + ItemInnerSpacing = ImTrunc(ItemInnerSpacing * scale_factor); + CellPadding = ImTrunc(CellPadding * scale_factor); + TouchExtraPadding = ImTrunc(TouchExtraPadding * scale_factor); + IndentSpacing = ImTrunc(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImTrunc(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImTrunc(ScrollbarSize * scale_factor); + ScrollbarRounding = ImTrunc(ScrollbarRounding * scale_factor); + GrabMinSize = ImTrunc(GrabMinSize * scale_factor); + GrabRounding = ImTrunc(GrabRounding * scale_factor); + LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); + ImageBorderSize = ImTrunc(ImageBorderSize * scale_factor); + TabRounding = ImTrunc(TabRounding * scale_factor); + TabCloseButtonMinWidthSelected = (TabCloseButtonMinWidthSelected > 0.0f && TabCloseButtonMinWidthSelected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthSelected * scale_factor) : TabCloseButtonMinWidthSelected; + TabCloseButtonMinWidthUnselected = (TabCloseButtonMinWidthUnselected > 0.0f && TabCloseButtonMinWidthUnselected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthUnselected * scale_factor) : TabCloseButtonMinWidthUnselected; + TabBarOverlineSize = ImTrunc(TabBarOverlineSize * scale_factor); + SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); + DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImTrunc(MouseCursorScale * scale_factor); +} + +ImGuiIO::ImGuiIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + IM_STATIC_ASSERT(IM_ARRAYSIZE(ImGuiIO::MouseDown) == ImGuiMouseButton_COUNT && IM_ARRAYSIZE(ImGuiIO::MouseClicked) == ImGuiMouseButton_COUNT); + + // Settings + ConfigFlags = ImGuiConfigFlags_None; + BackendFlags = ImGuiBackendFlags_None; + DisplaySize = ImVec2(-1.0f, -1.0f); + DeltaTime = 1.0f / 60.0f; + IniSavingRate = 5.0f; + IniFilename = "imgui.ini"; // Important: "imgui.ini" is relative to current working dir, most apps will want to lock this to an absolute path (e.g. same path as executables). + LogFilename = "imgui_log.txt"; + UserData = NULL; + + Fonts = NULL; + FontGlobalScale = 1.0f; + FontDefault = NULL; + FontAllowUserScaling = false; + DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Keyboard/Gamepad Navigation options + ConfigNavSwapGamepadButtons = false; + ConfigNavMoveSetMousePos = false; + ConfigNavCaptureKeyboard = true; + ConfigNavEscapeClearFocusItem = true; + ConfigNavEscapeClearFocusWindow = false; + ConfigNavCursorVisibleAuto = true; + ConfigNavCursorVisibleAlways = false; + + // Miscellaneous options + MouseDrawCursor = false; +#ifdef __APPLE__ + ConfigMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag +#else + ConfigMacOSXBehaviors = false; +#endif + ConfigInputTrickleEventQueue = true; + ConfigInputTextCursorBlink = true; + ConfigInputTextEnterKeepActive = false; + ConfigDragClickToInputText = false; + ConfigWindowsResizeFromEdges = true; + ConfigWindowsMoveFromTitleBarOnly = false; + ConfigWindowsCopyContentsWithCtrlC = false; + ConfigScrollbarScrollByPage = true; + ConfigMemoryCompactTimer = 60.0f; + ConfigDebugIsDebuggerPresent = false; + ConfigDebugHighlightIdConflicts = true; + ConfigDebugHighlightIdConflictsShowItemPicker = true; + ConfigDebugBeginReturnValueOnce = false; + ConfigDebugBeginReturnValueLoop = false; + + ConfigErrorRecovery = true; + ConfigErrorRecoveryEnableAssert = true; + ConfigErrorRecoveryEnableDebugLog = true; + ConfigErrorRecoveryEnableTooltip = true; + + // Inputs Behaviors + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + MouseDragThreshold = 6.0f; + KeyRepeatDelay = 0.275f; + KeyRepeatRate = 0.050f; + + // Platform Functions + // Note: Initialize() will setup default clipboard/ime handlers. + BackendPlatformName = BackendRendererName = NULL; + BackendPlatformUserData = BackendRendererUserData = BackendLanguageUserData = NULL; + + // Input (NB: we already have memset zero the entire structure!) + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); + MouseSource = ImGuiMouseSource_Mouse; + for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(KeysData); i++) { KeysData[i].DownDuration = KeysData[i].DownDurationPrev = -1.0f; } + AppAcceptingEvents = true; +} + +// Pass in translated ASCII characters for text input. +// - with glfw you can get those from the callback set in glfwSetCharCallback() +// - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message +// FIXME: Should in theory be called "AddCharacterEvent()" to be consistent with new API +void ImGuiIO::AddInputCharacter(unsigned int c) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (c == 0 || !AppAcceptingEvents) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Text; + e.Source = ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Text.Char = c; + g.InputEventsQueue.push_back(e); +} + +// UTF16 strings use surrogate pairs to encode codepoints >= 0x10000, so +// we should save the high surrogate. +void ImGuiIO::AddInputCharacterUTF16(ImWchar16 c) +{ + if ((c == 0 && InputQueueSurrogate == 0) || !AppAcceptingEvents) + return; + + if ((c & 0xFC00) == 0xD800) // High surrogate, must save + { + if (InputQueueSurrogate != 0) + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + InputQueueSurrogate = c; + return; + } + + ImWchar cp = c; + if (InputQueueSurrogate != 0) + { + if ((c & 0xFC00) != 0xDC00) // Invalid low surrogate + { + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + } + else + { +#if IM_UNICODE_CODEPOINT_MAX == 0xFFFF + cp = IM_UNICODE_CODEPOINT_INVALID; // Codepoint will not fit in ImWchar +#else + cp = (ImWchar)(((InputQueueSurrogate - 0xD800) << 10) + (c - 0xDC00) + 0x10000); +#endif + } + + InputQueueSurrogate = 0; + } + AddInputCharacter((unsigned)cp); +} + +void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) +{ + if (!AppAcceptingEvents) + return; + while (*utf8_chars != 0) + { + unsigned int c = 0; + utf8_chars += ImTextCharFromUtf8(&c, utf8_chars, NULL); + AddInputCharacter(c); + } +} + +// Clear all incoming events. +void ImGuiIO::ClearEventsQueue() +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsQueue.clear(); +} + +// Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. +void ImGuiIO::ClearInputKeys() +{ + ImGuiContext& g = *Ctx; + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key++) + { + if (ImGui::IsMouseKey((ImGuiKey)key)) + continue; + ImGuiKeyData* key_data = &g.IO.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->Down = false; + key_data->DownDuration = -1.0f; + key_data->DownDurationPrev = -1.0f; + } + KeyCtrl = KeyShift = KeyAlt = KeySuper = false; + KeyMods = ImGuiMod_None; + InputQueueCharacters.resize(0); // Behavior of old ClearInputCharacters(). +} + +void ImGuiIO::ClearInputMouse() +{ + for (ImGuiKey key = ImGuiKey_Mouse_BEGIN; key < ImGuiKey_Mouse_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->Down = false; + key_data->DownDuration = -1.0f; + key_data->DownDurationPrev = -1.0f; + } + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + for (int n = 0; n < IM_ARRAYSIZE(MouseDown); n++) + { + MouseDown[n] = false; + MouseDownDuration[n] = MouseDownDurationPrev[n] = -1.0f; + } + MouseWheel = MouseWheelH = 0.0f; +} + +// Removed this as it is ambiguous/misleading and generally incorrect to use with the existence of a higher-level input queue. +// Current frame character buffer is now also cleared by ClearInputKeys(). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +void ImGuiIO::ClearInputCharacters() +{ + InputQueueCharacters.resize(0); +} +#endif + +static ImGuiInputEvent* FindLatestInputEvent(ImGuiContext* ctx, ImGuiInputEventType type, int arg = -1) +{ + ImGuiContext& g = *ctx; + for (int n = g.InputEventsQueue.Size - 1; n >= 0; n--) + { + ImGuiInputEvent* e = &g.InputEventsQueue[n]; + if (e->Type != type) + continue; + if (type == ImGuiInputEventType_Key && e->Key.Key != arg) + continue; + if (type == ImGuiInputEventType_MouseButton && e->MouseButton.Button != arg) + continue; + return e; + } + return NULL; +} + +// Queue a new key down/up event. +// - ImGuiKey key: Translated key (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) +// - bool down: Is the key down? use false to signify a key release. +// - float analog_value: 0.0f..1.0f +// IMPORTANT: THIS FUNCTION AND OTHER "ADD" GRABS THE CONTEXT FROM OUR INSTANCE. +// WE NEED TO ENSURE THAT ALL FUNCTION CALLS ARE FULFILLING THIS, WHICH IS WHY GetKeyData() HAS AN EXPLICIT CONTEXT. +void ImGuiIO::AddKeyAnalogEvent(ImGuiKey key, bool down, float analog_value) +{ + //if (e->Down) { IMGUI_DEBUG_LOG_IO("AddKeyEvent() Key='%s' %d, NativeKeycode = %d, NativeScancode = %d\n", ImGui::GetKeyName(e->Key), e->Down, e->NativeKeycode, e->NativeScancode); } + IM_ASSERT(Ctx != NULL); + if (key == ImGuiKey_None || !AppAcceptingEvents) + return; + ImGuiContext& g = *Ctx; + IM_ASSERT(ImGui::IsNamedKeyOrMod(key)); // Backend needs to pass a valid ImGuiKey_ constant. 0..511 values are legacy native key codes which are not accepted by this API. + IM_ASSERT(ImGui::IsAliasKey(key) == false); // Backend cannot submit ImGuiKey_MouseXXX values they are automatically inferred from AddMouseXXX() events. + + // MacOS: swap Cmd(Super) and Ctrl + if (g.IO.ConfigMacOSXBehaviors) + { + if (key == ImGuiMod_Super) { key = ImGuiMod_Ctrl; } + else if (key == ImGuiMod_Ctrl) { key = ImGuiMod_Super; } + else if (key == ImGuiKey_LeftSuper) { key = ImGuiKey_LeftCtrl; } + else if (key == ImGuiKey_RightSuper){ key = ImGuiKey_RightCtrl; } + else if (key == ImGuiKey_LeftCtrl) { key = ImGuiKey_LeftSuper; } + else if (key == ImGuiKey_RightCtrl) { key = ImGuiKey_RightSuper; } + } + + // Filter duplicate (in particular: key mods and gamepad analog values are commonly spammed) + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)key); + const ImGuiKeyData* key_data = ImGui::GetKeyData(&g, key); + const bool latest_key_down = latest_event ? latest_event->Key.Down : key_data->Down; + const float latest_key_analog = latest_event ? latest_event->Key.AnalogValue : key_data->AnalogValue; + if (latest_key_down == down && latest_key_analog == analog_value) + return; + + // Add event + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Key; + e.Source = ImGui::IsGamepadKey(key) ? ImGuiInputSource_Gamepad : ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Key.Key = key; + e.Key.Down = down; + e.Key.AnalogValue = analog_value; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddKeyEvent(ImGuiKey key, bool down) +{ + if (!AppAcceptingEvents) + return; + AddKeyAnalogEvent(key, down, down ? 1.0f : 0.0f); +} + +// [Optional] Call after AddKeyEvent(). +// Specify native keycode, scancode + Specify index for legacy <1.87 IsKeyXXX() functions with native indices. +// If you are writing a backend in 2022 or don't use IsKeyXXX() with native values that are not ImGuiKey values, you can avoid calling this. +void ImGuiIO::SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index) +{ + if (key == ImGuiKey_None) + return; + IM_ASSERT(ImGui::IsNamedKey(key)); // >= 512 + IM_ASSERT(native_legacy_index == -1 || ImGui::IsLegacyKey((ImGuiKey)native_legacy_index)); // >= 0 && <= 511 + IM_UNUSED(key); // Yet unused + IM_UNUSED(native_keycode); // Yet unused + IM_UNUSED(native_scancode); // Yet unused + IM_UNUSED(native_legacy_index); // Yet unused +} + +// Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. +void ImGuiIO::SetAppAcceptingEvents(bool accepting_events) +{ + AppAcceptingEvents = accepting_events; +} + +// Queue a mouse move event +void ImGuiIO::AddMousePosEvent(float x, float y) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (!AppAcceptingEvents) + return; + + // Apply same flooring as UpdateMouseInputs() + ImVec2 pos((x > -FLT_MAX) ? ImFloor(x) : x, (y > -FLT_MAX) ? ImFloor(y) : y); + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MousePos); + const ImVec2 latest_pos = latest_event ? ImVec2(latest_event->MousePos.PosX, latest_event->MousePos.PosY) : g.IO.MousePos; + if (latest_pos.x == pos.x && latest_pos.y == pos.y) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MousePos; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MousePos.PosX = pos.x; + e.MousePos.PosY = pos.y; + e.MousePos.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddMouseButtonEvent(int mouse_button, bool down) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + if (!AppAcceptingEvents) + return; + + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click: handle held button. + if (ConfigMacOSXBehaviors && mouse_button == 0 && MouseCtrlLeftAsRightClick) + { + // Order of both statements matterns: this event will still release mouse button 1 + mouse_button = 1; + if (!down) + MouseCtrlLeftAsRightClick = false; + } + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MouseButton, (int)mouse_button); + const bool latest_button_down = latest_event ? latest_event->MouseButton.Down : g.IO.MouseDown[mouse_button]; + if (latest_button_down == down) + return; + + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click. + // - Note that this is actual physical Ctrl which is ImGuiMod_Super for us. + // - At this point we want from !down to down, so this is handling the initial press. + if (ConfigMacOSXBehaviors && mouse_button == 0 && down) + { + const ImGuiInputEvent* latest_super_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)ImGuiMod_Super); + if (latest_super_event ? latest_super_event->Key.Down : g.IO.KeySuper) + { + IMGUI_DEBUG_LOG_IO("[io] Super+Left Click aliased into Right Click\n"); + MouseCtrlLeftAsRightClick = true; + AddMouseButtonEvent(1, true); // This is just quicker to write that passing through, as we need to filter duplicate again. + return; + } + } + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseButton; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseButton.Button = mouse_button; + e.MouseButton.Down = down; + e.MouseButton.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +// Queue a mouse wheel event (some mouse/API may only have a Y component) +void ImGuiIO::AddMouseWheelEvent(float wheel_x, float wheel_y) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate (unlike most events, wheel values are relative and easy to filter) + if (!AppAcceptingEvents || (wheel_x == 0.0f && wheel_y == 0.0f)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseWheel; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseWheel.WheelX = wheel_x; + e.MouseWheel.WheelY = wheel_y; + e.MouseWheel.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +// This is not a real event, the data is latched in order to be stored in actual Mouse events. +// This is so that duplicate events (e.g. Windows sending extraneous WM_MOUSEMOVE) gets filtered and are not leading to actual source changes. +void ImGuiIO::AddMouseSourceEvent(ImGuiMouseSource source) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsNextMouseSource = source; +} + +void ImGuiIO::AddFocusEvent(bool focused) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Focus); + const bool latest_focused = latest_event ? latest_event->AppFocused.Focused : !g.IO.AppFocusLost; + if (latest_focused == focused || (ConfigDebugIgnoreFocusLoss && !focused)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Focus; + e.EventId = g.InputEventsNextEventId++; + e.AppFocused.Focused = focused; + g.InputEventsQueue.push_back(e); +} + +ImGuiPlatformIO::ImGuiPlatformIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + Platform_LocaleDecimalPoint = '.'; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +//----------------------------------------------------------------------------- + +ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments) +{ + IM_ASSERT(num_segments > 0); // Use ImBezierCubicClosestPointCasteljau() + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + ImVec2 p_current = ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + return p_closest; +} + +// Closely mimics PathBezierToCasteljau() in imgui_draw.cpp +static void ImBezierCubicClosestPointCasteljauStep(const ImVec2& p, ImVec2& p_closest, ImVec2& p_last, float& p_closest_dist2, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + ImVec2 p_current(x4, y4); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + else if (level < 10) + { + float x12 = (x1 + x2)*0.5f, y12 = (y1 + y2)*0.5f; + float x23 = (x2 + x3)*0.5f, y23 = (y2 + y3)*0.5f; + float x34 = (x3 + x4)*0.5f, y34 = (y3 + y4)*0.5f; + float x123 = (x12 + x23)*0.5f, y123 = (y12 + y23)*0.5f; + float x234 = (x23 + x34)*0.5f, y234 = (y23 + y34)*0.5f; + float x1234 = (x123 + x234)*0.5f, y1234 = (y123 + y234)*0.5f; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +// tess_tol is generally the same value you would find in ImGui::GetStyle().CurveTessellationTol +// Because those ImXXX functions are lower-level than ImGui:: we cannot access this value automatically. +ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol) +{ + IM_ASSERT(tess_tol > 0.0f); + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, tess_tol, 0); + return p_closest; +} + +ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) +{ + ImVec2 ap = p - a; + ImVec2 ab_dir = b - a; + float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if (dot < 0.0f) + return a; + float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if (dot > ab_len_sqr) + return b; + return a + ab_dir * dot / ab_len_sqr; +} + +bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + bool b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0f; + bool b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0f; + bool b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0f; + return ((b1 == b2) && (b2 == b3)); +} + +void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w) +{ + ImVec2 v0 = b - a; + ImVec2 v1 = c - a; + ImVec2 v2 = p - a; + const float denom = v0.x * v1.y - v1.x * v0.y; + out_v = (v2.x * v1.y - v1.x * v2.y) / denom; + out_w = (v0.x * v2.y - v2.x * v0.y) / denom; + out_u = 1.0f - out_v - out_w; +} + +ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p) +{ + ImVec2 proj_ab = ImLineClosestPoint(a, b, p); + ImVec2 proj_bc = ImLineClosestPoint(b, c, p); + ImVec2 proj_ca = ImLineClosestPoint(c, a, p); + float dist2_ab = ImLengthSqr(p - proj_ab); + float dist2_bc = ImLengthSqr(p - proj_bc); + float dist2_ca = ImLengthSqr(p - proj_ca); + float m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca)); + if (m == dist2_ab) + return proj_ab; + if (m == dist2_bc) + return proj_bc; + return proj_ca; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +//----------------------------------------------------------------------------- + +// Consider using _stricmp/_strnicmp under Windows or strcasecmp/strncasecmp. We don't actually use either ImStricmp/ImStrnicmp in the codebase any more. +int ImStricmp(const char* str1, const char* str2) +{ + int d; + while ((d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; } + return d; +} + +int ImStrnicmp(const char* str1, const char* str2, size_t count) +{ + int d = 0; + while (count > 0 && (d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; count--; } + return d; +} + +void ImStrncpy(char* dst, const char* src, size_t count) +{ + if (count < 1) + return; + if (count > 1) + strncpy(dst, src, count - 1); + dst[count - 1] = 0; +} + +char* ImStrdup(const char* str) +{ + size_t len = ImStrlen(str); + void* buf = IM_ALLOC(len + 1); + return (char*)memcpy(buf, (const void*)str, len + 1); +} + +char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src) +{ + size_t dst_buf_size = p_dst_size ? *p_dst_size : ImStrlen(dst) + 1; + size_t src_size = ImStrlen(src) + 1; + if (dst_buf_size < src_size) + { + IM_FREE(dst); + dst = (char*)IM_ALLOC(src_size); + if (p_dst_size) + *p_dst_size = src_size; + } + return (char*)memcpy(dst, (const void*)src, src_size); +} + +const char* ImStrchrRange(const char* str, const char* str_end, char c) +{ + const char* p = (const char*)ImMemchr(str, (int)c, str_end - str); + return p; +} + +int ImStrlenW(const ImWchar* str) +{ + //return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bit + int n = 0; + while (*str++) n++; + return n; +} + +// Find end-of-line. Return pointer will point to either first \n, either str_end. +const char* ImStreolRange(const char* str, const char* str_end) +{ + const char* p = (const char*)ImMemchr(str, '\n', str_end - str); + return p ? p : str_end; +} + +const char* ImStrbol(const char* buf_mid_line, const char* buf_begin) // find beginning-of-line +{ + IM_ASSERT_PARANOID(buf_mid_line >= buf_begin && buf_mid_line <= buf_begin + ImStrlen(buf_begin)); + while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') + buf_mid_line--; + return buf_mid_line; +} + +const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) +{ + if (!needle_end) + needle_end = needle + ImStrlen(needle); + + const char un0 = (char)ImToUpper(*needle); + while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) + { + if (ImToUpper(*haystack) == un0) + { + const char* b = needle + 1; + for (const char* a = haystack + 1; b < needle_end; a++, b++) + if (ImToUpper(*a) != ImToUpper(*b)) + break; + if (b == needle_end) + return haystack; + } + haystack++; + } + return NULL; +} + +// Trim str by offsetting contents when there's leading data + writing a \0 at the trailing position. We use this in situation where the cost is negligible. +void ImStrTrimBlanks(char* buf) +{ + char* p = buf; + while (p[0] == ' ' || p[0] == '\t') // Leading blanks + p++; + char* p_start = p; + while (*p != 0) // Find end of string + p++; + while (p > p_start && (p[-1] == ' ' || p[-1] == '\t')) // Trailing blanks + p--; + if (p_start != buf) // Copy memory if we had leading blanks + memmove(buf, p_start, p - p_start); + buf[p - p_start] = 0; // Zero terminate +} + +const char* ImStrSkipBlank(const char* str) +{ + while (str[0] == ' ' || str[0] == '\t') + str++; + return str; +} + +// A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). +// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. +// B) When buf==NULL vsnprintf() will return the output size. +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + +// We support stb_sprintf which is much faster (see: https://github.com/nothings/stb/blob/master/stb_sprintf.h) +// You may set IMGUI_USE_STB_SPRINTF to use our default wrapper, or set IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +// and setup the wrapper yourself. (FIXME-OPT: Some of our high-level operations such as ImGuiTextBuffer::appendfv() are +// designed using two-passes worst case, which probably could be improved using the stbsp_vsprintfcb() function.) +#ifdef IMGUI_USE_STB_SPRINTF +#ifndef IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION +#define STB_SPRINTF_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_SPRINTF_FILENAME +#include IMGUI_STB_SPRINTF_FILENAME +#else +#include "stb_sprintf.h" +#endif +#endif // #ifdef IMGUI_USE_STB_SPRINTF + +#if defined(_MSC_VER) && !defined(vsnprintf) +#define vsnprintf _vsnprintf +#endif + +int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} + +int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) +{ +#ifdef IMGUI_USE_STB_SPRINTF + int w = stbsp_vsnprintf(buf, (int)buf_size, fmt, args); +#else + int w = vsnprintf(buf, buf_size, fmt, args); +#endif + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} +#endif // #ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + +void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); + va_end(args); +} + +// FIXME: Should rework API toward allowing multiple in-flight temp buffers (easier and safer for caller) +// by making the caller acquire a temp buffer token, with either explicit or destructor release, e.g. +// ImGuiTempBufferToken token; +// ImFormatStringToTempBuffer(token, ...); +void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + { + const char* buf = va_arg(args, const char*); // Skip formatting when using "%s" + if (buf == NULL) + buf = "(null)"; + *out_buf = buf; + if (out_buf_end) { *out_buf_end = buf + ImStrlen(buf); } + } + else if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '*' && fmt[3] == 's' && fmt[4] == 0) + { + int buf_len = va_arg(args, int); // Skip formatting when using "%.*s" + const char* buf = va_arg(args, const char*); + if (buf == NULL) + { + buf = "(null)"; + buf_len = ImMin(buf_len, 6); + } + *out_buf = buf; + *out_buf_end = buf + buf_len; // Disallow not passing 'out_buf_end' here. User is expected to use it. + } + else + { + int buf_len = ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); + *out_buf = g.TempBuffer.Data; + if (out_buf_end) { *out_buf_end = g.TempBuffer.Data + buf_len; } + } +} + +#ifndef IMGUI_ENABLE_SSE4_2_CRC +// CRC32 needs a 1KB lookup table (not cache friendly) +// Although the code to generate the table is simple and shorter than the table itself, using a const table allows us to easily: +// - avoid an unnecessary branch/memory tap, - keep the ImHashXXX functions usable by static constructors, - make it thread-safe. +static const ImU32 GCrc32LookupTable[256] = +{ +#ifdef IMGUI_USE_LEGACY_CRC32_ADLER + // Legacy CRC32-adler table used pre 1.91.6 (before 2024/11/27). Only use if you cannot afford invalidating old .ini data. + 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, + 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, + 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, + 0x26D930AC,0x51DE003A,0xC8D75180,0xBFD06116,0x21B4F4B5,0x56B3C423,0xCFBA9599,0xB8BDA50F,0x2802B89E,0x5F058808,0xC60CD9B2,0xB10BE924,0x2F6F7C87,0x58684C11,0xC1611DAB,0xB6662D3D, + 0x76DC4190,0x01DB7106,0x98D220BC,0xEFD5102A,0x71B18589,0x06B6B51F,0x9FBFE4A5,0xE8B8D433,0x7807C9A2,0x0F00F934,0x9609A88E,0xE10E9818,0x7F6A0DBB,0x086D3D2D,0x91646C97,0xE6635C01, + 0x6B6B51F4,0x1C6C6162,0x856530D8,0xF262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0xF50FC457,0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0xFCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0xFBD44C65, + 0x4DB26158,0x3AB551CE,0xA3BC0074,0xD4BB30E2,0x4ADFA541,0x3DD895D7,0xA4D1C46D,0xD3D6F4FB,0x4369E96A,0x346ED9FC,0xAD678846,0xDA60B8D0,0x44042D73,0x33031DE5,0xAA0A4C5F,0xDD0D7CC9, + 0x5005713C,0x270241AA,0xBE0B1010,0xC90C2086,0x5768B525,0x206F85B3,0xB966D409,0xCE61E49F,0x5EDEF90E,0x29D9C998,0xB0D09822,0xC7D7A8B4,0x59B33D17,0x2EB40D81,0xB7BD5C3B,0xC0BA6CAD, + 0xEDB88320,0x9ABFB3B6,0x03B6E20C,0x74B1D29A,0xEAD54739,0x9DD277AF,0x04DB2615,0x73DC1683,0xE3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0xE40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1, + 0xF00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0xF762575D,0x806567CB,0x196C3671,0x6E6B06E7,0xFED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0xF9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5, + 0xD6D6A3E8,0xA1D1937E,0x38D8C2C4,0x4FDFF252,0xD1BB67F1,0xA6BC5767,0x3FB506DD,0x48B2364B,0xD80D2BDA,0xAF0A1B4C,0x36034AF6,0x41047A60,0xDF60EFC3,0xA867DF55,0x316E8EEF,0x4669BE79, + 0xCB61B38C,0xBC66831A,0x256FD2A0,0x5268E236,0xCC0C7795,0xBB0B4703,0x220216B9,0x5505262F,0xC5BA3BBE,0xB2BD0B28,0x2BB45A92,0x5CB36A04,0xC2D7FFA7,0xB5D0CF31,0x2CD99E8B,0x5BDEAE1D, + 0x9B64C2B0,0xEC63F226,0x756AA39C,0x026D930A,0x9C0906A9,0xEB0E363F,0x72076785,0x05005713,0x95BF4A82,0xE2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0xE5D5BE0D,0x7CDCEFB7,0x0BDBDF21, + 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, + 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, + 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, +#else + // CRC32c table compatible with SSE 4.2 instructions + 0x00000000,0xF26B8303,0xE13B70F7,0x1350F3F4,0xC79A971F,0x35F1141C,0x26A1E7E8,0xD4CA64EB,0x8AD958CF,0x78B2DBCC,0x6BE22838,0x9989AB3B,0x4D43CFD0,0xBF284CD3,0xAC78BF27,0x5E133C24, + 0x105EC76F,0xE235446C,0xF165B798,0x030E349B,0xD7C45070,0x25AFD373,0x36FF2087,0xC494A384,0x9A879FA0,0x68EC1CA3,0x7BBCEF57,0x89D76C54,0x5D1D08BF,0xAF768BBC,0xBC267848,0x4E4DFB4B, + 0x20BD8EDE,0xD2D60DDD,0xC186FE29,0x33ED7D2A,0xE72719C1,0x154C9AC2,0x061C6936,0xF477EA35,0xAA64D611,0x580F5512,0x4B5FA6E6,0xB93425E5,0x6DFE410E,0x9F95C20D,0x8CC531F9,0x7EAEB2FA, + 0x30E349B1,0xC288CAB2,0xD1D83946,0x23B3BA45,0xF779DEAE,0x05125DAD,0x1642AE59,0xE4292D5A,0xBA3A117E,0x4851927D,0x5B016189,0xA96AE28A,0x7DA08661,0x8FCB0562,0x9C9BF696,0x6EF07595, + 0x417B1DBC,0xB3109EBF,0xA0406D4B,0x522BEE48,0x86E18AA3,0x748A09A0,0x67DAFA54,0x95B17957,0xCBA24573,0x39C9C670,0x2A993584,0xD8F2B687,0x0C38D26C,0xFE53516F,0xED03A29B,0x1F682198, + 0x5125DAD3,0xA34E59D0,0xB01EAA24,0x42752927,0x96BF4DCC,0x64D4CECF,0x77843D3B,0x85EFBE38,0xDBFC821C,0x2997011F,0x3AC7F2EB,0xC8AC71E8,0x1C661503,0xEE0D9600,0xFD5D65F4,0x0F36E6F7, + 0x61C69362,0x93AD1061,0x80FDE395,0x72966096,0xA65C047D,0x5437877E,0x4767748A,0xB50CF789,0xEB1FCBAD,0x197448AE,0x0A24BB5A,0xF84F3859,0x2C855CB2,0xDEEEDFB1,0xCDBE2C45,0x3FD5AF46, + 0x7198540D,0x83F3D70E,0x90A324FA,0x62C8A7F9,0xB602C312,0x44694011,0x5739B3E5,0xA55230E6,0xFB410CC2,0x092A8FC1,0x1A7A7C35,0xE811FF36,0x3CDB9BDD,0xCEB018DE,0xDDE0EB2A,0x2F8B6829, + 0x82F63B78,0x709DB87B,0x63CD4B8F,0x91A6C88C,0x456CAC67,0xB7072F64,0xA457DC90,0x563C5F93,0x082F63B7,0xFA44E0B4,0xE9141340,0x1B7F9043,0xCFB5F4A8,0x3DDE77AB,0x2E8E845F,0xDCE5075C, + 0x92A8FC17,0x60C37F14,0x73938CE0,0x81F80FE3,0x55326B08,0xA759E80B,0xB4091BFF,0x466298FC,0x1871A4D8,0xEA1A27DB,0xF94AD42F,0x0B21572C,0xDFEB33C7,0x2D80B0C4,0x3ED04330,0xCCBBC033, + 0xA24BB5A6,0x502036A5,0x4370C551,0xB11B4652,0x65D122B9,0x97BAA1BA,0x84EA524E,0x7681D14D,0x2892ED69,0xDAF96E6A,0xC9A99D9E,0x3BC21E9D,0xEF087A76,0x1D63F975,0x0E330A81,0xFC588982, + 0xB21572C9,0x407EF1CA,0x532E023E,0xA145813D,0x758FE5D6,0x87E466D5,0x94B49521,0x66DF1622,0x38CC2A06,0xCAA7A905,0xD9F75AF1,0x2B9CD9F2,0xFF56BD19,0x0D3D3E1A,0x1E6DCDEE,0xEC064EED, + 0xC38D26C4,0x31E6A5C7,0x22B65633,0xD0DDD530,0x0417B1DB,0xF67C32D8,0xE52CC12C,0x1747422F,0x49547E0B,0xBB3FFD08,0xA86F0EFC,0x5A048DFF,0x8ECEE914,0x7CA56A17,0x6FF599E3,0x9D9E1AE0, + 0xD3D3E1AB,0x21B862A8,0x32E8915C,0xC083125F,0x144976B4,0xE622F5B7,0xF5720643,0x07198540,0x590AB964,0xAB613A67,0xB831C993,0x4A5A4A90,0x9E902E7B,0x6CFBAD78,0x7FAB5E8C,0x8DC0DD8F, + 0xE330A81A,0x115B2B19,0x020BD8ED,0xF0605BEE,0x24AA3F05,0xD6C1BC06,0xC5914FF2,0x37FACCF1,0x69E9F0D5,0x9B8273D6,0x88D28022,0x7AB90321,0xAE7367CA,0x5C18E4C9,0x4F48173D,0xBD23943E, + 0xF36E6F75,0x0105EC76,0x12551F82,0xE03E9C81,0x34F4F86A,0xC69F7B69,0xD5CF889D,0x27A40B9E,0x79B737BA,0x8BDCB4B9,0x988C474D,0x6AE7C44E,0xBE2DA0A5,0x4C4623A6,0x5F16D052,0xAD7D5351 +#endif +}; +#endif + +// Known size hash +// It is ok to call ImHashData on a string with known length but the ### operator won't be supported. +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashData(const void* data_p, size_t data_size, ImGuiID seed) +{ + ImU32 crc = ~seed; + const unsigned char* data = (const unsigned char*)data_p; + const unsigned char *data_end = (const unsigned char*)data_p + data_size; +#ifndef IMGUI_ENABLE_SSE4_2_CRC + const ImU32* crc32_lut = GCrc32LookupTable; + while (data < data_end) + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *data++]; + return ~crc; +#else + while (data + 4 <= data_end) + { + crc = _mm_crc32_u32(crc, *(ImU32*)data); + data += 4; + } + while (data < data_end) + crc = _mm_crc32_u8(crc, *data++); + return ~crc; +#endif +} + +// Zero-terminated string hash, with support for ### to reset back to seed value +// We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. +// Because this syntax is rarely used we are optimizing for the common case. +// - If we reach ### in the string we discard the hash so far and reset to the seed. +// - We don't do 'current += 2; continue;' after handling ### to keep the code smaller/faster (measured ~10% diff in Debug build) +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImGuiID ImHashStr(const char* data_p, size_t data_size, ImGuiID seed) +{ + seed = ~seed; + ImU32 crc = seed; + const unsigned char* data = (const unsigned char*)data_p; +#ifndef IMGUI_ENABLE_SSE4_2_CRC + const ImU32* crc32_lut = GCrc32LookupTable; +#endif + if (data_size != 0) + { + while (data_size-- != 0) + { + unsigned char c = *data++; + if (c == '#' && data_size >= 2 && data[0] == '#' && data[1] == '#') + crc = seed; +#ifndef IMGUI_ENABLE_SSE4_2_CRC + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; +#else + crc = _mm_crc32_u8(crc, c); +#endif + } + } + else + { + while (unsigned char c = *data++) + { + if (c == '#' && data[0] == '#' && data[1] == '#') + crc = seed; +#ifndef IMGUI_ENABLE_SSE4_2_CRC + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; +#else + crc = _mm_crc32_u8(crc, c); +#endif + } + } + return ~crc; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (File functions) +//----------------------------------------------------------------------------- + +// Default file functions +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +ImFileHandle ImFileOpen(const char* filename, const char* mode) +{ +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && (defined(__MINGW32__) || (!defined(__CYGWIN__) && !defined(__GNUC__))) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. + // Previously we used ImTextCountCharsFromUtf8/ImTextStrFromUtf8 here but we now need to support ImWchar16 and ImWchar32! + const int filename_wsize = ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + const int mode_wsize = ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); + + // Use stack buffer if possible, otherwise heap buffer. Sizes include zero terminator. + // We don't rely on current ImGuiContext as this is implied to be a helper function which doesn't depend on it (see #7314). + wchar_t local_temp_stack[FILENAME_MAX]; + ImVector local_temp_heap; + if (filename_wsize + mode_wsize > IM_ARRAYSIZE(local_temp_stack)) + local_temp_heap.resize(filename_wsize + mode_wsize); + wchar_t* filename_wbuf = local_temp_heap.Data ? local_temp_heap.Data : local_temp_stack; + wchar_t* mode_wbuf = filename_wbuf + filename_wsize; + ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, filename_wbuf, filename_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, mode_wbuf, mode_wsize); + return ::_wfopen(filename_wbuf, mode_wbuf); +#else + return fopen(filename, mode); +#endif +} + +// We should in theory be using fseeko()/ftello() with off_t and _fseeki64()/_ftelli64() with __int64, waiting for the PR that does that in a very portable pre-C++11 zero-warnings way. +bool ImFileClose(ImFileHandle f) { return fclose(f) == 0; } +ImU64 ImFileGetSize(ImFileHandle f) { long off = 0, sz = 0; return ((off = ftell(f)) != -1 && !fseek(f, 0, SEEK_END) && (sz = ftell(f)) != -1 && !fseek(f, off, SEEK_SET)) ? (ImU64)sz : (ImU64)-1; } +ImU64 ImFileRead(void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fread(data, (size_t)sz, (size_t)count, f); } +ImU64 ImFileWrite(const void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fwrite(data, (size_t)sz, (size_t)count, f); } +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +// Helper: Load file content into memory +// Memory allocated with IM_ALLOC(), must be freed by user using IM_FREE() == ImGui::MemFree() +// This can't really be used with "rt" because fseek size won't match read size. +void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size, int padding_bytes) +{ + IM_ASSERT(filename && mode); + if (out_file_size) + *out_file_size = 0; + + ImFileHandle f; + if ((f = ImFileOpen(filename, mode)) == NULL) + return NULL; + + size_t file_size = (size_t)ImFileGetSize(f); + if (file_size == (size_t)-1) + { + ImFileClose(f); + return NULL; + } + + void* file_data = IM_ALLOC(file_size + padding_bytes); + if (file_data == NULL) + { + ImFileClose(f); + return NULL; + } + if (ImFileRead(file_data, 1, file_size, f) != file_size) + { + ImFileClose(f); + IM_FREE(file_data); + return NULL; + } + if (padding_bytes > 0) + memset((void*)(((char*)file_data) + file_size), 0, (size_t)padding_bytes); + + ImFileClose(f); + if (out_file_size) + *out_file_size = file_size; + + return file_data; +} + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (ImText* functions) +//----------------------------------------------------------------------------- + +IM_MSVC_RUNTIME_CHECKS_OFF + +// Convert UTF-8 to 32-bit character, process single character input. +// A nearly-branchless UTF-8 decoder, based on work of Christopher Wellons (https://github.com/skeeto/branchless-utf8). +// We handle UTF-8 decoding error by skipping forward. +int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end) +{ + static const char lengths[32] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + static const int masks[] = { 0x00, 0x7f, 0x1f, 0x0f, 0x07 }; + static const uint32_t mins[] = { 0x400000, 0, 0x80, 0x800, 0x10000 }; + static const int shiftc[] = { 0, 18, 12, 6, 0 }; + static const int shifte[] = { 0, 6, 4, 2, 0 }; + int len = lengths[*(const unsigned char*)in_text >> 3]; + int wanted = len + (len ? 0 : 1); + + if (in_text_end == NULL) + in_text_end = in_text + wanted; // Max length, nulls will be taken into account. + + // Copy at most 'len' bytes, stop copying at 0 or past in_text_end. Branch predictor does a good job here, + // so it is fast even with excessive branching. + unsigned char s[4]; + s[0] = in_text + 0 < in_text_end ? in_text[0] : 0; + s[1] = in_text + 1 < in_text_end ? in_text[1] : 0; + s[2] = in_text + 2 < in_text_end ? in_text[2] : 0; + s[3] = in_text + 3 < in_text_end ? in_text[3] : 0; + + // Assume a four-byte character and load four bytes. Unused bits are shifted out. + *out_char = (uint32_t)(s[0] & masks[len]) << 18; + *out_char |= (uint32_t)(s[1] & 0x3f) << 12; + *out_char |= (uint32_t)(s[2] & 0x3f) << 6; + *out_char |= (uint32_t)(s[3] & 0x3f) << 0; + *out_char >>= shiftc[len]; + + // Accumulate the various error conditions. + int e = 0; + e = (*out_char < mins[len]) << 6; // non-canonical encoding + e |= ((*out_char >> 11) == 0x1b) << 7; // surrogate half? + e |= (*out_char > IM_UNICODE_CODEPOINT_MAX) << 8; // out of range we can store in ImWchar (FIXME: May evolve) + e |= (s[1] & 0xc0) >> 2; + e |= (s[2] & 0xc0) >> 4; + e |= (s[3] ) >> 6; + e ^= 0x2a; // top two bits of each tail byte correct? + e >>= shifte[len]; + + if (e) + { + // No bytes are consumed when *in_text == 0 || in_text == in_text_end. + // One byte is consumed in case of invalid first byte of in_text. + // All available bytes (at most `len` bytes) are consumed on incomplete/invalid second to last bytes. + // Invalid or incomplete input may consume less bytes than wanted, therefore every byte has to be inspected in s. + wanted = ImMin(wanted, !!s[0] + !!s[1] + !!s[2] + !!s[3]); + *out_char = IM_UNICODE_CODEPOINT_INVALID; + } + + return wanted; +} + +int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_text_remaining) +{ + ImWchar* buf_out = buf; + ImWchar* buf_end = buf + buf_size; + while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + *buf_out++ = (ImWchar)c; + } + *buf_out = 0; + if (in_text_remaining) + *in_text_remaining = in_text; + return (int)(buf_out - buf); +} + +int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end) +{ + int char_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c; + in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); + char_count++; + } + return char_count; +} + +// Based on stb_to_utf8() from github.com/nothings/stb/ +static inline int ImTextCharToUtf8_inline(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c ) & 0x3f)); + return 3; + } + if (c <= 0x10FFFF) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c ) & 0x3f)); + return 4; + } + // Invalid code point, the max unicode is 0x10FFFF + return 0; +} + +const char* ImTextCharToUtf8(char out_buf[5], unsigned int c) +{ + int count = ImTextCharToUtf8_inline(out_buf, 5, c); + out_buf[count] = 0; + return out_buf; +} + +// Not optimal but we very rarely use this function. +int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end) +{ + unsigned int unused = 0; + return ImTextCharFromUtf8(&unused, in_text, in_text_end); +} + +static inline int ImTextCountUtf8BytesFromChar(unsigned int c) +{ + if (c < 0x80) return 1; + if (c < 0x800) return 2; + if (c < 0x10000) return 3; + if (c <= 0x10FFFF) return 4; + return 3; +} + +int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end) +{ + char* buf_p = out_buf; + const char* buf_end = out_buf + out_buf_size; + while (buf_p < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + *buf_p++ = (char)c; + else + buf_p += ImTextCharToUtf8_inline(buf_p, (int)(buf_end - buf_p - 1), c); + } + *buf_p = 0; + return (int)(buf_p - out_buf); +} + +int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) +{ + int bytes_count = 0; + while ((!in_text_end || in_text < in_text_end) && *in_text) + { + unsigned int c = (unsigned int)(*in_text++); + if (c < 0x80) + bytes_count++; + else + bytes_count += ImTextCountUtf8BytesFromChar(c); + } + return bytes_count; +} + +const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr) +{ + while (in_text_curr > in_text_start) + { + in_text_curr--; + if ((*in_text_curr & 0xC0) != 0x80) + return in_text_curr; + } + return in_text_start; +} + +int ImTextCountLines(const char* in_text, const char* in_text_end) +{ + if (in_text_end == NULL) + in_text_end = in_text + ImStrlen(in_text); // FIXME-OPT: Not optimal approach, discourage use for now. + int count = 0; + while (in_text < in_text_end) + { + const char* line_end = (const char*)ImMemchr(in_text, '\n', in_text_end - in_text); + in_text = line_end ? line_end + 1 : in_text_end; + count++; + } + return count; +} + +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (Color functions) +// Note: The Convert functions are early design which are not consistent with other API. +//----------------------------------------------------------------------------- + +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b) +{ + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); +} + +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +{ + float s = 1.0f / 255.0f; + return ImVec4( + ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_B_SHIFT) & 0xFF) * s, + ((in >> IM_COL32_A_SHIFT) & 0xFF) * s); +} + +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) +{ + ImU32 out; + out = ((ImU32)IM_F32_TO_INT8_SAT(in.x)) << IM_COL32_R_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.y)) << IM_COL32_G_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.z)) << IM_COL32_B_SHIFT; + out |= ((ImU32)IM_F32_TO_INT8_SAT(in.w)) << IM_COL32_A_SHIFT; + return out; +} + +// Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592 +// Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv +void ImGui::ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v) +{ + float K = 0.f; + if (g < b) + { + ImSwap(g, b); + K = -1.f; + } + if (r < g) + { + ImSwap(r, g); + K = -2.f / 6.f - K; + } + + const float chroma = r - (g < b ? g : b); + out_h = ImFabs(K + (g - b) / (6.f * chroma + 1e-20f)); + out_s = chroma / (r + 1e-20f); + out_v = r; +} + +// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 +// also http://en.wikipedia.org/wiki/HSL_and_HSV +void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) +{ + if (s == 0.0f) + { + // gray + out_r = out_g = out_b = v; + return; + } + + h = ImFmod(h, 1.0f) / (60.0f / 360.0f); + int i = (int)h; + float f = h - (float)i; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + + switch (i) + { + case 0: out_r = v; out_g = t; out_b = p; break; + case 1: out_r = q; out_g = v; out_b = p; break; + case 2: out_r = p; out_g = v; out_b = t; break; + case 3: out_r = p; out_g = q; out_b = v; break; + case 4: out_r = t; out_g = p; out_b = v; break; + case 5: default: out_r = v; out_g = p; out_b = q; break; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStorage +// Helper: Key->value storage +//----------------------------------------------------------------------------- + +// std::lower_bound but without the bullshit +ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key) +{ + ImGuiStoragePair* in_p = in_begin; + for (size_t count = (size_t)(in_end - in_p); count > 0; ) + { + size_t count2 = count >> 1; + ImGuiStoragePair* mid = in_p + count2; + if (mid->key < key) + { + in_p = ++mid; + count -= count2 + 1; + } + else + { + count = count2; + } + } + return in_p; +} + +IM_MSVC_RUNTIME_CHECKS_OFF +static int IMGUI_CDECL PairComparerByID(const void* lhs, const void* rhs) +{ + // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. + ImGuiID lhs_v = ((const ImGuiStoragePair*)lhs)->key; + ImGuiID rhs_v = ((const ImGuiStoragePair*)rhs)->key; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); +} + +// For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. +void ImGuiStorage::BuildSortByKey() +{ + ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), PairComparerByID); +} + +int ImGuiStorage::GetInt(ImGuiID key, int default_val) const +{ + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return default_val; + return it->val_i; +} + +bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const +{ + return GetInt(key, default_val ? 1 : 0) != 0; +} + +float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const +{ + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return default_val; + return it->val_f; +} + +void* ImGuiStorage::GetVoidPtr(ImGuiID key) const +{ + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) + return NULL; + return it->val_p; +} + +// References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. +int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_i; +} + +bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) +{ + return (bool*)GetIntRef(key, default_val ? 1 : 0); +} + +float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_f; +} + +void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + it = Data.insert(it, ImGuiStoragePair(key, default_val)); + return &it->val_p; +} + +// FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) +void ImGuiStorage::SetInt(ImGuiID key, int val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_i = val; +} + +void ImGuiStorage::SetBool(ImGuiID key, bool val) +{ + SetInt(key, val ? 1 : 0); +} + +void ImGuiStorage::SetFloat(ImGuiID key, float val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_f = val; +} + +void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) +{ + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) + Data.insert(it, ImGuiStoragePair(key, val)); + else + it->val_p = val; +} + +void ImGuiStorage::SetAllInt(int v) +{ + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextFilter +//----------------------------------------------------------------------------- + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) //-V1077 +{ + InputBuf[0] = 0; + CountGrep = 0; + if (default_filter) + { + ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); + Build(); + } +} + +bool ImGuiTextFilter::Draw(const char* label, float width) +{ + if (width != 0.0f) + ImGui::SetNextItemWidth(width); + bool value_changed = ImGui::InputText(label, InputBuf, IM_ARRAYSIZE(InputBuf)); + if (value_changed) + Build(); + return value_changed; +} + +void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector* out) const +{ + out->resize(0); + const char* wb = b; + const char* we = wb; + while (we < e) + { + if (*we == separator) + { + out->push_back(ImGuiTextRange(wb, we)); + wb = we + 1; + } + we++; + } + if (wb != we) + out->push_back(ImGuiTextRange(wb, we)); +} + +void ImGuiTextFilter::Build() +{ + Filters.resize(0); + ImGuiTextRange input_range(InputBuf, InputBuf + ImStrlen(InputBuf)); + input_range.split(',', &Filters); + + CountGrep = 0; + for (ImGuiTextRange& f : Filters) + { + while (f.b < f.e && ImCharIsBlankA(f.b[0])) + f.b++; + while (f.e > f.b && ImCharIsBlankA(f.e[-1])) + f.e--; + if (f.empty()) + continue; + if (f.b[0] != '-') + CountGrep += 1; + } +} + +bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const +{ + if (Filters.Size == 0) + return true; + + if (text == NULL) + text = text_end = ""; + + for (const ImGuiTextRange& f : Filters) + { + if (f.b == f.e) + continue; + if (f.b[0] == '-') + { + // Subtract + if (ImStristr(text, text_end, f.b + 1, f.e) != NULL) + return false; + } + else + { + // Grep + if (ImStristr(text, text_end, f.b, f.e) != NULL) + return true; + } + } + + // Implicit * grep + if (CountGrep == 0) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex +//----------------------------------------------------------------------------- + +// On some platform vsnprintf() takes va_list by reference and modifies it. +// va_copy is the 'correct' way to copy a va_list but Visual Studio prior to 2013 doesn't have it. +#ifndef va_copy +#if defined(__GNUC__) || defined(__clang__) +#define va_copy(dest, src) __builtin_va_copy(dest, src) +#else +#define va_copy(dest, src) (dest = src) +#endif +#endif + +char ImGuiTextBuffer::EmptyString[1] = { 0 }; + +void ImGuiTextBuffer::append(const char* str, const char* str_end) +{ + int len = str_end ? (int)(str_end - str) : (int)ImStrlen(str); + + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } + + Buf.resize(needed_sz); + memcpy(&Buf[write_off - 1], str, (size_t)len); + Buf[write_off - 1 + len] = 0; +} + +void ImGuiTextBuffer::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); +} + +// Helper: Text buffer for logging/accumulating text +void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) +{ + va_list args_copy; + va_copy(args_copy, args); + + int len = ImFormatStringV(NULL, 0, fmt, args); // FIXME-OPT: could do a first pass write attempt, likely successful on first pass. + if (len <= 0) + { + va_end(args_copy); + return; + } + + // Add zero-terminator the first time + const int write_off = (Buf.Size != 0) ? Buf.Size : 1; + const int needed_sz = write_off + len; + if (write_off + len >= Buf.Capacity) + { + int new_capacity = Buf.Capacity * 2; + Buf.reserve(needed_sz > new_capacity ? needed_sz : new_capacity); + } + + Buf.resize(needed_sz); + ImFormatStringV(&Buf[write_off - 1], (size_t)len + 1, fmt, args_copy); + va_end(args_copy); +} + +void ImGuiTextIndex::append(const char* base, int old_size, int new_size) +{ + IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset); + if (old_size == new_size) + return; + if (EndOffset == 0 || base[EndOffset - 1] == '\n') + LineOffsets.push_back(EndOffset); + const char* base_end = base + new_size; + for (const char* p = base + old_size; (p = (const char*)ImMemchr(p, '\n', base_end - p)) != 0; ) + if (++p < base_end) // Don't push a trailing offset on last \n + LineOffsets.push_back((int)(intptr_t)(p - base)); + EndOffset = ImMax(EndOffset, new_size); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiListClipper +//----------------------------------------------------------------------------- + +// FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. +// The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous. +static bool GetSkipItemForListClipping() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentTable ? g.CurrentTable->HostSkipItems : g.CurrentWindow->SkipItems); +} + +static void ImGuiListClipper_SortAndFuseRanges(ImVector& ranges, int offset = 0) +{ + if (ranges.Size - offset <= 1) + return; + + // Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries) + for (int sort_end = ranges.Size - offset - 1; sort_end > 0; --sort_end) + for (int i = offset; i < sort_end + offset; ++i) + if (ranges[i].Min > ranges[i + 1].Min) + ImSwap(ranges[i], ranges[i + 1]); + + // Now fuse ranges together as much as possible. + for (int i = 1 + offset; i < ranges.Size; i++) + { + IM_ASSERT(!ranges[i].PosToIndexConvert && !ranges[i - 1].PosToIndexConvert); + if (ranges[i - 1].Max < ranges[i].Min) + continue; + ranges[i - 1].Min = ImMin(ranges[i - 1].Min, ranges[i].Min); + ranges[i - 1].Max = ImMax(ranges[i - 1].Max, ranges[i].Max); + ranges.erase(ranges.Data + i); + i--; + } +} + +static void ImGuiListClipper_SeekCursorAndSetupPrevLine(float pos_y, float line_height) +{ + // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor. + // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. + // The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek? + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float off_y = pos_y - window->DC.CursorPos.y; + window->DC.CursorPos.y = pos_y; + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y - g.Style.ItemSpacing.y); + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage. + window->DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. + if (ImGuiOldColumns* columns = window->DC.CurrentColumns) + columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly + if (ImGuiTable* table = g.CurrentTable) + { + if (table->IsInsideRow) + ImGui::TableEndRow(table); + table->RowPosY2 = window->DC.CursorPos.y; + const int row_increase = (int)((off_y / line_height) + 0.5f); + //table->CurrentRow += row_increase; // Can't do without fixing TableEndRow() + table->RowBgColorCounter += row_increase; + } +} + +ImGuiListClipper::ImGuiListClipper() +{ + memset(this, 0, sizeof(*this)); +} + +ImGuiListClipper::~ImGuiListClipper() +{ + End(); +} + +void ImGuiListClipper::Begin(int items_count, float items_height) +{ + if (Ctx == NULL) + Ctx = ImGui::GetCurrentContext(); + + ImGuiContext& g = *Ctx; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Begin(%d,%.2f) in '%s'\n", items_count, items_height, window->Name); + + if (ImGuiTable* table = g.CurrentTable) + if (table->IsInsideRow) + ImGui::TableEndRow(table); + + StartPosY = window->DC.CursorPos.y; + ItemsHeight = items_height; + ItemsCount = items_count; + DisplayStart = -1; + DisplayEnd = 0; + + // Acquire temporary buffer + if (++g.ClipperTempDataStacked > g.ClipperTempData.Size) + g.ClipperTempData.resize(g.ClipperTempDataStacked, ImGuiListClipperData()); + ImGuiListClipperData* data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->Reset(this); + data->LossynessOffset = window->DC.CursorStartPosLossyness.y; + TempData = data; + StartSeekOffsetY = data->LossynessOffset; +} + +void ImGuiListClipper::End() +{ + if (ImGuiListClipperData* data = (ImGuiListClipperData*)TempData) + { + // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user. + ImGuiContext& g = *Ctx; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: End() in '%s'\n", g.CurrentWindow->Name); + if (ItemsCount >= 0 && ItemsCount < INT_MAX && DisplayStart >= 0) + SeekCursorForItem(ItemsCount); + + // Restore temporary buffer and fix back pointers which may be invalidated when nesting + IM_ASSERT(data->ListClipper == this); + data->StepNo = data->Ranges.Size; + if (--g.ClipperTempDataStacked > 0) + { + data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->ListClipper->TempData = data; + } + TempData = NULL; + } + ItemsCount = -1; +} + +void ImGuiListClipper::IncludeItemsByIndex(int item_begin, int item_end) +{ + ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; + IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet. + IM_ASSERT(item_begin <= item_end); + if (item_begin < item_end) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(item_begin, item_end)); +} + +// This is already called while stepping. +// The ONLY reason you may want to call this is if you passed INT_MAX to ImGuiListClipper::Begin() because you couldn't step item count beforehand. +void ImGuiListClipper::SeekCursorForItem(int item_n) +{ + // - Perform the add and multiply with double to allow seeking through larger ranges. + // - StartPosY starts from ItemsFrozen, by adding SeekOffsetY we generally cancel that out (SeekOffsetY == LossynessOffset - ItemsFrozen * ItemsHeight). + // - The reason we store SeekOffsetY instead of inferring it, is because we want to allow user to perform Seek after the last step, where ImGuiListClipperData is already done. + float pos_y = (float)((double)StartPosY + StartSeekOffsetY + (double)item_n * ItemsHeight); + ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, ItemsHeight); +} + +static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) +{ + ImGuiContext& g = *clipper->Ctx; + ImGuiWindow* window = g.CurrentWindow; + ImGuiListClipperData* data = (ImGuiListClipperData*)clipper->TempData; + IM_ASSERT(data != NULL && "Called ImGuiListClipper::Step() too many times, or before ImGuiListClipper::Begin() ?"); + + ImGuiTable* table = g.CurrentTable; + if (table && table->IsInsideRow) + ImGui::TableEndRow(table); + + // No items + if (clipper->ItemsCount == 0 || GetSkipItemForListClipping()) + return false; + + // While we are in frozen row state, keep displaying items one by one, unclipped + // FIXME: Could be stored as a table-agnostic state. + if (data->StepNo == 0 && table != NULL && !table->IsUnfrozenRows) + { + clipper->DisplayStart = data->ItemsFrozen; + clipper->DisplayEnd = ImMin(data->ItemsFrozen + 1, clipper->ItemsCount); + if (clipper->DisplayStart < clipper->DisplayEnd) + data->ItemsFrozen++; + return true; + } + + // Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height) + bool calc_clipping = false; + if (data->StepNo == 0) + { + clipper->StartPosY = window->DC.CursorPos.y; + if (clipper->ItemsHeight <= 0.0f) + { + // Submit the first item (or range) so we can measure its height (generally the first range is 0..1) + data->Ranges.push_front(ImGuiListClipperRange::FromIndices(data->ItemsFrozen, data->ItemsFrozen + 1)); + clipper->DisplayStart = ImMax(data->Ranges[0].Min, data->ItemsFrozen); + clipper->DisplayEnd = ImMin(data->Ranges[0].Max, clipper->ItemsCount); + data->StepNo = 1; + return true; + } + calc_clipping = true; // If on the first step with known item height, calculate clipping. + } + + // Step 1: Let the clipper infer height from first range + if (clipper->ItemsHeight <= 0.0f) + { + IM_ASSERT(data->StepNo == 1); + if (table) + IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y); + + clipper->ItemsHeight = (window->DC.CursorPos.y - clipper->StartPosY) / (float)(clipper->DisplayEnd - clipper->DisplayStart); + bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision(clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y); + if (affected_by_floating_point_precision) + clipper->ItemsHeight = window->DC.PrevLineSize.y + g.Style.ItemSpacing.y; // FIXME: Technically wouldn't allow multi-line entries. + if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode. + return false; + IM_ASSERT(clipper->ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); + calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. + } + + // Step 0 or 1: Calculate the actual ranges of visible elements. + const int already_submitted = clipper->DisplayEnd; + if (calc_clipping) + { + // Record seek offset, this is so ImGuiListClipper::Seek() can be called after ImGuiListClipperData is done + clipper->StartSeekOffsetY = (double)data->LossynessOffset - data->ItemsFrozen * (double)clipper->ItemsHeight; + + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(0, clipper->ItemsCount)); + } + else + { + // Add range selected to be included for navigation + const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (is_nav_request) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0)); + if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && g.NavTabbingDir == -1) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(clipper->ItemsCount - 1, clipper->ItemsCount)); + + // Add focused/active item + ImRect nav_rect_abs = ImGui::WindowRectRelToAbs(window, window->NavRectRel[0]); + if (g.NavId != 0 && window->NavLastIds[0] == g.NavId) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); + + // Add visible range + float min_y = window->ClipRect.Min.y; + float max_y = window->ClipRect.Max.y; + + // Add box selection range + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (bs->IsActive && bs->Window == window) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (bs->UnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); + } + + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; + const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max)); + } + + // Convert position ranges to item index ranges + // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping. + // - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list, + // which with the flooring/ceiling tend to lead to 2 items instead of one being submitted. + for (ImGuiListClipperRange& range : data->Ranges) + if (range.PosToIndexConvert) + { + int m1 = (int)(((double)range.Min - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight); + int m2 = (int)((((double)range.Max - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight) + 0.999999f); + range.Min = ImClamp(already_submitted + m1 + range.PosToIndexOffsetMin, already_submitted, clipper->ItemsCount - 1); + range.Max = ImClamp(already_submitted + m2 + range.PosToIndexOffsetMax, range.Min + 1, clipper->ItemsCount); + range.PosToIndexConvert = false; + } + ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo); + } + + // Step 0+ (if item height is given in advance) or 1+: Display the next range in line. + while (data->StepNo < data->Ranges.Size) + { + clipper->DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted); + clipper->DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, clipper->ItemsCount); + data->StepNo++; + if (clipper->DisplayStart >= clipper->DisplayEnd) + continue; + if (clipper->DisplayStart > already_submitted) + clipper->SeekCursorForItem(clipper->DisplayStart); + return true; + } + + // After the last step: Let the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), + // Advance the cursor to the end of the list and then returns 'false' to end the loop. + if (clipper->ItemsCount < INT_MAX) + clipper->SeekCursorForItem(clipper->ItemsCount); + + return false; +} + +bool ImGuiListClipper::Step() +{ + ImGuiContext& g = *Ctx; + bool need_items_height = (ItemsHeight <= 0.0f); + bool ret = ImGuiListClipper_StepInternal(this); + if (ret && (DisplayStart >= DisplayEnd)) + ret = false; + if (g.CurrentTable && g.CurrentTable->IsUnfrozenRows == false) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): inside frozen table row.\n"); + if (need_items_height && ItemsHeight > 0.0f) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): computed ItemsHeight: %.2f.\n", ItemsHeight); + if (ret) + { + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): display %d to %d.\n", DisplayStart, DisplayEnd); + } + else + { + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): End.\n"); + End(); + } + return ret; +} + +//----------------------------------------------------------------------------- +// [SECTION] STYLING +//----------------------------------------------------------------------------- + +ImGuiStyle& ImGui::GetStyle() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->Style; +} + +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); +} + +ImU32 ImGui::GetColorU32(const ImVec4& col) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); +} + +const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) +{ + ImGuiStyle& style = GImGui->Style; + return style.Colors[idx]; +} + +ImU32 ImGui::GetColorU32(ImU32 col, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + alpha_mul *= style.Alpha; + if (alpha_mul >= 1.0f) + return col; + ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; + a = (ImU32)(a * alpha_mul); // We don't need to clamp 0..255 because alpha is in 0..1 range. + return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); +} + +// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 +void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); +} + +void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = col; +} + +void ImGui::PopStyleColor(int count) +{ + ImGuiContext& g = *GImGui; + if (g.ColorStack.Size < count) + { + IM_ASSERT_USER_ERROR(0, "Calling PopStyleColor() too many times!"); + count = g.ColorStack.Size; + } + while (count > 0) + { + ImGuiColorMod& backup = g.ColorStack.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorStack.pop_back(); + count--; + } +} + +static const ImGuiStyleVarInfo GStyleVarsInfo[] = +{ + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ImageBorderSize) }, // ImGuiStyleVar_ImageBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBorderSize) }, // ImGuiStyleVar_TabBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarOverlineSize) }, // ImGuiStyleVar_TabBarOverlineSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding +}; + +const ImGuiStyleVarInfo* ImGui::GetStyleVarInfo(ImGuiStyleVar idx) +{ + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_COUNT); + IM_STATIC_ASSERT(IM_ARRAYSIZE(GStyleVarsInfo) == ImGuiStyleVar_COUNT); + return &GStyleVarsInfo[idx]; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + float* pvar = (float*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; +} + +void ImGui::PushStyleVarX(ImGuiStyleVar idx, float val_x) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->x = val_x; +} + +void ImGui::PushStyleVarY(ImGuiStyleVar idx, float val_y) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->y = val_y; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; +} + +void ImGui::PopStyleVar(int count) +{ + ImGuiContext& g = *GImGui; + if (g.StyleVarStack.Size < count) + { + IM_ASSERT_USER_ERROR(0, "Calling PopStyleVar() too many times!"); + count = g.StyleVarStack.Size; + } + while (count > 0) + { + // We avoid a generic memcpy(data, &backup.Backup.., GDataTypeSize[info->Type] * info->Count), the overhead in Debug is not worth it. + ImGuiStyleMod& backup = g.StyleVarStack.back(); + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(backup.VarIdx); + void* data = var_info->GetVarPtr(&g.Style); + if (var_info->DataType == ImGuiDataType_Float && var_info->Count == 1) { ((float*)data)[0] = backup.BackupFloat[0]; } + else if (var_info->DataType == ImGuiDataType_Float && var_info->Count == 2) { ((float*)data)[0] = backup.BackupFloat[0]; ((float*)data)[1] = backup.BackupFloat[1]; } + g.StyleVarStack.pop_back(); + count--; + } +} + +const char* ImGui::GetStyleColorName(ImGuiCol idx) +{ + // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; + switch (idx) + { + case ImGuiCol_Text: return "Text"; + case ImGuiCol_TextDisabled: return "TextDisabled"; + case ImGuiCol_WindowBg: return "WindowBg"; + case ImGuiCol_ChildBg: return "ChildBg"; + case ImGuiCol_PopupBg: return "PopupBg"; + case ImGuiCol_Border: return "Border"; + case ImGuiCol_BorderShadow: return "BorderShadow"; + case ImGuiCol_FrameBg: return "FrameBg"; + case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; + case ImGuiCol_FrameBgActive: return "FrameBgActive"; + case ImGuiCol_TitleBg: return "TitleBg"; + case ImGuiCol_TitleBgActive: return "TitleBgActive"; + case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; + case ImGuiCol_MenuBarBg: return "MenuBarBg"; + case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; + case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; + case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; + case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; + case ImGuiCol_CheckMark: return "CheckMark"; + case ImGuiCol_SliderGrab: return "SliderGrab"; + case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; + case ImGuiCol_Button: return "Button"; + case ImGuiCol_ButtonHovered: return "ButtonHovered"; + case ImGuiCol_ButtonActive: return "ButtonActive"; + case ImGuiCol_Header: return "Header"; + case ImGuiCol_HeaderHovered: return "HeaderHovered"; + case ImGuiCol_HeaderActive: return "HeaderActive"; + case ImGuiCol_Separator: return "Separator"; + case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; + case ImGuiCol_SeparatorActive: return "SeparatorActive"; + case ImGuiCol_ResizeGrip: return "ResizeGrip"; + case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; + case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; + case ImGuiCol_TabHovered: return "TabHovered"; + case ImGuiCol_Tab: return "Tab"; + case ImGuiCol_TabSelected: return "TabSelected"; + case ImGuiCol_TabSelectedOverline: return "TabSelectedOverline"; + case ImGuiCol_TabDimmed: return "TabDimmed"; + case ImGuiCol_TabDimmedSelected: return "TabDimmedSelected"; + case ImGuiCol_TabDimmedSelectedOverline: return "TabDimmedSelectedOverline"; + case ImGuiCol_PlotLines: return "PlotLines"; + case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; + case ImGuiCol_PlotHistogram: return "PlotHistogram"; + case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TableHeaderBg: return "TableHeaderBg"; + case ImGuiCol_TableBorderStrong: return "TableBorderStrong"; + case ImGuiCol_TableBorderLight: return "TableBorderLight"; + case ImGuiCol_TableRowBg: return "TableRowBg"; + case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt"; + case ImGuiCol_TextLink: return "TextLink"; + case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; + case ImGuiCol_DragDropTarget: return "DragDropTarget"; + case ImGuiCol_NavCursor: return "NavCursor"; + case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; + case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; + case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; + } + IM_ASSERT(0); + return "Unknown"; +} + +//----------------------------------------------------------------------------- +// [SECTION] RENDER HELPERS +// Some of those (internal) functions are currently quite a legacy mess - their signature and behavior will change, +// we need a nicer separation between low-level functions and high-level functions relying on the ImGui context. +// Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: context. +//----------------------------------------------------------------------------- + +const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) +{ + const char* text_display_end = text; + if (!text_end) + text_end = (const char*)-1; + + while (text_display_end < text_end && *text_display_end != '\0' && (text_display_end[0] != '#' || text_display_end[1] != '#')) + text_display_end++; + return text_display_end; +} + +// Internal ImGui functions to render text +// RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText() +void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool hide_text_after_hash) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Hide anything after a '##' string + const char* text_display_end; + if (hide_text_after_hash) + { + text_display_end = FindRenderedTextEnd(text, text_end); + } + else + { + if (!text_end) + text_end = text + ImStrlen(text); // FIXME-OPT + text_display_end = text_end; + } + + if (text != text_display_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_display_end); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_display_end); + } +} + +void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (!text_end) + text_end = text + ImStrlen(text); // FIXME-OPT + + if (text != text_end) + { + window->DrawList->AddText(g.Font, g.FontSize, pos, GetColorU32(ImGuiCol_Text), text, text_end, wrap_width); + if (g.LogEnabled) + LogRenderedText(&pos, text, text_end); + } +} + +// Default clip_rect uses (pos_min,pos_max) +// Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) +// FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especally for text above draw_list->DrawList. +// Effectively as this is called from widget doing their own coarse clipping it's not very valuable presently. Next time function will take +// better advantage of the render function taking size into account for coarse clipping. +void ImGui::RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Perform CPU side clipping for single clipped element to avoid using scissor state + ImVec2 pos = pos_min; + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f); + + const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min; + const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max; + bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); + if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min + need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); + + // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. + if (align.x > 0.0f) pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x); + if (align.y > 0.0f) pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y); + + // Render + if (need_clipping) + { + ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); + } + else + { + draw_list->AddText(NULL, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); + } +} + +void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Hide anything after a '##' string + const char* text_display_end = FindRenderedTextEnd(text, text_end); + const int text_len = (int)(text_display_end - text); + if (text_len == 0) + return; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + RenderTextClippedEx(window->DrawList, pos_min, pos_max, text, text_display_end, text_size_if_known, align, clip_rect); + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_display_end); +} + +// Another overly complex function until we reorganize everything into a nice all-in-one helper. +// This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display. +// This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move. +void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end_full, const ImVec2* text_size_if_known) +{ + ImGuiContext& g = *GImGui; + if (text_end_full == NULL) + text_end_full = FindRenderedTextEnd(text); + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_end_full, false, 0.0f); + + //draw_list->AddLine(ImVec2(pos_max.x, pos_min.y - 4), ImVec2(pos_max.x, pos_max.y + 4), IM_COL32(0, 0, 255, 255)); + //draw_list->AddLine(ImVec2(ellipsis_max_x, pos_min.y-2), ImVec2(ellipsis_max_x, pos_max.y+2), IM_COL32(0, 255, 0, 255)); + //draw_list->AddLine(ImVec2(clip_max_x, pos_min.y), ImVec2(clip_max_x, pos_max.y), IM_COL32(255, 0, 0, 255)); + // FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels. + if (text_size.x > pos_max.x - pos_min.x) + { + // Hello wo... + // | | | + // min max ellipsis_max + // <-> this is generally some padding value + + ImFont* font = draw_list->_Data->Font; + const float font_size = draw_list->_Data->FontSize; + const float font_scale = draw_list->_Data->FontScale; + const char* text_end_ellipsis = NULL; + const float ellipsis_width = font->EllipsisWidth * font_scale; + + // We can now claim the space between pos_max.x and ellipsis_max.x + const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_width) - pos_min.x, 1.0f); + float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x; + if (text == text_end_ellipsis && text_end_ellipsis < text_end_full) + { + // Always display at least 1 character if there's no room for character + ellipsis + text_end_ellipsis = text + ImTextCountUtf8BytesFromChar(text, text_end_full); + text_size_clipped_x = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text, text_end_ellipsis).x; + } + while (text_end_ellipsis > text && ImCharIsBlankA(text_end_ellipsis[-1])) + { + // Trim trailing space before ellipsis (FIXME: Supporting non-ascii blanks would be nice, for this we need a function to backtrack in UTF-8 text) + text_end_ellipsis--; + text_size_clipped_x -= font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, text_end_ellipsis, text_end_ellipsis + 1).x; // Ascii blanks are always 1 byte + } + + // Render text, render ellipsis + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); + ImVec2 ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y)); + if (ellipsis_pos.x + ellipsis_width <= ellipsis_max_x) + for (int i = 0; i < font->EllipsisCharCount; i++, ellipsis_pos.x += font->EllipsisCharStep * font_scale) + font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar); + } + else + { + RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_full, &text_size, ImVec2(0.0f, 0.0f)); + } + + if (g.LogEnabled) + LogRenderedText(&pos_min, text, text_end_full); +} + +// Render a rectangle shaped with optional rounding and borders +void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); + const float border_size = g.Style.FrameBorderSize; + if (borders && border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } +} + +void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const float border_size = g.Style.FrameBorderSize; + if (border_size > 0.0f) + { + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); + } +} + +void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags) +{ + ImGuiContext& g = *GImGui; + if (id != g.NavId) + return; + if (!g.NavCursorVisible && !(flags & ImGuiNavRenderCursorFlags_AlwaysDraw)) + return; + if (id == g.LastItemData.ID && (g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav)) + return; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.NavHideHighlightOneFrame) + return; + + float rounding = (flags & ImGuiNavRenderCursorFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + ImRect display_rect = bb; + display_rect.ClipWith(window->ClipRect); + const float thickness = 2.0f; + if (flags & ImGuiNavRenderCursorFlags_Compact) + { + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); + } + else + { + const float distance = 3.0f + thickness * 0.5f; + display_rect.Expand(ImVec2(distance, distance)); + bool fully_visible = window->ClipRect.Contains(display_rect); + if (!fully_visible) + window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); + if (!fully_visible) + window->DrawList->PopClipRect(); + } +} + +void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) +{ + ImGuiContext& g = *GImGui; + if (mouse_cursor <= ImGuiMouseCursor_None || mouse_cursor >= ImGuiMouseCursor_COUNT) // We intentionally accept out of bound values. + mouse_cursor = ImGuiMouseCursor_Arrow; + ImFontAtlas* font_atlas = g.DrawListSharedData.Font->ContainerAtlas; + for (ImGuiViewportP* viewport : g.Viewports) + { + // We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor. + ImVec2 offset, size, uv[4]; + if (!ImFontAtlasGetMouseCursorTexData(font_atlas, mouse_cursor, &offset, &size, &uv[0], &uv[2])) + continue; + const ImVec2 pos = base_pos - offset; + const float scale = base_scale; + if (!viewport->GetMainRect().Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale))) + continue; + ImDrawList* draw_list = GetForegroundDrawList(viewport); + ImTextureID tex_id = font_atlas->TexID; + draw_list->PushTextureID(tex_id); + draw_list->AddImage(tex_id, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[2], uv[3], col_border); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[0], uv[1], col_fill); + if (mouse_cursor == ImGuiMouseCursor_Wait || mouse_cursor == ImGuiMouseCursor_Progress) + { + float a_min = ImFmod((float)g.Time * 5.0f, 2.0f * IM_PI); + float a_max = a_min + IM_PI * 1.65f; + draw_list->PathArcTo(pos + ImVec2(14, -1) * scale, 6.0f * scale, a_min, a_max); + draw_list->PathStroke(col_fill, ImDrawFlags_None, 3.0f * scale); + } + draw_list->PopTextureID(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] INITIALIZATION, SHUTDOWN +//----------------------------------------------------------------------------- + +// Internal state access - if you want to share Dear ImGui state between modules (e.g. DLL) or allocate it yourself +// Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module +ImGuiContext* ImGui::GetCurrentContext() +{ + return GImGui; +} + +void ImGui::SetCurrentContext(ImGuiContext* ctx) +{ +#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. +#else + GImGui = ctx; +#endif +} + +void ImGui::SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data) +{ + GImAllocatorAllocFunc = alloc_func; + GImAllocatorFreeFunc = free_func; + GImAllocatorUserData = user_data; +} + +// This is provided to facilitate copying allocators from one static/DLL boundary to another (e.g. retrieve default allocator of your executable address space) +void ImGui::GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data) +{ + *p_alloc_func = GImAllocatorAllocFunc; + *p_free_func = GImAllocatorFreeFunc; + *p_user_data = GImAllocatorUserData; +} + +ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +{ + ImGuiContext* prev_ctx = GetCurrentContext(); + ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + SetCurrentContext(ctx); + Initialize(); + if (prev_ctx != NULL) + SetCurrentContext(prev_ctx); // Restore previous context if any, else keep new one. + return ctx; +} + +void ImGui::DestroyContext(ImGuiContext* ctx) +{ + ImGuiContext* prev_ctx = GetCurrentContext(); + if (ctx == NULL) //-V1051 + ctx = prev_ctx; + SetCurrentContext(ctx); + Shutdown(); + SetCurrentContext((prev_ctx != ctx) ? prev_ctx : NULL); + IM_DELETE(ctx); +} + +// IMPORTANT: interactive elements requires a fixed ###xxx suffix, it must be same in ALL languages to allow for automation. +static const ImGuiLocEntry GLocalizationEntriesEnUS[] = +{ + { ImGuiLocKey_VersionStr, "Dear ImGui " IMGUI_VERSION " (" IM_STRINGIFY(IMGUI_VERSION_NUM) ")" }, + { ImGuiLocKey_TableSizeOne, "Size column to fit###SizeOne" }, + { ImGuiLocKey_TableSizeAllFit, "Size all columns to fit###SizeAll" }, + { ImGuiLocKey_TableSizeAllDefault, "Size all columns to default###SizeAll" }, + { ImGuiLocKey_TableResetOrder, "Reset order###ResetOrder" }, + { ImGuiLocKey_WindowingMainMenuBar, "(Main menu bar)" }, + { ImGuiLocKey_WindowingPopup, "(Popup)" }, + { ImGuiLocKey_WindowingUntitled, "(Untitled)" }, + { ImGuiLocKey_OpenLink_s, "Open '%s'" }, + { ImGuiLocKey_CopyLink, "Copy Link###CopyLink" }, +}; + +ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) +{ + IO.Ctx = this; + InputTextState.Ctx = this; + + Initialized = false; + FontAtlasOwnedByContext = shared_font_atlas ? false : true; + Font = NULL; + FontSize = FontBaseSize = FontScale = CurrentDpiScale = 0.0f; + IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); + Time = 0.0f; + FrameCount = 0; + FrameCountEnded = FrameCountRendered = -1; + WithinEndChildID = 0; + WithinFrameScope = WithinFrameScopeWithImplicitWindow = false; + GcCompactAll = false; + TestEngineHookItems = false; + TestEngine = NULL; + memset(ContextName, 0, sizeof(ContextName)); + + InputEventsNextMouseSource = ImGuiMouseSource_Mouse; + InputEventsNextEventId = 1; + + WindowsActiveCount = 0; + WindowsBorderHoverPadding = 0.0f; + CurrentWindow = NULL; + HoveredWindow = NULL; + HoveredWindowUnderMovingWindow = NULL; + HoveredWindowBeforeClear = NULL; + MovingWindow = NULL; + WheelingWindow = NULL; + WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; + WheelingWindowReleaseTimer = 0.0f; + + DebugDrawIdConflicts = 0; + DebugHookIdInfo = 0; + HoveredId = HoveredIdPreviousFrame = 0; + HoveredIdPreviousFrameItemCount = 0; + HoveredIdAllowOverlap = false; + HoveredIdIsDisabled = false; + HoveredIdTimer = HoveredIdNotActiveTimer = 0.0f; + ItemUnclipByLog = false; + ActiveId = 0; + ActiveIdIsAlive = 0; + ActiveIdTimer = 0.0f; + ActiveIdIsJustActivated = false; + ActiveIdAllowOverlap = false; + ActiveIdNoClearOnFocusLoss = false; + ActiveIdHasBeenPressedBefore = false; + ActiveIdHasBeenEditedBefore = false; + ActiveIdHasBeenEditedThisFrame = false; + ActiveIdFromShortcut = false; + ActiveIdClickOffset = ImVec2(-1, -1); + ActiveIdWindow = NULL; + ActiveIdSource = ImGuiInputSource_None; + ActiveIdMouseButton = -1; + ActiveIdPreviousFrame = 0; + memset(&DeactivatedItemData, 0, sizeof(DeactivatedItemData)); + memset(&ActiveIdValueOnActivation, 0, sizeof(ActiveIdValueOnActivation)); + LastActiveId = 0; + LastActiveIdTimer = 0.0f; + + LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; + + ActiveIdUsingNavDirMask = 0x00; + ActiveIdUsingAllKeyboardKeys = false; + + CurrentFocusScopeId = 0; + CurrentItemFlags = ImGuiItemFlags_None; + DebugShowGroupRects = false; + + NavCursorVisible = false; + NavHighlightItemUnderNav = false; + NavMousePosDirty = false; + NavIdIsAlive = false; + NavId = 0; + NavWindow = NULL; + NavFocusScopeId = NavActivateId = NavActivateDownId = NavActivatePressedId = 0; + NavLayer = ImGuiNavLayer_Main; + NavNextActivateId = 0; + NavActivateFlags = NavNextActivateFlags = ImGuiActivateFlags_None; + NavHighlightActivatedId = 0; + NavHighlightActivatedTimer = 0.0f; + NavInputSource = ImGuiInputSource_Keyboard; + NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + NavCursorHideFrames = 0; + + NavAnyRequest = false; + NavInitRequest = false; + NavInitRequestFromMove = false; + NavMoveSubmitted = false; + NavMoveScoringItems = false; + NavMoveForwardToNextFrame = false; + NavMoveFlags = ImGuiNavMoveFlags_None; + NavMoveScrollFlags = ImGuiScrollFlags_None; + NavMoveKeyMods = ImGuiMod_None; + NavMoveDir = NavMoveDirForDebug = NavMoveClipDir = ImGuiDir_None; + NavScoringDebugCount = 0; + NavTabbingDir = 0; + NavTabbingCounter = 0; + + NavJustMovedFromFocusScopeId = NavJustMovedToId = NavJustMovedToFocusScopeId = 0; + NavJustMovedToKeyMods = ImGuiMod_None; + NavJustMovedToIsTabbing = false; + NavJustMovedToHasSelectionData = false; + + // All platforms use Ctrl+Tab but Ctrl<>Super are swapped on Mac... + // FIXME: Because this value is stored, it annoyingly interfere with toggling io.ConfigMacOSXBehaviors updating this.. + ConfigNavWindowingKeyNext = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiKey_Tab); + ConfigNavWindowingKeyPrev = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab); + NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL; + NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; + NavWindowingToggleLayer = false; + NavWindowingToggleKey = ImGuiKey_None; + + DimBgRatio = 0.0f; + + DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; + DragDropSourceFlags = ImGuiDragDropFlags_None; + DragDropSourceFrameCount = -1; + DragDropMouseButton = -1; + DragDropTargetId = 0; + DragDropAcceptFlags = ImGuiDragDropFlags_None; + DragDropAcceptIdCurrRectSurface = 0.0f; + DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; + DragDropAcceptFrameCount = -1; + DragDropHoldJustPressedId = 0; + memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + + ClipperTempDataStacked = 0; + + CurrentTable = NULL; + TablesTempDataStacked = 0; + CurrentTabBar = NULL; + CurrentMultiSelect = NULL; + MultiSelectTempDataStacked = 0; + + HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; + HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; + + MouseCursor = ImGuiMouseCursor_Arrow; + MouseStationaryTimer = 0.0f; + + TempInputId = 0; + memset(&DataTypeZeroValue, 0, sizeof(DataTypeZeroValue)); + BeginMenuDepth = BeginComboDepth = 0; + ColorEditOptions = ImGuiColorEditFlags_DefaultOptions_; + ColorEditCurrentID = ColorEditSavedID = 0; + ColorEditSavedHue = ColorEditSavedSat = 0.0f; + ColorEditSavedColor = 0; + WindowResizeRelativeMode = false; + ScrollbarSeekMode = 0; + ScrollbarClickDeltaToGrabCenter = 0.0f; + SliderGrabClickOffset = 0.0f; + SliderCurrentAccum = 0.0f; + SliderCurrentAccumDirty = false; + DragCurrentAccumDirty = false; + DragCurrentAccum = 0.0f; + DragSpeedDefaultRatio = 1.0f / 100.0f; + DisabledAlphaBackup = 0.0f; + DisabledStackSize = 0; + TooltipOverrideCount = 0; + TooltipPreviousWindow = NULL; + + PlatformImeData.InputPos = ImVec2(0.0f, 0.0f); + PlatformImeDataPrev.InputPos = ImVec2(-1.0f, -1.0f); // Different to ensure initial submission + + SettingsLoaded = false; + SettingsDirtyTimer = 0.0f; + HookIdNext = 0; + + memset(LocalizationTable, 0, sizeof(LocalizationTable)); + + LogEnabled = false; + LogFlags = ImGuiLogFlags_None; + LogWindow = NULL; + LogNextPrefix = LogNextSuffix = NULL; + LogFile = NULL; + LogLinePosY = FLT_MAX; + LogLineFirstItem = false; + LogDepthRef = 0; + LogDepthToExpand = LogDepthToExpandDefault = 2; + + ErrorCallback = NULL; + ErrorCallbackUserData = NULL; + ErrorFirst = true; + ErrorCountCurrentFrame = 0; + StackSizesInBeginForCurrentWindow = NULL; + + DebugDrawIdConflictsCount = 0; + DebugLogFlags = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_OutputToTTY; + DebugLocateId = 0; + DebugLogSkippedErrors = 0; + DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + DebugLogAutoDisableFrames = 0; + DebugLocateFrames = 0; + DebugBeginReturnValueCullDepth = -1; + DebugItemPickerActive = false; + DebugItemPickerMouseButton = ImGuiMouseButton_Left; + DebugItemPickerBreakId = 0; + DebugFlashStyleColorTime = 0.0f; + DebugFlashStyleColorIdx = ImGuiCol_COUNT; + + // Same as DebugBreakClearData(). Those fields are scattered in their respective subsystem to stay in hot-data locations + DebugBreakInWindow = 0; + DebugBreakInTable = 0; + DebugBreakInLocateId = false; + DebugBreakKeyChord = ImGuiKey_Pause; + DebugBreakInShortcutRouting = ImGuiKey_None; + + memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); + FramerateSecPerFrameIdx = FramerateSecPerFrameCount = 0; + FramerateSecPerFrameAccum = 0.0f; + WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempKeychordName, 0, sizeof(TempKeychordName)); +} + +void ImGui::Initialize() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(!g.Initialized && !g.SettingsLoaded); + + // Add .ini handle for ImGuiWindow and ImGuiTable types + { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Window"; + ini_handler.TypeHash = ImHashStr("Window"); + ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = WindowSettingsHandler_WriteAll; + AddSettingsHandler(&ini_handler); + } + TableSettingsAddSettingsHandler(); + + // Setup default localization table + LocalizeRegisterEntries(GLocalizationEntriesEnUS, IM_ARRAYSIZE(GLocalizationEntriesEnUS)); + + // Setup default ImGuiPlatformIO clipboard/IME handlers. + g.PlatformIO.Platform_GetClipboardTextFn = Platform_GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + g.PlatformIO.Platform_SetClipboardTextFn = Platform_SetClipboardTextFn_DefaultImpl; + g.PlatformIO.Platform_OpenInShellFn = Platform_OpenInShellFn_DefaultImpl; + g.PlatformIO.Platform_SetImeDataFn = Platform_SetImeDataFn_DefaultImpl; + + // Create default viewport + ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = IMGUI_VIEWPORT_DEFAULT_ID; + g.Viewports.push_back(viewport); + g.TempBuffer.resize(1024 * 3 + 1, 0); + + // Build KeysMayBeCharInput[] lookup table (1 bool per named key) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + if ((key >= ImGuiKey_0 && key <= ImGuiKey_9) || (key >= ImGuiKey_A && key <= ImGuiKey_Z) || (key >= ImGuiKey_Keypad0 && key <= ImGuiKey_Keypad9) + || key == ImGuiKey_Tab || key == ImGuiKey_Space || key == ImGuiKey_Apostrophe || key == ImGuiKey_Comma || key == ImGuiKey_Minus || key == ImGuiKey_Period + || key == ImGuiKey_Slash || key == ImGuiKey_Semicolon || key == ImGuiKey_Equal || key == ImGuiKey_LeftBracket || key == ImGuiKey_RightBracket || key == ImGuiKey_GraveAccent + || key == ImGuiKey_KeypadDecimal || key == ImGuiKey_KeypadDivide || key == ImGuiKey_KeypadMultiply || key == ImGuiKey_KeypadSubtract || key == ImGuiKey_KeypadAdd || key == ImGuiKey_KeypadEqual) + g.KeysMayBeCharInput.SetBit(key); + +#ifdef IMGUI_HAS_DOCK +#endif + + g.Initialized = true; +} + +// This function is merely here to free heap allocations. +void ImGui::Shutdown() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR(g.IO.BackendPlatformUserData == NULL, "Forgot to shutdown Platform backend?"); + IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == NULL, "Forgot to shutdown Renderer backend?"); + + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) + if (g.IO.Fonts && g.FontAtlasOwnedByContext) + { + g.IO.Fonts->Locked = false; + IM_DELETE(g.IO.Fonts); + } + g.IO.Fonts = NULL; + g.DrawListSharedData.TempBuffer.clear(); + + // Cleanup of other data are conditional on actually having initialized Dear ImGui. + if (!g.Initialized) + return; + + // Save settings (unless we haven't attempted to load them: CreateContext/DestroyContext without a call to NewFrame shouldn't save an empty file) + if (g.SettingsLoaded && g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + + CallContextHooks(&g, ImGuiContextHookType_Shutdown); + + // Clear everything else + g.Windows.clear_delete(); + g.WindowsFocusOrder.clear(); + g.WindowsTempSortBuffer.clear(); + g.CurrentWindow = NULL; + g.CurrentWindowStack.clear(); + g.WindowsById.Clear(); + g.NavWindow = NULL; + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + g.ActiveIdWindow = NULL; + g.MovingWindow = NULL; + + g.KeysRoutingTable.Clear(); + + g.ColorStack.clear(); + g.StyleVarStack.clear(); + g.FontStack.clear(); + g.OpenPopupStack.clear(); + g.BeginPopupStack.clear(); + g.TreeNodeStack.clear(); + + g.Viewports.clear_delete(); + + g.TabBars.Clear(); + g.CurrentTabBarStack.clear(); + g.ShrinkWidthBuffer.clear(); + + g.ClipperTempData.clear_destruct(); + + g.Tables.Clear(); + g.TablesTempData.clear_destruct(); + g.DrawChannelsTempMergeBuffer.clear(); + + g.MultiSelectStorage.Clear(); + g.MultiSelectTempData.clear_destruct(); + + g.ClipboardHandlerData.clear(); + g.MenusIdSubmittedThisFrame.clear(); + g.InputTextState.ClearFreeMemory(); + g.InputTextDeactivatedState.ClearFreeMemory(); + + g.SettingsWindows.clear(); + g.SettingsHandlers.clear(); + + if (g.LogFile) + { +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + if (g.LogFile != stdout) +#endif + ImFileClose(g.LogFile); + g.LogFile = NULL; + } + g.LogBuffer.clear(); + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + + g.Initialized = false; +} + +// No specific ordering/dependency support, will see as needed +ImGuiID ImGui::AddContextHook(ImGuiContext* ctx, const ImGuiContextHook* hook) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook->Callback != NULL && hook->HookId == 0 && hook->Type != ImGuiContextHookType_PendingRemoval_); + g.Hooks.push_back(*hook); + g.Hooks.back().HookId = ++g.HookIdNext; + return g.HookIdNext; +} + +// Deferred removal, avoiding issue with changing vector while iterating it +void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook_id != 0); + for (ImGuiContextHook& hook : g.Hooks) + if (hook.HookId == hook_id) + hook.Type = ImGuiContextHookType_PendingRemoval_; +} + +// Call context hooks (used by e.g. test engine) +// We assume a small number of hooks so all stored in same array +void ImGui::CallContextHooks(ImGuiContext* ctx, ImGuiContextHookType hook_type) +{ + ImGuiContext& g = *ctx; + for (ImGuiContextHook& hook : g.Hooks) + if (hook.Type == hook_type) + hook.Callback(&g, &hook); +} + +//----------------------------------------------------------------------------- +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +//----------------------------------------------------------------------------- + +// ImGuiWindow is mostly a dumb struct. It merely has a constructor and a few helper methods +ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NULL) +{ + memset(this, 0, sizeof(*this)); + Ctx = ctx; + Name = ImStrdup(name); + NameBufLen = (int)ImStrlen(name) + 1; + ID = ImHashStr(name); + IDStack.push_back(ID); + MoveId = GetID("#MOVE"); + ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); + AutoFitFramesX = AutoFitFramesY = -1; + AutoPosLastDirection = ImGuiDir_None; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = 0; + SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); + LastFrameActive = -1; + LastTimeActive = -1.0f; + FontRefSize = 0.0f; + FontWindowScale = FontWindowScaleParents = 1.0f; + SettingsOffset = -1; + DrawList = &DrawListInst; + DrawList->_OwnerName = Name; + DrawList->_Data = &Ctx->DrawListSharedData; + NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX); +} + +ImGuiWindow::~ImGuiWindow() +{ + IM_ASSERT(DrawList == &DrawListInst); + IM_DELETE(Name); + ColumnsStorage.clear_destruct(); +} + +static void SetCurrentWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow = window; + g.StackSizesInBeginForCurrentWindow = g.CurrentWindow ? &g.CurrentWindowStack.back().StackSizesInBegin : NULL; + g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL; + g.CurrentDpiScale = 1.0f; // FIXME-DPI: WIP this is modified in docking + if (window) + { + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; + ImGui::NavUpdateCurrentWindowIsScrollPushableX(); + } +} + +void ImGui::GcCompactTransientMiscBuffers() +{ + ImGuiContext& g = *GImGui; + g.ItemFlagsStack.clear(); + g.GroupStack.clear(); + g.MultiSelectTempDataStacked = 0; + g.MultiSelectTempData.clear_destruct(); + TableGcCompactSettings(); +} + +// Free up/compact internal window buffers, we can use this when a window becomes unused. +// Not freed: +// - ImGuiWindow, ImGuiWindowSettings, Name, StateStorage, ColumnsStorage (may hold useful data) +// This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost. +void ImGui::GcCompactTransientWindowBuffers(ImGuiWindow* window) +{ + window->MemoryCompacted = true; + window->MemoryDrawListIdxCapacity = window->DrawList->IdxBuffer.Capacity; + window->MemoryDrawListVtxCapacity = window->DrawList->VtxBuffer.Capacity; + window->IDStack.clear(); + window->DrawList->_ClearFreeMemory(); + window->DC.ChildWindows.clear(); + window->DC.ItemWidthStack.clear(); + window->DC.TextWrapPosStack.clear(); +} + +void ImGui::GcAwakeTransientWindowBuffers(ImGuiWindow* window) +{ + // We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening. + // The other buffers tends to amortize much faster. + window->MemoryCompacted = false; + window->DrawList->IdxBuffer.reserve(window->MemoryDrawListIdxCapacity); + window->DrawList->VtxBuffer.reserve(window->MemoryDrawListVtxCapacity); + window->MemoryDrawListIdxCapacity = window->MemoryDrawListVtxCapacity = 0; +} + +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + // Clear previous active id + if (g.ActiveId != 0) + { + // While most behaved code would make an effort to not steal active id during window move/drag operations, + // we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch + // may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that. + if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n"); + g.MovingWindow = NULL; + } + + // Store deactivate data + ImGuiDeactivatedItemData* deactivated_data = &g.DeactivatedItemData; + deactivated_data->ID = g.ActiveId; + deactivated_data->ElapseFrame = (g.LastItemData.ID == g.ActiveId) ? g.FrameCount : g.FrameCount + 1; // FIXME: OK to use LastItemData? + deactivated_data->HasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore; + deactivated_data->IsAlive = (g.ActiveIdIsAlive == g.ActiveId); + + // This could be written in a more general way (e.g associate a hook to ActiveId), + // but since this is currently quite an exception we'll leave it as is. + // One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveID() + if (g.InputTextState.ID == g.ActiveId) + InputTextDeactivateHook(g.ActiveId); + } + + // Set active id + g.ActiveIdIsJustActivated = (g.ActiveId != id); + if (g.ActiveIdIsJustActivated) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() old:0x%08X (window \"%s\") -> new:0x%08X (window \"%s\")\n", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "", id, window ? window->Name : ""); + g.ActiveIdTimer = 0.0f; + g.ActiveIdHasBeenPressedBefore = false; + g.ActiveIdHasBeenEditedBefore = false; + g.ActiveIdMouseButton = -1; + if (id != 0) + { + g.LastActiveId = id; + g.LastActiveIdTimer = 0.0f; + } + } + g.ActiveId = id; + g.ActiveIdAllowOverlap = false; + g.ActiveIdNoClearOnFocusLoss = false; + g.ActiveIdWindow = window; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdFromShortcut = false; + if (id) + { + g.ActiveIdIsAlive = id; + g.ActiveIdSource = (g.NavActivateId == id || g.NavJustMovedToId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + IM_ASSERT(g.ActiveIdSource != ImGuiInputSource_None); + } + + // Clear declaration of inputs claimed by the widget + // (Please note that this is WIP and not all keys/inputs are thoroughly declared by all widgets yet) + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; +} + +void ImGui::ClearActiveID() +{ + SetActiveID(0, NULL); // g.ActiveId = 0; +} + +void ImGui::SetHoveredID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + if (id != 0 && g.HoveredIdPreviousFrame != id) + g.HoveredIdTimer = g.HoveredIdNotActiveTimer = 0.0f; +} + +ImGuiID ImGui::GetHoveredID() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; +} + +void ImGui::MarkItemEdited(ImGuiID id) +{ + // This marking is to be able to provide info for IsItemDeactivatedAfterEdit(). + // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. + ImGuiContext& g = *GImGui; + if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited) + return; + if (g.ActiveId == id || g.ActiveId == 0) + { + // FIXME: Can't we fully rely on LastItemData yet? + g.ActiveIdHasBeenEditedThisFrame = true; + g.ActiveIdHasBeenEditedBefore = true; + if (g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.HasBeenEditedBefore = true; + } + + // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) + // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); + + //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; +} + +bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) +{ + // An active popup disable hovering on other windows (apart from its own children) + // FIXME-OPT: This could be cached/stored within the window. + ImGuiContext& g = *GImGui; + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) + if (focused_root_window->WasActive && focused_root_window != window->RootWindow) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The 'else' is important because Modal windows are also Popups. + bool want_inhibit = false; + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + want_inhibit = true; + else if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + want_inhibit = true; + + // Inhibit hover unless the window is within the stack of our modal/popup + if (want_inhibit) + if (!IsWindowWithinBeginStackOf(window->RootWindow, focused_root_window)) + return false; + } + return true; +} + +static inline float CalcDelayFromHoveredFlags(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiHoveredFlags_DelayNormal) + return g.Style.HoverDelayNormal; + if (flags & ImGuiHoveredFlags_DelayShort) + return g.Style.HoverDelayShort; + return 0.0f; +} + +static ImGuiHoveredFlags ApplyHoverFlagsForTooltip(ImGuiHoveredFlags user_flags, ImGuiHoveredFlags shared_flags) +{ + // Allow instance flags to override shared flags + if (user_flags & (ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal)) + shared_flags &= ~(ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal); + return user_flags | shared_flags; +} + +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT_USER_ERROR((flags & ~ImGuiHoveredFlags_AllowedMaskForIsItemHovered) == 0, "Invalid flags for IsItemHovered()!"); + + if (g.NavHighlightItemUnderNav && g.NavCursorVisible && !(flags & ImGuiHoveredFlags_NoNavOverride)) + { + if (!IsItemFocused()) + return false; + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipNav); + } + else + { + // Test for bounding box overlap, as updated as ItemAdd() + ImGuiItemStatusFlags status_flags = g.LastItemData.StatusFlags; + if (!(status_flags & ImGuiItemStatusFlags_HoveredRect)) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + + // Done with rectangle culling so we can perform heavier checks now + // Test if we are hovering the right window (our window could be behind another window) + // [2021/03/02] Reworked / reverted the revert, finally. Note we want e.g. BeginGroup/ItemAdd/EndGroup to work as well. (#3851) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable + // to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was + // the test that has been running for a long while. + if (g.HoveredWindow != window && (status_flags & ImGuiItemStatusFlags_HoveredWindow) == 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByWindow) == 0) + return false; + + // Test if another item is active (e.g. being dragged) + const ImGuiID id = g.LastItemData.ID; + if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0) + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (g.ActiveId != window->MoveId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal. + // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. + if (!IsWindowContentHoverable(window, flags) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_NoWindowHoverableCheck)) + return false; + + // Test if the item is disabled + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + // Special handling for calling after Begin() which represent the title bar or tab. + // When the window is skipped/collapsed (SkipItems==true) that last item (always ->MoveId submitted by Begin) + // will never be overwritten so we need to detect the case. + if (id == window->MoveId && window->WriteAccessed) + return false; + + // Test if using AllowOverlap and overlapped + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap) && id != 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByItem) == 0) + if (g.HoveredIdPreviousFrame != g.LastItemData.ID) + return false; + } + + // Handle hover delay + // (some ideas: https://www.nngroup.com/articles/timing-exposing-content) + const float delay = CalcDelayFromHoveredFlags(flags); + if (delay > 0.0f || (flags & ImGuiHoveredFlags_Stationary)) + { + ImGuiID hover_delay_id = (g.LastItemData.ID != 0) ? g.LastItemData.ID : window->GetIDFromPos(g.LastItemData.Rect.Min); + if ((flags & ImGuiHoveredFlags_NoSharedDelay) && (g.HoverItemDelayIdPreviousFrame != hover_delay_id)) + g.HoverItemDelayTimer = 0.0f; + g.HoverItemDelayId = hover_delay_id; + + // When changing hovered item we requires a bit of stationary delay before activating hover timer, + // but once unlocked on a given item we also moving. + //if (g.HoverDelayTimer >= delay && (g.HoverDelayTimer - g.IO.DeltaTime < delay || g.MouseStationaryTimer - g.IO.DeltaTime < g.Style.HoverStationaryDelay)) { IMGUI_DEBUG_LOG("HoverDelayTimer = %f/%f, MouseStationaryTimer = %f\n", g.HoverDelayTimer, delay, g.MouseStationaryTimer); } + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverItemUnlockedStationaryId != hover_delay_id) + return false; + + if (g.HoverItemDelayTimer < delay) + return false; + } + + return true; +} + +// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). +// (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call) +// FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28. +// If you used this in your legacy/custom widgets code: +// - Commonly: if your ItemHoverable() call comes after an ItemAdd() call: pass 'item_flags = g.LastItemData.ItemFlags'. +// - Rare: otherwise you may pass 'item_flags = 0' (ImGuiItemFlags_None) unless you want to benefit from special behavior handled by ItemHoverable. +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Detect ID conflicts +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0 && g.HoveredIdPreviousFrame == id && (item_flags & ImGuiItemFlags_AllowDuplicateId) == 0) + { + g.HoveredIdPreviousFrameItemCount++; + if (g.DebugDrawIdConflicts == id) + window->DrawList->AddRect(bb.Min - ImVec2(1,1), bb.Max + ImVec2(1,1), IM_COL32(255, 0, 0, 255), 0.0f, ImDrawFlags_None, 2.0f); + } +#endif + + if (g.HoveredWindow != window) + return false; + if (!IsMouseHoveringRect(bb.Min, bb.Max)) + return false; + + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (!g.ActiveIdFromShortcut) + return false; + + // Done with rectangle culling so we can perform heavier checks now. + if (!(item_flags & ImGuiItemFlags_NoWindowHoverableCheck) && !IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) + { + g.HoveredIdIsDisabled = true; + return false; + } + + // We exceptionally allow this function to be called with id==0 to allow using it for easy high-level + // hover test in widgets code. We could also decide to split this function is two. + if (id != 0) + { + // Drag source doesn't report as hovered + if (g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) + return false; + + SetHoveredID(id); + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. + // This allows using patterns where a later submitted widget overlaps a previous one. Generally perceived as a front-to-back hit-test. + if (item_flags & ImGuiItemFlags_AllowOverlap) + { + g.HoveredIdAllowOverlap = true; + if (g.HoveredIdPreviousFrame != id) + return false; + } + + // Display shortcut (only works with mouse) + // (ImGuiItemStatusFlags_HasShortcut in LastItemData denotes we want a tooltip) + if (id == g.LastItemData.ID && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasShortcut) && g.ActiveId != id) + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal)) + SetTooltip("%s", GetKeyChordName(g.LastItemData.Shortcut)); + } + + // When disabled we'll return false but still set HoveredId + if (item_flags & ImGuiItemFlags_Disabled) + { + // Release active id if turning disabled + if (g.ActiveId == id && id != 0) + ClearActiveID(); + g.HoveredIdIsDisabled = true; + return false; + } + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + // [DEBUG] Item Picker tool! + // We perform the check here because reaching is path is rare (1~ time a frame), + // making the cost of this tool near-zero! We could get better call-stack and support picking non-hovered + // items if we performed the test in ItemAdd(), but that would incur a bigger runtime cost. + if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) + GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); + if (g.DebugItemPickerBreakId == id) + IM_DEBUG_BREAK(); + } +#endif + + if (g.NavHighlightItemUnderNav && (item_flags & ImGuiItemFlags_NoNavDisableMouseHover) == 0) + return false; + + return true; +} + +// FIXME: This is inlined/duplicated in ItemAdd() +// FIXME: The id != 0 path is not used by our codebase, may get rid of it? +bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!bb.Overlaps(window->ClipRect)) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return true; + return false; +} + +// This is also inlined in ItemAdd() +// Note: if ImGuiItemStatusFlags_HasDisplayRect is set, user needs to set g.LastItemData.DisplayRect. +void ImGui::SetLastItemData(ImGuiID item_id, ImGuiItemFlags item_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect) +{ + ImGuiContext& g = *GImGui; + g.LastItemData.ID = item_id; + g.LastItemData.ItemFlags = item_flags; + g.LastItemData.StatusFlags = status_flags; + g.LastItemData.Rect = g.LastItemData.NavRect = item_rect; +} + +static void ImGui::SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + SetLastItemData(window->MoveId, g.CurrentItemFlags, window->DC.WindowItemStatusFlags, rect); +} + +static void ImGui::SetLastItemDataForChildWindowItem(ImGuiWindow* window, const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + SetLastItemData(window->ChildId, g.CurrentItemFlags, window->DC.ChildItemStatusFlags, rect); +} + +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (wrap_pos_x == 0.0f) + { + // We could decide to setup a default wrapping max point for auto-resizing windows, + // or have auto-wrap (with unspecified wrapping pos) behave as a ContentSize extending function? + //if (window->Hidden && (window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) + // wrap_pos_x = ImMax(window->WorkRect.Min.x + g.FontSize * 10.0f, window->WorkRect.Max.x); + //else + wrap_pos_x = window->WorkRect.Max.x; + } + else if (wrap_pos_x > 0.0f) + { + wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + } + + return ImMax(wrap_pos_x - pos.x, 1.0f); +} + +// IM_ALLOC() == ImGui::MemAlloc() +void* ImGui::MemAlloc(size_t size) +{ + void* ptr = (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, size); +#endif + return ptr; +} + +// IM_FREE() == ImGui::MemFree() +void ImGui::MemFree(void* ptr) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ptr != NULL) + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, (size_t)-1); +#endif + return (*GImAllocatorFreeFunc)(ptr, GImAllocatorUserData); +} + +// We record the number of allocation in recent frames, as a way to audit/sanitize our guiding principles of "no allocations on idle/repeating frames" +void ImGui::DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size) +{ + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + IM_UNUSED(ptr); + if (entry->FrameCount != frame_count) + { + info->LastEntriesIdx = (info->LastEntriesIdx + 1) % IM_ARRAYSIZE(info->LastEntriesBuf); + entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + entry->FrameCount = frame_count; + entry->AllocCount = entry->FreeCount = 0; + } + if (size != (size_t)-1) + { + //printf("[%05d] MemAlloc(%d) -> 0x%p\n", frame_count, (int)size, ptr); + entry->AllocCount++; + info->TotalAllocCount++; + } + else + { + //printf("[%05d] MemFree(0x%p)\n", frame_count, ptr); + entry->FreeCount++; + info->TotalFreeCount++; + } +} + +const char* ImGui::GetClipboardText() +{ + ImGuiContext& g = *GImGui; + return g.PlatformIO.Platform_GetClipboardTextFn ? g.PlatformIO.Platform_GetClipboardTextFn(&g) : ""; +} + +void ImGui::SetClipboardText(const char* text) +{ + ImGuiContext& g = *GImGui; + if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) + g.PlatformIO.Platform_SetClipboardTextFn(&g, text); +} + +const char* ImGui::GetVersion() +{ + return IMGUI_VERSION; +} + +ImGuiIO& ImGui::GetIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->IO; +} + +// This variant exists to facilitate backends experimenting with multi-threaded parallel context. (#8069, #6293, #5856) +ImGuiIO& ImGui::GetIO(ImGuiContext* ctx) +{ + IM_ASSERT(ctx != NULL); + return ctx->IO; +} + +ImGuiPlatformIO& ImGui::GetPlatformIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext()?"); + return GImGui->PlatformIO; +} + +// This variant exists to facilitate backends experimenting with multi-threaded parallel context. (#8069, #6293, #5856) +ImGuiPlatformIO& ImGui::GetPlatformIO(ImGuiContext* ctx) +{ + IM_ASSERT(ctx != NULL); + return ctx->PlatformIO; +} + +// Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame() +ImDrawData* ImGui::GetDrawData() +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + return viewport->DrawDataP.Valid ? &viewport->DrawDataP : NULL; +} + +double ImGui::GetTime() +{ + return GImGui->Time; +} + +int ImGui::GetFrameCount() +{ + return GImGui->FrameCount; +} + +static ImDrawList* GetViewportBgFgDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) +{ + // Create the draw list on demand, because they are not frequently used for all viewports + ImGuiContext& g = *GImGui; + IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->BgFgDrawLists)); + ImDrawList* draw_list = viewport->BgFgDrawLists[drawlist_no]; + if (draw_list == NULL) + { + draw_list = IM_NEW(ImDrawList)(&g.DrawListSharedData); + draw_list->_OwnerName = drawlist_name; + viewport->BgFgDrawLists[drawlist_no] = draw_list; + } + + // Our ImDrawList system requires that there is always a command + if (viewport->BgFgDrawListsLastFrame[drawlist_no] != g.FrameCount) + { + draw_list->_ResetForNewFrame(); + draw_list->PushTextureID(g.IO.Fonts->TexID); + draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false); + viewport->BgFgDrawListsLastFrame[drawlist_no] = g.FrameCount; + } + return draw_list; +} + +ImDrawList* ImGui::GetBackgroundDrawList(ImGuiViewport* viewport) +{ + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 0, "##Background"); +} + +ImDrawList* ImGui::GetBackgroundDrawList() +{ + ImGuiContext& g = *GImGui; + return GetBackgroundDrawList(g.Viewports[0]); +} + +ImDrawList* ImGui::GetForegroundDrawList(ImGuiViewport* viewport) +{ + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); +} + +ImDrawList* ImGui::GetForegroundDrawList() +{ + ImGuiContext& g = *GImGui; + return GetForegroundDrawList(g.Viewports[0]); +} + +ImDrawListSharedData* ImGui::GetDrawListSharedData() +{ + return &GImGui->DrawListSharedData; +} + +void ImGui::StartMouseMovingWindow(ImGuiWindow* window) +{ + // Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows. + // We _also_ call this when clicking in a window empty space when io.ConfigWindowsMoveFromTitleBarOnly is set, but clear g.MovingWindow afterward. + // This is because we want ActiveId to be set even when the window is not permitted to move. + ImGuiContext& g = *GImGui; + FocusWindow(window); + SetActiveID(window->MoveId, window); + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindow->Pos; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingAllKeyboardKeys(); + + bool can_move_window = true; + if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindow->Flags & ImGuiWindowFlags_NoMove)) + can_move_window = false; + if (can_move_window) + g.MovingWindow = window; +} + +// Handle mouse moving window +// Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing() +// FIXME: We don't have strong guarantee that g.MovingWindow stay synced with g.ActiveId == g.MovingWindow->MoveId. +// This is currently enforced by the fact that BeginDragDropSource() is setting all g.ActiveIdUsingXXXX flags to inhibit navigation inputs, +// but if we should more thoroughly test cases where g.ActiveId or g.MovingWindow gets changed and not the other. +void ImGui::UpdateMouseMovingWindowNewFrame() +{ + ImGuiContext& g = *GImGui; + if (g.MovingWindow != NULL) + { + // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). + // We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. + KeepAliveID(g.ActiveId); + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); + ImGuiWindow* moving_window = g.MovingWindow->RootWindow; + if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos)) + { + ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; + SetWindowPos(moving_window, pos, ImGuiCond_Always); + FocusWindow(g.MovingWindow); + } + else + { + g.MovingWindow = NULL; + ClearActiveID(); + } + } + else + { + // When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others. + if (g.ActiveIdWindow && g.ActiveIdWindow->MoveId == g.ActiveId) + { + KeepAliveID(g.ActiveId); + if (!g.IO.MouseDown[0]) + ClearActiveID(); + } + } +} + +// Initiate focusing and moving window when clicking on empty space or title bar. +// Initiate focusing window when clicking on a disabled item. +// Handle left-click and right-click focus. +void ImGui::UpdateMouseMovingWindowEndFrame() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId != 0 || (g.HoveredId != 0 && !g.HoveredIdIsDisabled)) + return; + + // Unless we just made a window/popup appear + if (g.NavWindow && g.NavWindow->Appearing) + return; + + // Click on empty space to focus window and start moving + // (after we're done with all our widgets) + if (g.IO.MouseClicked[0]) + { + // Handle the edge case of a popup being closed while clicking in its empty space. + // If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more. + ImGuiWindow* root_window = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + const bool is_closed_popup = root_window && (root_window->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(root_window->PopupId, ImGuiPopupFlags_AnyPopupLevel); + + if (root_window != NULL && !is_closed_popup) + { + StartMouseMovingWindow(g.HoveredWindow); //-V595 + + // Cancel moving if clicked outside of title bar + if (g.IO.ConfigWindowsMoveFromTitleBarOnly) + if (!(root_window->Flags & ImGuiWindowFlags_NoTitleBar)) + if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) + g.MovingWindow = NULL; + + // Cancel moving if clicked over an item which was disabled or inhibited by popups + // (when g.HoveredIdIsDisabled == true && g.HoveredId == 0 we are inhibited by popups, when g.HoveredIdIsDisabled == true && g.HoveredId != 0 we are over a disabled item)0 already) + if (g.HoveredIdIsDisabled) + g.MovingWindow = NULL; + } + else if (root_window == NULL && g.NavWindow != NULL) + { + // Clicking on void disable focus + FocusWindow(NULL, ImGuiFocusRequestFlags_UnlessBelowModal); + } + } + + // With right mouse button we close popups without changing focus based on where the mouse is aimed + // Instead, focus will be restored to the window under the bottom-most closed popup. + // (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger) + if (g.IO.MouseClicked[1] && g.HoveredId == 0) + { + // Find the top-most window between HoveredWindow and the top-most Modal Window. + // This is where we can trim the popup stack. + ImGuiWindow* modal = GetTopMostPopupModal(); + bool hovered_window_above_modal = g.HoveredWindow && (modal == NULL || IsWindowAbove(g.HoveredWindow, modal)); + ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal, true); + } +} + +static bool IsWindowActiveAndVisible(ImGuiWindow* window) +{ + return (window->Active) && (!window->Hidden); +} + +// The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app) +void ImGui::UpdateHoveredWindowAndCaptureFlags() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // FIXME-DPI: This storage was added on 2021/03/31 for test engine, but if we want to multiply WINDOWS_HOVER_PADDING + // by DpiScale, we need to make this window-agnostic anyhow, maybe need storing inside ImGuiWindow. + g.WindowsBorderHoverPadding = ImMax(ImMax(g.Style.TouchExtraPadding.x, g.Style.TouchExtraPadding.y), g.Style.WindowBorderHoverPadding); + + // Find the window hovered by mouse: + // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. + // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point of the frame. + // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. + bool clear_hovered_windows = false; + FindHoveredWindowEx(g.IO.MousePos, false, &g.HoveredWindow, &g.HoveredWindowUnderMovingWindow); + g.HoveredWindowBeforeClear = g.HoveredWindow; + + // Modal windows prevents mouse from hovering behind them. + ImGuiWindow* modal_window = GetTopMostPopupModal(); + if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) + clear_hovered_windows = true; + + // Disabled mouse hovering (we don't currently clear MousePos, we could) + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) + clear_hovered_windows = true; + + // We track click ownership. When clicked outside of a window the click is owned by the application and + // won't report hovering nor request capture even while dragging over our windows afterward. + const bool has_open_popup = (g.OpenPopupStack.Size > 0); + const bool has_open_modal = (modal_window != NULL); + int mouse_earliest_down = -1; + bool mouse_any_down = false; + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + if (io.MouseClicked[i]) + { + io.MouseDownOwned[i] = (g.HoveredWindow != NULL) || has_open_popup; + io.MouseDownOwnedUnlessPopupClose[i] = (g.HoveredWindow != NULL) || has_open_modal; + } + mouse_any_down |= io.MouseDown[i]; + if (io.MouseDown[i] || io.MouseReleased[i]) // Increase release frame for our evaluation of earliest button (#1392) + if (mouse_earliest_down == -1 || io.MouseClickedTime[i] < io.MouseClickedTime[mouse_earliest_down]) + mouse_earliest_down = i; + } + const bool mouse_avail = (mouse_earliest_down == -1) || io.MouseDownOwned[mouse_earliest_down]; + const bool mouse_avail_unless_popup_close = (mouse_earliest_down == -1) || io.MouseDownOwnedUnlessPopupClose[mouse_earliest_down]; + + // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. + // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) + const bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; + if (!mouse_avail && !mouse_dragging_extern_payload) + clear_hovered_windows = true; + + if (clear_hovered_windows) + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + + // Update io.WantCaptureMouse for the user application (true = dispatch mouse info to Dear ImGui only, false = dispatch mouse to Dear ImGui + underlying app) + // Update io.WantCaptureMouseAllowPopupClose (experimental) to give a chance for app to react to popup closure with a drag + if (g.WantCaptureMouseNextFrame != -1) + { + io.WantCaptureMouse = io.WantCaptureMouseUnlessPopupClose = (g.WantCaptureMouseNextFrame != 0); + } + else + { + io.WantCaptureMouse = (mouse_avail && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_popup; + io.WantCaptureMouseUnlessPopupClose = (mouse_avail_unless_popup_close && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_modal; + } + + // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app) + io.WantCaptureKeyboard = false; + if ((io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) == 0) + { + if ((g.ActiveId != 0) || (modal_window != NULL)) + io.WantCaptureKeyboard = true; + else if (io.NavActive && (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && io.ConfigNavCaptureKeyboard) + io.WantCaptureKeyboard = true; + } + if (g.WantCaptureKeyboardNextFrame != -1) // Manual override + io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); + + // Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible + io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; +} + +// Called once a frame. Followed by SetCurrentFont() which sets up the remaining data. +// FIXME-VIEWPORT: the concept of a single ClipRectFullscreen is not ideal! +static void SetupDrawListSharedData() +{ + ImGuiContext& g = *GImGui; + ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiViewportP* viewport : g.Viewports) + virtual_space.Add(viewport->GetMainRect()); + g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); + g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; + if (g.Style.AntiAliasedLines) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; + if (g.Style.AntiAliasedLinesUseTex && !(g.IO.Fonts->Flags & ImFontAtlasFlags_NoBakedLines)) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; + if (g.Style.AntiAliasedFill) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; + if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; + g.DrawListSharedData.InitialFringeScale = 1.0f; // FIXME-DPI: Change this for some DPI scaling experiments. +} + +void ImGui::NewFrame() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + ImGuiContext& g = *GImGui; + + // Remove pending delete hooks before frame start. + // This deferred removal avoid issues of removal while iterating the hook vector + for (int n = g.Hooks.Size - 1; n >= 0; n--) + if (g.Hooks[n].Type == ImGuiContextHookType_PendingRemoval_) + g.Hooks.erase(&g.Hooks[n]); + + CallContextHooks(&g, ImGuiContextHookType_NewFramePre); + + // Check and assert for various common IO and Configuration mistakes + ErrorCheckNewFrameSanityChecks(); + + // Load settings on first frame, save settings when modified (after a delay) + UpdateSettings(); + + g.Time += g.IO.DeltaTime; + g.WithinFrameScope = true; + g.FrameCount += 1; + g.TooltipOverrideCount = 0; + g.WindowsActiveCount = 0; + g.MenusIdSubmittedThisFrame.resize(0); + + // Calculate frame-rate for the user, as a purely luxurious feature + g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; + g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; + g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); + g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, IM_ARRAYSIZE(g.FramerateSecPerFrame)); + g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)g.FramerateSecPerFrameCount)) : FLT_MAX; + + // Process input queue (trickle as many events as possible), turn events into writes to IO structure + g.InputEventsTrail.resize(0); + UpdateInputEvents(g.IO.ConfigInputTrickleEventQueue); + + // Update viewports (after processing input queue, so io.MouseHoveredViewport is set) + UpdateViewportsNewFrame(); + + // Setup current font and draw list shared data + g.IO.Fonts->Locked = true; + SetupDrawListSharedData(); + SetCurrentFont(GetDefaultFont()); + IM_ASSERT(g.Font->IsLoaded()); + + // Mark rendering data as invalid to prevent user who may have a handle on it to use it. + for (ImGuiViewportP* viewport : g.Viewports) + viewport->DrawDataP.Valid = false; + + // Drag and drop keep the source ID alive so even if the source disappear our state is consistent + if (g.DragDropActive && g.DragDropPayload.SourceId == g.ActiveId) + KeepAliveID(g.DragDropPayload.SourceId); + + // [DEBUG] + if (!g.IO.ConfigDebugHighlightIdConflicts || !g.IO.KeyCtrl) // Count is locked while holding CTRL + g.DebugDrawIdConflicts = 0; + if (g.IO.ConfigDebugHighlightIdConflicts && g.HoveredIdPreviousFrameItemCount > 1) + g.DebugDrawIdConflicts = g.HoveredIdPreviousFrame; + + // Update HoveredId data + if (!g.HoveredIdPreviousFrame) + g.HoveredIdTimer = 0.0f; + if (!g.HoveredIdPreviousFrame || (g.HoveredId && g.ActiveId == g.HoveredId)) + g.HoveredIdNotActiveTimer = 0.0f; + if (g.HoveredId) + g.HoveredIdTimer += g.IO.DeltaTime; + if (g.HoveredId && g.ActiveId != g.HoveredId) + g.HoveredIdNotActiveTimer += g.IO.DeltaTime; + g.HoveredIdPreviousFrame = g.HoveredId; + g.HoveredIdPreviousFrameItemCount = 0; + g.HoveredId = 0; + g.HoveredIdAllowOverlap = false; + g.HoveredIdIsDisabled = false; + + // Clear ActiveID if the item is not alive anymore. + // In 1.87, the common most call to KeepAliveID() was moved from GetID() to ItemAdd(). + // As a result, custom widget using ButtonBehavior() _without_ ItemAdd() need to call KeepAliveID() themselves. + if (g.ActiveId != 0 && g.ActiveIdIsAlive != g.ActiveId && g.ActiveIdPreviousFrame == g.ActiveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("NewFrame(): ClearActiveID() because it isn't marked alive anymore!\n"); + ClearActiveID(); + } + + // Update ActiveId data (clear reference to active widget if the widget isn't alive anymore) + if (g.ActiveId) + g.ActiveIdTimer += g.IO.DeltaTime; + g.LastActiveIdTimer += g.IO.DeltaTime; + g.ActiveIdPreviousFrame = g.ActiveId; + g.ActiveIdIsAlive = 0; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdIsJustActivated = false; + if (g.TempInputId != 0 && g.ActiveId != g.TempInputId) + g.TempInputId = 0; + if (g.ActiveId == 0) + { + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; + } + if (g.DeactivatedItemData.ElapseFrame < g.FrameCount) + g.DeactivatedItemData.ID = 0; + g.DeactivatedItemData.IsAlive = false; + + // Record when we have been stationary as this state is preserved while over same item. + // FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values. + // To allow this we should store HoverItemMaxStationaryTime+ID and perform the >= check in IsItemHovered() function. + if (g.HoverItemDelayId != 0 && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverItemUnlockedStationaryId = g.HoverItemDelayId; + else if (g.HoverItemDelayId == 0) + g.HoverItemUnlockedStationaryId = 0; + if (g.HoveredWindow != NULL && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverWindowUnlockedStationaryId = g.HoveredWindow->ID; + else if (g.HoveredWindow == NULL) + g.HoverWindowUnlockedStationaryId = 0; + + // Update hover delay for IsItemHovered() with delays and tooltips + g.HoverItemDelayIdPreviousFrame = g.HoverItemDelayId; + if (g.HoverItemDelayId != 0) + { + g.HoverItemDelayTimer += g.IO.DeltaTime; + g.HoverItemDelayClearTimer = 0.0f; + g.HoverItemDelayId = 0; + } + else if (g.HoverItemDelayTimer > 0.0f) + { + // This gives a little bit of leeway before clearing the hover timer, allowing mouse to cross gaps + // We could expose 0.25f as style.HoverClearDelay but I am not sure of the logic yet, this is particularly subtle. + g.HoverItemDelayClearTimer += g.IO.DeltaTime; + if (g.HoverItemDelayClearTimer >= ImMax(0.25f, g.IO.DeltaTime * 2.0f)) // ~7 frames at 30 Hz + allow for low framerate + g.HoverItemDelayTimer = g.HoverItemDelayClearTimer = 0.0f; // May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer. + } + + // Drag and drop + g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; + g.DragDropAcceptIdCurr = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropWithinSource = false; + g.DragDropWithinTarget = false; + g.DragDropHoldJustPressedId = 0; + g.TooltipPreviousWindow = NULL; + + // Close popups on focus lost (currently wip/opt-in) + //if (g.IO.AppFocusLost) + // ClosePopupsExceptModals(); + + // Update keyboard input state + UpdateKeyboardInputs(); + + //IM_ASSERT(g.IO.KeyCtrl == IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl)); + //IM_ASSERT(g.IO.KeyShift == IsKeyDown(ImGuiKey_LeftShift) || IsKeyDown(ImGuiKey_RightShift)); + //IM_ASSERT(g.IO.KeyAlt == IsKeyDown(ImGuiKey_LeftAlt) || IsKeyDown(ImGuiKey_RightAlt)); + //IM_ASSERT(g.IO.KeySuper == IsKeyDown(ImGuiKey_LeftSuper) || IsKeyDown(ImGuiKey_RightSuper)); + + // Update keyboard/gamepad navigation + NavUpdate(); + + // Update mouse input state + UpdateMouseInputs(); + + // Mark all windows as not visible and compact unused memory. + IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); + const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; + for (ImGuiWindow* window : g.Windows) + { + window->WasActive = window->Active; + window->Active = false; + window->WriteAccessed = false; + window->BeginCountPreviousFrame = window->BeginCount; + window->BeginCount = 0; + + // Garbage collect transient buffers of recently unused windows + if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) + GcCompactTransientWindowBuffers(window); + } + + // Find hovered window + // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) + // (currently needs to be done after the WasActive=Active loop and FindHoveredWindowEx uses ->Active) + UpdateHoveredWindowAndCaptureFlags(); + + // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) + UpdateMouseMovingWindowNewFrame(); + + // Background darkening/whitening + if (GetTopMostPopupModal() != NULL || (g.NavWindowingTarget != NULL && g.NavWindowingHighlightAlpha > 0.0f)) + g.DimBgRatio = ImMin(g.DimBgRatio + g.IO.DeltaTime * 6.0f, 1.0f); + else + g.DimBgRatio = ImMax(g.DimBgRatio - g.IO.DeltaTime * 10.0f, 0.0f); + + g.MouseCursor = ImGuiMouseCursor_Arrow; + g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; + + // Platform IME data: reset for the frame + g.PlatformImeDataPrev = g.PlatformImeData; + g.PlatformImeData.WantVisible = false; + + // Mouse wheel scrolling, scale + UpdateMouseWheel(); + + // Garbage collect transient buffers of recently unused tables + for (int i = 0; i < g.TablesLastTimeActive.Size; i++) + if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) + TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); + for (ImGuiTableTempData& table_temp_data : g.TablesTempData) + if (table_temp_data.LastTimeActive >= 0.0f && table_temp_data.LastTimeActive < memory_compact_start_time) + TableGcCompactTransientBuffers(&table_temp_data); + if (g.GcCompactAll) + GcCompactTransientMiscBuffers(); + g.GcCompactAll = false; + + // Closing the focused window restore focus to the first active root window in descending z-order + if (g.NavWindow && !g.NavWindow->WasActive) + FocusTopMostWindowUnderOne(NULL, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); + + // No window should be open at the beginning of the frame. + // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. + g.CurrentWindowStack.resize(0); + g.BeginPopupStack.resize(0); + g.ItemFlagsStack.resize(0); + g.ItemFlagsStack.push_back(ImGuiItemFlags_AutoClosePopups); // Default flags + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.GroupStack.resize(0); + + // [DEBUG] Update debug features +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + UpdateDebugToolItemPicker(); + UpdateDebugToolStackQueries(); + UpdateDebugToolFlashStyleColor(); + if (g.DebugLocateFrames > 0 && --g.DebugLocateFrames == 0) + { + g.DebugLocateId = 0; + g.DebugBreakInLocateId = false; + } + if (g.DebugLogAutoDisableFrames > 0 && --g.DebugLogAutoDisableFrames == 0) + { + DebugLog("(Debug Log: Auto-disabled some ImGuiDebugLogFlags after 2 frames)\n"); + g.DebugLogFlags &= ~g.DebugLogAutoDisableFlags; + g.DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + } +#endif + + // Create implicit/fallback window - which we will only render it if the user has added something to it. + // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. + // This fallback is particularly important as it prevents ImGui:: calls from crashing. + g.WithinFrameScopeWithImplicitWindow = true; + SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver); + Begin("Debug##Default"); + IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true); + + // Store stack sizes + g.ErrorCountCurrentFrame = 0; + ErrorRecoveryStoreState(&g.StackSizesInNewFrame); + + // [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack, + // allowing to validate correct Begin/End behavior in user code. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.IO.ConfigDebugBeginReturnValueLoop) + g.DebugBeginReturnValueCullDepth = (g.DebugBeginReturnValueCullDepth == -1) ? 0 : ((g.DebugBeginReturnValueCullDepth + ((g.FrameCount % 4) == 0 ? 1 : 0)) % 10); + else + g.DebugBeginReturnValueCullDepth = -1; +#endif + + CallContextHooks(&g, ImGuiContextHookType_NewFramePost); +} + +// FIXME: Add a more explicit sort order in the window structure. +static int IMGUI_CDECL ChildWindowComparer(const void* lhs, const void* rhs) +{ + const ImGuiWindow* const a = *(const ImGuiWindow* const *)lhs; + const ImGuiWindow* const b = *(const ImGuiWindow* const *)rhs; + if (int d = (a->Flags & ImGuiWindowFlags_Popup) - (b->Flags & ImGuiWindowFlags_Popup)) + return d; + if (int d = (a->Flags & ImGuiWindowFlags_Tooltip) - (b->Flags & ImGuiWindowFlags_Tooltip)) + return d; + return (a->BeginOrderWithinParent - b->BeginOrderWithinParent); +} + +static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window) +{ + out_sorted_windows->push_back(window); + if (window->Active) + { + int count = window->DC.ChildWindows.Size; + ImQsort(window->DC.ChildWindows.Data, (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); + for (int i = 0; i < count; i++) + { + ImGuiWindow* child = window->DC.ChildWindows[i]; + if (child->Active) + AddWindowToSortBuffer(out_sorted_windows, child); + } + } +} + +static void AddWindowToDrawData(ImGuiWindow* window, int layer) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = g.Viewports[0]; + g.IO.MetricsRenderWindows++; + if (window->DrawList->_Splitter._Count > 1) + window->DrawList->ChannelsMerge(); // Merge if user forgot to merge back. Also required in Docking branch for ImGuiWindowFlags_DockNodeHost windows. + ImGui::AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[layer], window->DrawList); + for (ImGuiWindow* child : window->DC.ChildWindows) + if (IsWindowActiveAndVisible(child)) // Clipped children may have been marked not active + AddWindowToDrawData(child, layer); +} + +static inline int GetWindowDisplayLayer(ImGuiWindow* window) +{ + return (window->Flags & ImGuiWindowFlags_Tooltip) ? 1 : 0; +} + +// Layer is locked for the root window, however child windows may use a different viewport (e.g. extruding menu) +static inline void AddRootWindowToDrawData(ImGuiWindow* window) +{ + AddWindowToDrawData(window, GetWindowDisplayLayer(window)); +} + +static void FlattenDrawDataIntoSingleLayer(ImDrawDataBuilder* builder) +{ + int n = builder->Layers[0]->Size; + int full_size = n; + for (int i = 1; i < IM_ARRAYSIZE(builder->Layers); i++) + full_size += builder->Layers[i]->Size; + builder->Layers[0]->resize(full_size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(builder->Layers); layer_n++) + { + ImVector* layer = builder->Layers[layer_n]; + if (layer->empty()) + continue; + memcpy(builder->Layers[0]->Data + n, layer->Data, layer->Size * sizeof(ImDrawList*)); + n += layer->Size; + layer->resize(0); + } +} + +static void InitViewportDrawData(ImGuiViewportP* viewport) +{ + ImGuiIO& io = ImGui::GetIO(); + ImDrawData* draw_data = &viewport->DrawDataP; + + viewport->DrawDataBuilder.Layers[0] = &draw_data->CmdLists; + viewport->DrawDataBuilder.Layers[1] = &viewport->DrawDataBuilder.LayerData1; + viewport->DrawDataBuilder.Layers[0]->resize(0); + viewport->DrawDataBuilder.Layers[1]->resize(0); + + draw_data->Valid = true; + draw_data->CmdListsCount = 0; + draw_data->TotalVtxCount = draw_data->TotalIdxCount = 0; + draw_data->DisplayPos = viewport->Pos; + draw_data->DisplaySize = viewport->Size; + draw_data->FramebufferScale = io.DisplayFramebufferScale; + draw_data->OwnerViewport = viewport; +} + +// Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. +// - When using this function it is sane to ensure that float are perfectly rounded to integer values, +// so that e.g. (int)(max.x-min.x) in user's render produce correct result. +// - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect(): +// some frequently called functions which to modify both channels and clipping simultaneously tend to use the +// more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds. +// - This is analoguous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack, +// which in the case of ClipRect is not so problematic but tends to be more restrictive for fonts. +void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +void ImGui::PopClipRect() +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PopClipRect(); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + ImGuiViewportP* viewport = (ImGuiViewportP*)GetMainViewport(); + ImRect viewport_rect = viewport->GetMainRect(); + + // Draw behind window by moving the draw command at the FRONT of the draw list + { + // We've already called AddWindowToDrawData() which called DrawList->ChannelsMerge() on DockNodeHost windows, + // and draw list have been trimmed already, hence the explicit recreation of a draw command if missing. + // FIXME: This is creating complication, might be simpler if we could inject a drawlist in drawdata at a given position and not attempt to manipulate ImDrawCmd order. + ImDrawList* draw_list = window->RootWindow->DrawList; + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // FIXME: Need to stricty ensure ImDrawCmd are not merged (ElemCount==6 checks below will verify that) + draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col); + ImDrawCmd cmd = draw_list->CmdBuffer.back(); + IM_ASSERT(cmd.ElemCount == 6); + draw_list->CmdBuffer.pop_back(); + draw_list->CmdBuffer.push_front(cmd); + draw_list->AddDrawCmd(); // We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command. + draw_list->PopClipRect(); + } +} + +ImGuiWindow* ImGui::FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* parent_window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* bottom_most_visible_window = parent_window; + for (int i = FindWindowDisplayIndex(parent_window); i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + if (!IsWindowWithinBeginStackOf(window, parent_window)) + break; + if (IsWindowActiveAndVisible(window) && GetWindowDisplayLayer(window) <= GetWindowDisplayLayer(parent_window)) + bottom_most_visible_window = window; + } + return bottom_most_visible_window; +} + +static void ImGui::RenderDimmedBackgrounds() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* modal_window = GetTopMostAndVisiblePopupModal(); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + return; + const bool dim_bg_for_modal = (modal_window != NULL); + const bool dim_bg_for_window_list = (g.NavWindowingTargetAnim != NULL && g.NavWindowingTargetAnim->Active); + if (!dim_bg_for_modal && !dim_bg_for_window_list) + return; + + if (dim_bg_for_modal) + { + // Draw dimming behind modal or a begin stack child, whichever comes first in draw order. + ImGuiWindow* dim_behind_window = FindBottomMostVisibleWindowWithinBeginStack(modal_window); + RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(modal_window->DC.ModalDimBgColor, g.DimBgRatio)); + } + else if (dim_bg_for_window_list) + { + // Draw dimming behind CTRL+Tab target window and behind CTRL+Tab UI window + RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + + // Draw border around CTRL+Tab target window + ImGuiWindow* window = g.NavWindowingTargetAnim; + ImGuiViewport* viewport = GetMainViewport(); + float distance = g.FontSize; + ImRect bb = window->Rect(); + bb.Expand(distance); + if (bb.GetWidth() >= viewport->Size.x && bb.GetHeight() >= viewport->Size.y) + bb.Expand(-distance - 1.0f); // If a window fits the entire viewport, adjust its highlight inward + if (window->DrawList->CmdBuffer.Size == 0) + window->DrawList->AddDrawCmd(); + window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); + window->DrawList->PopClipRect(); + } +} + +// This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. +void ImGui::EndFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + + // Don't process EndFrame() multiple times. + if (g.FrameCountEnded == g.FrameCount) + return; + IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?"); + + CallContextHooks(&g, ImGuiContextHookType_EndFramePre); + + // [EXPERIMENTAL] Recover from errors + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverState(&g.StackSizesInNewFrame); + ErrorCheckEndFrameSanityChecks(); + ErrorCheckEndFrameFinalizeErrorTooltip(); + + // Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) + ImGuiPlatformImeData* ime_data = &g.PlatformImeData; + if (g.PlatformIO.Platform_SetImeDataFn != NULL && memcmp(ime_data, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0) + { + IMGUI_DEBUG_LOG_IO("[io] Calling Platform_SetImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f)\n", ime_data->WantVisible, ime_data->InputPos.x, ime_data->InputPos.y); + ImGuiViewport* viewport = GetMainViewport(); + g.PlatformIO.Platform_SetImeDataFn(&g, viewport, ime_data); + } + + // Hide implicit/fallback "Debug" window if it hasn't been used + g.WithinFrameScopeWithImplicitWindow = false; + if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) + g.CurrentWindow->Active = false; + End(); + + // Update navigation: CTRL+Tab, wrap-around requests + NavEndFrame(); + + // Drag and Drop: Elapse payload (if delivered, or if source stops being submitted) + if (g.DragDropActive) + { + bool is_delivered = g.DragDropPayload.Delivery; + bool is_elapsed = (g.DragDropSourceFrameCount + 1 < g.FrameCount) && ((g.DragDropSourceFlags & ImGuiDragDropFlags_PayloadAutoExpire) || g.DragDropMouseButton == -1 || !IsMouseDown(g.DragDropMouseButton)); + if (is_delivered || is_elapsed) + ClearDragDrop(); + } + + // Drag and Drop: Fallback for missing source tooltip. This is not ideal but better than nothing. + // If you want to handle source item disappearing: instead of submitting your description tooltip + // in the BeginDragDropSource() block of the dragged item, you can submit them from a safe single spot + // (e.g. end of your item loop, or before EndFrame) by reading payload data. + // In the typical case, the contents of drag tooltip should be possible to infer solely from payload data. + if (g.DragDropActive && g.DragDropSourceFrameCount + 1 < g.FrameCount && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + g.DragDropWithinSource = true; + SetTooltip("..."); + g.DragDropWithinSource = false; + } + + // End frame + g.WithinFrameScope = false; + g.FrameCountEnded = g.FrameCount; + + // Initiate moving window + handle left-click and right-click focus + UpdateMouseMovingWindowEndFrame(); + + // Sort the window list so that all child windows are after their parent + // We cannot do that on FocusWindow() because children may not exist yet + g.WindowsTempSortBuffer.resize(0); + g.WindowsTempSortBuffer.reserve(g.Windows.Size); + for (ImGuiWindow* window : g.Windows) + { + if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it + continue; + AddWindowToSortBuffer(&g.WindowsTempSortBuffer, window); + } + + // This usually assert if there is a mismatch between the ImGuiWindowFlags_ChildWindow / ParentWindow values and DC.ChildWindows[] in parents, aka we've done something wrong. + IM_ASSERT(g.Windows.Size == g.WindowsTempSortBuffer.Size); + g.Windows.swap(g.WindowsTempSortBuffer); + g.IO.MetricsActiveWindows = g.WindowsActiveCount; + + // Unlock font atlas + g.IO.Fonts->Locked = false; + + // Clear Input data for next frame + g.IO.MousePosPrev = g.IO.MousePos; + g.IO.AppFocusLost = false; + g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; + g.IO.InputQueueCharacters.resize(0); + + CallContextHooks(&g, ImGuiContextHookType_EndFramePost); +} + +// Prepare the data for rendering so you can call GetDrawData() +// (As with anything within the ImGui:: namspace this doesn't touch your GPU or graphics API at all: +// it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend) +void ImGui::Render() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + + if (g.FrameCountEnded != g.FrameCount) + EndFrame(); + if (g.FrameCountRendered == g.FrameCount) + return; + g.FrameCountRendered = g.FrameCount; + + g.IO.MetricsRenderWindows = 0; + CallContextHooks(&g, ImGuiContextHookType_RenderPre); + + // Add background ImDrawList (for each active viewport) + for (ImGuiViewportP* viewport : g.Viewports) + { + InitViewportDrawData(viewport); + if (viewport->BgFgDrawLists[0] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); + } + + // Draw modal/window whitening backgrounds + RenderDimmedBackgrounds(); + + // Add ImDrawList to render + ImGuiWindow* windows_to_render_top_most[2]; + windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindow : NULL; + windows_to_render_top_most[1] = (g.NavWindowingTarget ? g.NavWindowingListWindow : NULL); + for (ImGuiWindow* window : g.Windows) + { + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + if (IsWindowActiveAndVisible(window) && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0 && window != windows_to_render_top_most[0] && window != windows_to_render_top_most[1]) + AddRootWindowToDrawData(window); + } + for (int n = 0; n < IM_ARRAYSIZE(windows_to_render_top_most); n++) + if (windows_to_render_top_most[n] && IsWindowActiveAndVisible(windows_to_render_top_most[n])) // NavWindowingTarget is always temporarily displayed as the top-most window + AddRootWindowToDrawData(windows_to_render_top_most[n]); + + // Draw software mouse cursor if requested by io.MouseDrawCursor flag + if (g.IO.MouseDrawCursor && g.MouseCursor != ImGuiMouseCursor_None) + RenderMouseCursor(g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); + + // Setup ImDrawData structures for end-user + g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = 0; + for (ImGuiViewportP* viewport : g.Viewports) + { + FlattenDrawDataIntoSingleLayer(&viewport->DrawDataBuilder); + + // Add foreground ImDrawList (for each active viewport) + if (viewport->BgFgDrawLists[1] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); + + // We call _PopUnusedDrawCmd() last thing, as RenderDimmedBackgrounds() rely on a valid command being there (especially in docking branch). + ImDrawData* draw_data = &viewport->DrawDataP; + IM_ASSERT(draw_data->CmdLists.Size == draw_data->CmdListsCount); + for (ImDrawList* draw_list : draw_data->CmdLists) + draw_list->_PopUnusedDrawCmd(); + + g.IO.MetricsRenderVertices += draw_data->TotalVtxCount; + g.IO.MetricsRenderIndices += draw_data->TotalIdxCount; + } + + CallContextHooks(&g, ImGuiContextHookType_RenderPost); +} + +// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. +// CalcTextSize("") should return ImVec2(0.0f, g.FontSize) +ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) +{ + ImGuiContext& g = *GImGui; + + const char* text_display_end; + if (hide_text_after_double_hash) + text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string + else + text_display_end = text_end; + + ImFont* font = g.Font; + const float font_size = g.FontSize; + if (text == text_display_end) + return ImVec2(0.0f, font_size); + ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); + + // Round + // FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out. + // FIXME: Investigate using ceilf or e.g. + // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c + // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html + text_size.x = IM_TRUNC(text_size.x + 0.99999f); + + return text_size; +} + +// Find window given position, search front-to-back +// - Typically write output back to g.HoveredWindow and g.HoveredWindowUnderMovingWindow. +// - FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programmatically +// with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is +// called, aka before the next Begin(). Moving window isn't affected. +// - The 'find_first_and_in_any_viewport = true' mode is only used by TestEngine. It is simpler to maintain here. +void ImGui::FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* hovered_window = NULL; + ImGuiWindow* hovered_window_under_moving_window = NULL; + + if (find_first_and_in_any_viewport == false && g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) + hovered_window = g.MovingWindow; + + ImVec2 padding_regular = g.Style.TouchExtraPadding; + ImVec2 padding_for_resize = ImMax(g.Style.TouchExtraPadding, ImVec2(g.Style.WindowBorderHoverPadding, g.Style.WindowBorderHoverPadding)); + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (!window->WasActive || window->Hidden) + continue; + if (window->Flags & ImGuiWindowFlags_NoMouseInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImVec2 hit_padding = (window->Flags & (ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) ? padding_regular : padding_for_resize; + if (!window->OuterRectClipped.ContainsWithPad(pos, hit_padding)) + continue; + + // Support for one rectangular hole in any given window + // FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512) + if (window->HitTestHoleSize.x != 0) + { + ImVec2 hole_pos(window->Pos.x + (float)window->HitTestHoleOffset.x, window->Pos.y + (float)window->HitTestHoleOffset.y); + ImVec2 hole_size((float)window->HitTestHoleSize.x, (float)window->HitTestHoleSize.y); + if (ImRect(hole_pos, hole_pos + hole_size).Contains(pos)) + continue; + } + + if (find_first_and_in_any_viewport) + { + hovered_window = window; + break; + } + else + { + if (hovered_window == NULL) + hovered_window = window; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (hovered_window_under_moving_window == NULL && (!g.MovingWindow || window->RootWindow != g.MovingWindow->RootWindow)) + hovered_window_under_moving_window = window; + if (hovered_window && hovered_window_under_moving_window) + break; + } + } + + *out_hovered_window = hovered_window; + if (out_hovered_window_under_moving_window != NULL) + *out_hovered_window_under_moving_window = hovered_window_under_moving_window; +} + +bool ImGui::IsItemActive() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId) + return g.ActiveId == g.LastItemData.ID; + return false; +} + +bool ImGui::IsItemActivated() +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId) + if (g.ActiveId == g.LastItemData.ID && g.ActiveIdPreviousFrame != g.LastItemData.ID) + return true; + return false; +} + +bool ImGui::IsItemDeactivated() +{ + ImGuiContext& g = *GImGui; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDeactivated) + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Deactivated) != 0; + return (g.DeactivatedItemData.ID == g.LastItemData.ID && g.LastItemData.ID != 0 && g.DeactivatedItemData.ElapseFrame >= g.FrameCount); +} + +bool ImGui::IsItemDeactivatedAfterEdit() +{ + ImGuiContext& g = *GImGui; + return IsItemDeactivated() && g.DeactivatedItemData.HasBeenEditedBefore; +} + +// == (GetItemID() == GetFocusID() && GetFocusID() != 0) +bool ImGui::IsItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId == g.LastItemData.ID && g.NavId != 0; +} + +// Important: this can be useful but it is NOT equivalent to the behavior of e.g.Button()! +// Most widgets have specific reactions based on mouse-up/down state, mouse position etc. +bool ImGui::IsItemClicked(ImGuiMouseButton mouse_button) +{ + return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); +} + +bool ImGui::IsItemToggledOpen() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; +} + +// Call after a Selectable() or TreeNode() involved in multi-selection. +// Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose. +// This is only meant to be called inside a BeginMultiSelect()/EndMultiSelect() block. +// (Outside of multi-select, it would be misleading/ambiguous to report this signal, as widgets +// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.) +bool ImGui::IsItemToggledSelection() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentMultiSelect != NULL); // Can only be used inside a BeginMultiSelect()/EndMultiSelect() + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; +} + +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. +bool ImGui::IsAnyItemHovered() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; +} + +bool ImGui::IsAnyItemActive() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId != 0; +} + +bool ImGui::IsAnyItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId != 0 && g.NavCursorVisible; +} + +bool ImGui::IsItemVisible() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) != 0; +} + +bool ImGui::IsItemEdited() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Edited) != 0; +} + +// Allow next item to be overlapped by subsequent items. +// This works by requiring HoveredId to match for two subsequent frames, +// so if a following items overwrite it our interactions will naturally be disabled. +void ImGui::SetNextItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_AllowOverlap; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. +// FIXME-LEGACY: Use SetNextItemAllowOverlap() *before* your item instead. +void ImGui::SetItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (g.HoveredId == id) + g.HoveredIdAllowOverlap = true; + if (g.ActiveId == id) // Before we made this obsolete, most calls to SetItemAllowOverlap() used to avoid this path by testing g.ActiveId != id. + g.ActiveIdAllowOverlap = true; +} +#endif + +// This is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations. +// FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version if needed? +void ImGui::SetActiveIdUsingAllKeyboardKeys() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId != 0); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_COUNT) - 1; + g.ActiveIdUsingAllKeyboardKeys = true; + NavMoveRequestCancel(); +} + +ImGuiID ImGui::GetItemID() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.ID; +} + +ImVec2 ImGui::GetItemRectMin() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Min; +} + +ImVec2 ImGui::GetItemRectMax() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Max; +} + +ImVec2 ImGui::GetItemRectSize() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.GetSize(); +} + +// Prior to v1.90 2023/10/16, the BeginChild() function took a 'bool border = false' parameter instead of 'ImGuiChildFlags child_flags = 0'. +// ImGuiChildFlags_Borders is defined as always == 1 in order to allow old code passing 'true'. Read comments in imgui.h for details! +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + ImGuiID id = GetCurrentWindow()->GetID(str_id); + return BeginChildEx(str_id, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + return BeginChildEx(NULL, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + IM_ASSERT(id != 0); + + // Sanity check as it is likely that some user will accidentally pass ImGuiWindowFlags into the ImGuiChildFlags argument. + const ImGuiChildFlags ImGuiChildFlags_SupportedMask_ = ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_FrameStyle | ImGuiChildFlags_NavFlattened; + IM_UNUSED(ImGuiChildFlags_SupportedMask_); + IM_ASSERT((child_flags & ~ImGuiChildFlags_SupportedMask_) == 0 && "Illegal ImGuiChildFlags value. Did you pass ImGuiWindowFlags values instead of ImGuiChildFlags?"); + IM_ASSERT((window_flags & ImGuiWindowFlags_AlwaysAutoResize) == 0 && "Cannot specify ImGuiWindowFlags_AlwaysAutoResize for BeginChild(). Use ImGuiChildFlags_AlwaysAutoResize!"); + if (child_flags & ImGuiChildFlags_AlwaysAutoResize) + { + IM_ASSERT((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0 && "Cannot use ImGuiChildFlags_ResizeX or ImGuiChildFlags_ResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + IM_ASSERT((child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY)) != 0 && "Must use ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window_flags & ImGuiWindowFlags_AlwaysUseWindowPadding) + child_flags |= ImGuiChildFlags_AlwaysUseWindowPadding; + if (window_flags & ImGuiWindowFlags_NavFlattened) + child_flags |= ImGuiChildFlags_NavFlattened; +#endif + if (child_flags & ImGuiChildFlags_AutoResizeX) + child_flags &= ~ImGuiChildFlags_ResizeX; + if (child_flags & ImGuiChildFlags_AutoResizeY) + child_flags &= ~ImGuiChildFlags_ResizeY; + + // Set window flags + window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar; + window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize)) + window_flags |= ImGuiWindowFlags_AlwaysAutoResize; + if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0) + window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + // Special framed style + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PushStyleColor(ImGuiCol_ChildBg, g.Style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, g.Style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, g.Style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.FramePadding); + child_flags |= ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding; + window_flags |= ImGuiWindowFlags_NoMove; + } + + // Forward size + // Important: Begin() has special processing to switch condition to ImGuiCond_FirstUseEver for a given axis when ImGuiChildFlags_ResizeXXX is set. + // (the alternative would to store conditional flags per axis, which is possible but more code) + const ImVec2 size_avail = GetContentRegionAvail(); + const ImVec2 size_default((child_flags & ImGuiChildFlags_AutoResizeX) ? 0.0f : size_avail.x, (child_flags & ImGuiChildFlags_AutoResizeY) ? 0.0f : size_avail.y); + ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); + + // A SetNextWindowSize() call always has priority (#8020) + // (since the code in Begin() never supported SizeVal==0.0f aka auto-resize via SetNextWindowSize() call, we don't support it here for now) + // FIXME: We only support ImGuiCond_Always in this path. Supporting other paths would requires to obtain window pointer. + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) != 0 && (g.NextWindowData.SizeCond & ImGuiCond_Always) != 0) + { + if (g.NextWindowData.SizeVal.x > 0.0f) + { + size.x = g.NextWindowData.SizeVal.x; + child_flags &= ~ImGuiChildFlags_ResizeX; + } + if (g.NextWindowData.SizeVal.y > 0.0f) + { + size.y = g.NextWindowData.SizeVal.y; + child_flags &= ~ImGuiChildFlags_ResizeY; + } + } + SetNextWindowSize(size); + + // Forward child flags (we allow prior settings to merge but it'll only work for adding flags) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) + g.NextWindowData.ChildFlags |= child_flags; + else + g.NextWindowData.ChildFlags = child_flags; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasChildFlags; + + // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. + // FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround. + // e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it. + const char* temp_window_name; + /*if (name && parent_window->IDStack.back() == parent_window->ID) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s", parent_window->Name, name); // May omit ID if in root of ID stack + else*/ + if (name) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s_%08X", parent_window->Name, name, id); + else + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%08X", parent_window->Name, id); + + // Set style + const float backup_border_size = g.Style.ChildBorderSize; + if ((child_flags & ImGuiChildFlags_Borders) == 0) + g.Style.ChildBorderSize = 0.0f; + + // Begin into window + const bool ret = Begin(temp_window_name, NULL, window_flags); + + // Restore style + g.Style.ChildBorderSize = backup_border_size; + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PopStyleVar(3); + PopStyleColor(); + } + + ImGuiWindow* child_window = g.CurrentWindow; + child_window->ChildId = id; + + // Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually. + // While this is not really documented/defined, it seems that the expected thing to do. + if (child_window->BeginCount == 1) + parent_window->DC.CursorPos = child_window->Pos; + + // Process navigation-in immediately so NavInit can run on first frame + // Can enter a child if (A) it has navigable items or (B) it can be scrolled. + const ImGuiID temp_id_for_activation = ImHashStr("##Child", 0, id); + if (g.ActiveId == temp_id_for_activation) + ClearActiveID(); + if (g.NavActivateId == id && !(child_flags & ImGuiChildFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY)) + { + FocusWindow(child_window); + NavInitWindow(child_window, false); + SetActiveID(temp_id_for_activation, child_window); // Steal ActiveId with another arbitrary id so that key-press won't activate child item + g.ActiveIdSource = g.NavInputSource; + } + return ret; +} + +void ImGui::EndChild() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* child_window = g.CurrentWindow; + + const ImGuiID backup_within_end_child_id = g.WithinEndChildID; + IM_ASSERT(child_window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls + + g.WithinEndChildID = child_window->ID; + ImVec2 child_size = child_window->Size; + End(); + if (child_window->BeginCount == 1) + { + ImGuiWindow* parent_window = g.CurrentWindow; + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + child_size); + ItemSize(child_size); + const bool nav_flattened = (child_window->ChildFlags & ImGuiChildFlags_NavFlattened) != 0; + if ((child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY) && !nav_flattened) + { + ItemAdd(bb, child_window->ChildId); + RenderNavCursor(bb, child_window->ChildId); + + // When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying) + if (child_window->DC.NavLayersActiveMask == 0 && child_window == g.NavWindow) + RenderNavCursor(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavRenderCursorFlags_Compact); + } + else + { + // Not navigable into + // - This is a bit of a fringe use case, mostly useful for undecorated, non-scrolling contents childs, or empty childs. + // - We could later decide to not apply this path if ImGuiChildFlags_FrameStyle or ImGuiChildFlags_Borders is set. + ItemAdd(bb, child_window->ChildId, NULL, ImGuiItemFlags_NoNav); + + // But when flattened we directly reach items, adjust active layer mask accordingly + if (nav_flattened) + parent_window->DC.NavLayersActiveMaskNext |= child_window->DC.NavLayersActiveMaskNext; + } + if (g.HoveredWindow == child_window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + child_window->DC.ChildItemStatusFlags = g.LastItemData.StatusFlags; + //SetLastItemDataForChildWindowItem(child_window, child_window->Rect()); // Not needed, effectively done by ItemAdd() + } + else + { + SetLastItemDataForChildWindowItem(child_window, child_window->Rect()); + } + + g.WithinEndChildID = backup_within_end_child_id; + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) +{ + window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); + window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); + window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); +} + +ImGuiWindow* ImGui::FindWindowByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); +} + +ImGuiWindow* ImGui::FindWindowByName(const char* name) +{ + ImGuiID id = ImHashStr(name); + return FindWindowByID(id); +} + +static void ApplyWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) +{ + window->Pos = ImTrunc(ImVec2(settings->Pos.x, settings->Pos.y)); + if (settings->Size.x > 0 && settings->Size.y > 0) + window->Size = window->SizeFull = ImTrunc(ImVec2(settings->Size.x, settings->Size.y)); + window->Collapsed = settings->Collapsed; +} + +static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) +{ + // Initial window state with e.g. default/arbitrary window position + // Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->Pos = main_viewport->Pos + ImVec2(60, 60); + window->Size = window->SizeFull = ImVec2(0, 0); + window->SetWindowPosAllowFlags = window->SetWindowSizeAllowFlags = window->SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; + + if (settings != NULL) + { + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + ApplyWindowSettings(window, settings); + } + window->DC.CursorStartPos = window->DC.CursorMaxPos = window->DC.IdealMaxPos = window->Pos; // So first call to CalcWindowContentSizes() doesn't return crazy values + + if ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + { + window->AutoFitFramesX = window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } + else + { + if (window->Size.x <= 0.0f) + window->AutoFitFramesX = 2; + if (window->Size.y <= 0.0f) + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); + } +} + +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags) +{ + // Create window the first time + //IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + ImGuiWindowSettings* settings = NULL; + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + if ((settings = ImGui::FindWindowSettingsByWindow(window)) != 0) + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + + InitOrLoadWindowSettings(window, settings); + + if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) + g.Windows.push_front(window); // Quite slow but rare and only once + else + g.Windows.push_back(window); + + return window; +} + +static inline ImVec2 CalcWindowMinSize(ImGuiWindow* window) +{ + // We give windows non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + // FIXME: Essentially we want to restrict manual resizing to WindowMinSize+Decoration, and allow api resizing to be smaller. + // Perhaps should tend further a neater test for this. + ImGuiContext& g = *GImGui; + ImVec2 size_min; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) + { + size_min.x = (window->ChildFlags & ImGuiChildFlags_ResizeX) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = (window->ChildFlags & ImGuiChildFlags_ResizeY) ? g.Style.WindowMinSize.y : 4.0f; + } + else + { + size_min.x = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.y : 4.0f; + } + + // Reduce artifacts with very small windows + ImGuiWindow* window_for_height = window; + size_min.y = ImMax(size_min.y, window_for_height->TitleBarHeight + window_for_height->MenuBarHeight + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); + return size_min; +} + +static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& size_desired) +{ + ImGuiContext& g = *GImGui; + ImVec2 new_size = size_desired; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + // See comments in SetNextWindowSizeConstraints() for details about setting size_min an size_max. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + new_size.x = IM_TRUNC(new_size.x); + new_size.y = IM_TRUNC(new_size.y); + } + + // Minimum size + ImVec2 size_min = CalcWindowMinSize(window); + return ImMax(new_size, size_min); +} + +static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_current, ImVec2* content_size_ideal) +{ + bool preserve_old_content_sizes = false; + if (window->Collapsed && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + preserve_old_content_sizes = true; + else if (window->Hidden && window->HiddenFramesCannotSkipItems == 0 && window->HiddenFramesCanSkipItems > 0) + preserve_old_content_sizes = true; + if (preserve_old_content_sizes) + { + *content_size_current = window->ContentSize; + *content_size_ideal = window->ContentSizeIdeal; + return; + } + + content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); + content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); + content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); + content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); +} + +static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + const float decoration_w_without_scrollbars = window->DecoOuterSizeX1 + window->DecoOuterSizeX2 - window->ScrollbarSizes.x; + const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y; + ImVec2 size_pad = window->WindowPadding * 2.0f; + ImVec2 size_desired = size_contents + size_pad + ImVec2(decoration_w_without_scrollbars, decoration_h_without_scrollbars); + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Tooltip always resize + return size_desired; + } + else + { + // Maximum window size is determined by the viewport size or monitor size + ImVec2 size_min = CalcWindowMinSize(window); + ImVec2 size_max = ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) ? ImVec2(FLT_MAX, FLT_MAX) : ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f; + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, size_max); + + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one axis may be auto-fit when calculating scrollbars, + // we may need to compute/store three variants of size_auto_fit, for x/y/xy. + // Here we implement a workaround for child windows only, but a full solution would apply to normal windows as well: + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && !(window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.y = window->SizeFull.y; + else if (!(window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.x = window->SizeFull.x; + + // When the window cannot fit all contents (either because of constraints, either because screen is too small), + // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. + ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); + bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - decoration_w_without_scrollbars < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); + bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_h_without_scrollbars < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); + if (will_have_scrollbar_x) + size_auto_fit.y += style.ScrollbarSize; + if (will_have_scrollbar_y) + size_auto_fit.x += style.ScrollbarSize; + return size_auto_fit; + } +} + +ImVec2 ImGui::CalcWindowNextAutoFitSize(ImGuiWindow* window) +{ + ImVec2 size_contents_current; + ImVec2 size_contents_ideal; + CalcWindowContentSizes(window, &size_contents_current, &size_contents_ideal); + ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, size_contents_ideal); + ImVec2 size_final = CalcWindowSizeAfterConstraint(window, size_auto_fit); + return size_final; +} + +static ImGuiCol GetWindowBgColorIdx(ImGuiWindow* window) +{ + if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + return ImGuiCol_PopupBg; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + return ImGuiCol_ChildBg; + return ImGuiCol_WindowBg; +} + +static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) +{ + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = pos_max - pos_min; + ImVec2 size_constrained = CalcWindowSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; +} + +// Data for resizing from resize grip / corner +struct ImGuiResizeGripDef +{ + ImVec2 CornerPosN; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; +}; +static const ImGuiResizeGripDef resize_grip_def[4] = +{ + { ImVec2(1, 1), ImVec2(-1, -1), 0, 3 }, // Lower-right + { ImVec2(0, 1), ImVec2(+1, -1), 3, 6 }, // Lower-left + { ImVec2(0, 0), ImVec2(+1, +1), 6, 9 }, // Upper-left (Unused) + { ImVec2(1, 0), ImVec2(-1, +1), 9, 12 } // Upper-right (Unused) +}; + +// Data for resizing from borders +struct ImGuiResizeBorderDef +{ + ImVec2 InnerDir; // Normal toward inside + ImVec2 SegmentN1, SegmentN2; // End positions, normalized (0,0: upper left) + float OuterAngle; // Angle toward outside +}; +static const ImGuiResizeBorderDef resize_border_def[4] = +{ + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down +}; + +static ImRect GetResizeBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) +{ + ImRect rect = window->Rect(); + if (thickness == 0.0f) + rect.Max -= ImVec2(1, 1); + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } + IM_ASSERT(0); + return ImRect(); +} + +// 0..3: corners (Lower-right, Lower-left, Unused, Unused) +ImGuiID ImGui::GetWindowResizeCornerID(ImGuiWindow* window, int n) +{ + IM_ASSERT(n >= 0 && n < 4); + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + +// Borders (Left, Right, Up, Down) +ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) +{ + IM_ASSERT(dir >= 0 && dir < 4); + int n = (int)dir + 4; + ImGuiID id = window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + +// Handle resize for: Resize Grips, Borders, Gamepad +// Return true when using auto-fit (double-click on resize grip) +static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) +{ + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; + + if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + return false; + if (window->WasActive == false) // Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window. + return false; + + int ret_auto_fit_mask = 0x00; + const float grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = (resize_grip_count > 0) ? IM_TRUNC(grip_draw_size * 0.75f) : 0.0f; + const float grip_hover_outer_size = g.WindowsBorderHoverPadding; + + ImRect clamp_rect = visibility_rect; + const bool window_move_from_title_bar = g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar); + if (window_move_from_title_bar) + clamp_rect.Min.y -= window->TitleBarHeight; + + ImVec2 pos_target(FLT_MAX, FLT_MAX); + ImVec2 size_target(FLT_MAX, FLT_MAX); + + // Resize grips and borders are on layer 1 + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Manual resize grips + PushID("#RESIZE"); + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& def = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, def.CornerPosN); + + // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window + bool hovered, held; + ImRect resize_rect(corner - def.InnerDir * grip_hover_outer_size, corner + def.InnerDir * grip_hover_inner_size); + if (resize_rect.Min.x > resize_rect.Max.x) ImSwap(resize_rect.Min.x, resize_rect.Max.x); + if (resize_rect.Min.y > resize_rect.Max.y) ImSwap(resize_rect.Min.y, resize_rect.Max.y); + ImGuiID resize_grip_id = window->GetID(resize_grip_n); // == GetWindowResizeCornerID() + ItemAdd(resize_rect, resize_grip_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(resize_rect, resize_grip_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered || held) + SetMouseCursor((resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE); + + if (held && g.IO.MouseDoubleClicked[0]) + { + // Auto-fit when double-clicking + size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); + ret_auto_fit_mask = 0x03; // Both axes + ClearActiveID(); + } + else if (held) + { + // Resize from any of the four corners + // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position + ImVec2 clamp_min = ImVec2(def.CornerPosN.x == 1.0f ? clamp_rect.Min.x : -FLT_MAX, (def.CornerPosN.y == 1.0f || (def.CornerPosN.y == 0.0f && window_move_from_title_bar)) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? clamp_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? clamp_rect.Max.y : +FLT_MAX); + ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip + corner_target = ImClamp(corner_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); + } + + // Only lower-left grip is visible before hovering/activating + if (resize_grip_n == 0 || held || hovered) + resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); + } + + int resize_border_mask = 0x00; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + resize_border_mask |= ((window->ChildFlags & ImGuiChildFlags_ResizeX) ? 0x02 : 0) | ((window->ChildFlags & ImGuiChildFlags_ResizeY) ? 0x08 : 0); + else + resize_border_mask = g.IO.ConfigWindowsResizeFromEdges ? 0x0F : 0x00; + for (int border_n = 0; border_n < 4; border_n++) + { + if ((resize_border_mask & (1 << border_n)) == 0) + continue; + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + + bool hovered, held; + ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, g.WindowsBorderHoverPadding); + ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() + ItemAdd(border_rect, border_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered && g.HoveredIdTimer <= WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) + hovered = false; + if (hovered || held) + SetMouseCursor((axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS); + if (held && g.IO.MouseDoubleClicked[0]) + { + // Double-clicking bottom or right border auto-fit on this axis + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one side may be auto-fit when calculating scrollbars. + // FIXME: Support top and right borders: rework CalcResizePosSizeFromAnyCorner() to be reusable in both cases. + if (border_n == 1 || border_n == 3) // Right and bottom border + { + size_target[axis] = CalcWindowSizeAfterConstraint(window, size_auto_fit)[axis]; + ret_auto_fit_mask |= (1 << axis); + hovered = held = false; // So border doesn't show highlighted at new position + } + ClearActiveID(); + } + else if (held) + { + // Switch to relative resizing mode when border geometry moved (e.g. resizing a child altering parent scroll), in order to avoid resizing feedback loop. + // Currently only using relative mode on resizable child windows, as the problem to solve is more likely noticeable for them, but could apply for all windows eventually. + // FIXME: May want to generalize this idiom at lower-level, so more widgets can use it! + const bool just_scrolled_manually_while_resizing = (g.WheelingWindow != NULL && g.WheelingWindowScrolledFrame == g.FrameCount && IsWindowChildOf(window, g.WheelingWindow, false)); + if (g.ActiveIdIsJustActivated || just_scrolled_manually_while_resizing) + { + g.WindowResizeBorderExpectedRect = border_rect; + g.WindowResizeRelativeMode = false; + } + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && memcmp(&g.WindowResizeBorderExpectedRect, &border_rect, sizeof(ImRect)) != 0) + g.WindowResizeRelativeMode = true; + + const ImVec2 border_curr = (window->Pos + ImMin(def.SegmentN1, def.SegmentN2) * window->Size); + const float border_target_rel_mode_for_axis = border_curr[axis] + g.IO.MouseDelta[axis]; + const float border_target_abs_mode_for_axis = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + g.WindowsBorderHoverPadding; // Match ButtonBehavior() padding above. + + // Use absolute mode position + ImVec2 border_target = window->Pos; + border_target[axis] = border_target_abs_mode_for_axis; + + // Use relative mode target for child window, ignore resize when moving back toward the ideal absolute position. + bool ignore_resize = false; + if (g.WindowResizeRelativeMode) + { + //GetForegroundDrawList()->AddText(GetMainViewport()->WorkPos, IM_COL32_WHITE, "Relative Mode"); + border_target[axis] = border_target_rel_mode_for_axis; + if (g.IO.MouseDelta[axis] == 0.0f || (g.IO.MouseDelta[axis] > 0.0f) == (border_target_rel_mode_for_axis > border_target_abs_mode_for_axis)) + ignore_resize = true; + } + + // Clamp, apply + ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); + border_target = ImClamp(border_target, clamp_min, clamp_max); + if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent + { + ImGuiWindow* parent_window = window->ParentWindow; + ImGuiWindowFlags parent_flags = parent_window->Flags; + ImRect border_limit_rect = parent_window->InnerRect; + border_limit_rect.Expand(ImVec2(-ImMax(parent_window->WindowPadding.x, parent_window->WindowBorderSize), -ImMax(parent_window->WindowPadding.y, parent_window->WindowBorderSize))); + if ((axis == ImGuiAxis_X) && ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar))) + border_target.x = ImClamp(border_target.x, border_limit_rect.Min.x, border_limit_rect.Max.x); + if ((axis == ImGuiAxis_Y) && (parent_flags & ImGuiWindowFlags_NoScrollbar)) + border_target.y = ImClamp(border_target.y, border_limit_rect.Min.y, border_limit_rect.Max.y); + } + if (!ignore_resize) + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); + } + if (hovered) + *border_hovered = border_n; + if (held) + *border_held = border_n; + } + PopID(); + + // Restore nav layer + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + + // Navigation resize (keyboard/gamepad) + // FIXME: This cannot be moved to NavUpdateWindowing() because CalcWindowSizeAfterConstraint() need to callback into user. + // Not even sure the callback works here. + if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindow == window) + { + ImVec2 nav_resize_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && g.IO.KeyShift) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown); + if (nav_resize_dir.x != 0.0f || nav_resize_dir.y != 0.0f) + { + const float NAV_RESIZE_SPEED = 600.0f; + const float resize_step = NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaSize += nav_resize_dir * resize_step; + g.NavWindowingAccumDeltaSize = ImMax(g.NavWindowingAccumDeltaSize, clamp_rect.Min - window->Pos - window->Size); // We need Pos+Size >= clmap_rect.Min, so Size >= clmap_rect.Min - Pos, so size_delta >= clmap_rect.Min - window->Pos - window->Size + g.NavWindowingToggleLayer = false; + g.NavHighlightItemUnderNav = true; + resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaSize); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. + size_target = CalcWindowSizeAfterConstraint(window, window->SizeFull + accum_floored); + g.NavWindowingAccumDeltaSize -= accum_floored; + } + } + } + + // Apply back modified position/size to window + const ImVec2 curr_pos = window->Pos; + const ImVec2 curr_size = window->SizeFull; + if (size_target.x != FLT_MAX && (window->Size.x != size_target.x || window->SizeFull.x != size_target.x)) + window->Size.x = window->SizeFull.x = size_target.x; + if (size_target.y != FLT_MAX && (window->Size.y != size_target.y || window->SizeFull.y != size_target.y)) + window->Size.y = window->SizeFull.y = size_target.y; + if (pos_target.x != FLT_MAX && window->Pos.x != ImTrunc(pos_target.x)) + window->Pos.x = ImTrunc(pos_target.x); + if (pos_target.y != FLT_MAX && window->Pos.y != ImTrunc(pos_target.y)) + window->Pos.y = ImTrunc(pos_target.y); + if (curr_pos.x != window->Pos.x || curr_pos.y != window->Pos.y || curr_size.x != window->SizeFull.x || curr_size.y != window->SizeFull.y) + MarkIniSettingsDirty(window); + + // Recalculate next expected border expected coordinates + if (*border_held != -1) + g.WindowResizeBorderExpectedRect = GetResizeBorderRect(window, *border_held, grip_hover_inner_size, g.WindowsBorderHoverPadding); + + return ret_auto_fit_mask; +} + +static inline void ClampWindowPos(ImGuiWindow* window, const ImRect& visibility_rect) +{ + ImGuiContext& g = *GImGui; + ImVec2 size_for_clamping = window->Size; + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + size_for_clamping.y = window->TitleBarHeight; + window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); +} + +static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU32 border_col, float border_size) +{ + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const float rounding = window->WindowRounding; + const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(border_col, ImDrawFlags_None, border_size); +} + +static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + const float border_size = window->WindowBorderSize; + const ImU32 border_col = GetColorU32(ImGuiCol_Border); + if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0) + window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, 0, window->WindowBorderSize); + else if (border_size > 0.0f) + { + if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border. + RenderWindowOuterSingleBorder(window, 1, border_col, border_size); + if (window->ChildFlags & ImGuiChildFlags_ResizeY) + RenderWindowOuterSingleBorder(window, 3, border_col, border_size); + } + if (window->ResizeBorderHovered != -1 || window->ResizeBorderHeld != -1) + { + const int border_n = (window->ResizeBorderHeld != -1) ? window->ResizeBorderHeld : window->ResizeBorderHovered; + const ImU32 border_col_resizing = GetColorU32((window->ResizeBorderHeld != -1) ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0f, window->WindowBorderSize)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + { + float y = window->Pos.y + window->TitleBarHeight - 1; + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size * 0.5f, y), ImVec2(window->Pos.x + window->Size.x - border_size * 0.5f, y), border_col, g.Style.FrameBorderSize); + } +} + +// Draw background and borders +// Draw and handle scrollbars +void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + // Ensure that Scrollbar() doesn't read last frame's SkipItems + IM_ASSERT(window->BeginCount == 0); + window->SkipItems = false; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Draw window + handle manual resize + // As we highlight the title bar when want_focus is set, multiple reappearing windows will have their title bar highlighted on their reappearing frame. + const float window_rounding = window->WindowRounding; + const float window_border_size = window->WindowBorderSize; + if (window->Collapsed) + { + // Title bar only + const float backup_border_size = style.FrameBorderSize; + g.Style.FrameBorderSize = window->WindowBorderSize; + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && g.NavCursorVisible) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); + g.Style.FrameBorderSize = backup_border_size; + } + else + { + // Window background + if (!(flags & ImGuiWindowFlags_NoBackground)) + { + ImU32 bg_col = GetColorU32(GetWindowBgColorIdx(window)); + bool override_alpha = false; + float alpha = 1.0f; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasBgAlpha) + { + alpha = g.NextWindowData.BgAlphaVal; + override_alpha = true; + } + if (override_alpha) + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); + window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + { + ImU32 title_bar_col = GetColorU32(title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags_RoundCornersTop); + } + + // Menu bar + if (flags & ImGuiWindowFlags_MenuBar) + { + ImRect menu_bar_rect = window->MenuBarRect(); + menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. + window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop); + if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) + window->DrawList->AddLine(menu_bar_rect.GetBL() + ImVec2(window_border_size * 0.5f, 0.0f), menu_bar_rect.GetBR() - ImVec2(window_border_size * 0.5f, 0.0f), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Scrollbars + if (window->ScrollbarX) + Scrollbar(ImGuiAxis_X); + if (window->ScrollbarY) + Scrollbar(ImGuiAxis_Y); + + // Render resize grips (after their input handling so we don't have a frame of latency) + if (handle_borders_and_resize_grips && !(flags & ImGuiWindowFlags_NoResize)) + { + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImU32 col = resize_grip_col[resize_grip_n]; + if ((col & IM_COL32_A_MASK) == 0) + continue; + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPosN); + const float border_inner = IM_ROUND(window_border_size * 0.5f); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(border_inner, resize_grip_draw_size) : ImVec2(resize_grip_draw_size, border_inner))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(resize_grip_draw_size, border_inner) : ImVec2(border_inner, resize_grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + border_inner), corner.y + grip.InnerDir.y * (window_rounding + border_inner)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(col); + } + } + + // Borders + if (handle_borders_and_resize_grips) + RenderWindowOuterBorders(window); + } + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; +} + +// Render title text, collapse button, close button +void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + + const bool has_close_button = (p_open != NULL); + const bool has_collapse_button = !(flags & ImGuiWindowFlags_NoCollapse) && (style.WindowMenuButtonPosition != ImGuiDir_None); + + // Close & Collapse button are on the Menu NavLayer and don't default focus (unless there's nothing else on that layer) + // FIXME-NAV: Might want (or not?) to set the equivalent of ImGuiButtonFlags_NoNavFocus so that mouse clicks on standard title bar items don't necessarily set nav/keyboard ref? + const ImGuiItemFlags item_flags_backup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + + // Layout buttons + // FIXME: Would be nice to generalize the subtleties expressed here into reusable code. + float pad_l = style.FramePadding.x; + float pad_r = style.FramePadding.x; + float button_sz = g.FontSize; + ImVec2 close_button_pos; + ImVec2 collapse_button_pos; + if (has_close_button) + { + close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Right) + { + collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; + } + if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Left) + { + collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y + style.FramePadding.y); + pad_l += button_sz + style.ItemInnerSpacing.x; + } + + // Collapse button (submitting first so it gets priority when choosing a navigation init fallback) + if (has_collapse_button) + if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos)) + window->WantCollapseToggle = true; // Defer actual collapsing to next frame as we are too far in the Begin() function + + // Close button + if (has_close_button) + if (CloseButton(window->GetID("#CLOSE"), close_button_pos)) + *p_open = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + g.CurrentItemFlags = item_flags_backup; + + // Title bar text (with: horizontal alignment, avoiding collapse/close button, optional "unsaved document" marker) + // FIXME: Refactor text alignment facilities along with RenderText helpers, this is WAY too much messy code.. + const float marker_size_x = (flags & ImGuiWindowFlags_UnsavedDocument) ? button_sz * 0.80f : 0.0f; + const ImVec2 text_size = CalcTextSize(name, NULL, true) + ImVec2(marker_size_x, 0.0f); + + // As a nice touch we try to ensure that centered title text doesn't get affected by visibility of Close/Collapse button, + // while uncentered title text will still reach edges correctly. + if (pad_l > style.FramePadding.x) + pad_l += g.Style.ItemInnerSpacing.x; + if (pad_r > style.FramePadding.x) + pad_r += g.Style.ItemInnerSpacing.x; + if (style.WindowTitleAlign.x > 0.0f && style.WindowTitleAlign.x < 1.0f) + { + float centerness = ImSaturate(1.0f - ImFabs(style.WindowTitleAlign.x - 0.5f) * 2.0f); // 0.0f on either edges, 1.0f on center + float pad_extend = ImMin(ImMax(pad_l, pad_r), title_bar_rect.GetWidth() - pad_l - pad_r - text_size.x); + pad_l = ImMax(pad_l, pad_extend * centerness); + pad_r = ImMax(pad_r, pad_extend * centerness); + } + + ImRect layout_r(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y, title_bar_rect.Max.x - pad_r, title_bar_rect.Max.y); + ImRect clip_r(layout_r.Min.x, layout_r.Min.y, ImMin(layout_r.Max.x + g.Style.ItemInnerSpacing.x, title_bar_rect.Max.x), layout_r.Max.y); + if (flags & ImGuiWindowFlags_UnsavedDocument) + { + ImVec2 marker_pos; + marker_pos.x = ImClamp(layout_r.Min.x + (layout_r.GetWidth() - text_size.x) * style.WindowTitleAlign.x + text_size.x, layout_r.Min.x, layout_r.Max.x); + marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5f; + if (marker_pos.x > layout_r.Min.x) + { + RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_Text)); + clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - (int)(marker_size_x * 0.5f)); + } + } + //if (g.IO.KeyShift) window->DrawList->AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + //if (g.IO.KeyCtrl) window->DrawList->AddRect(clip_r.Min, clip_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + RenderTextClipped(layout_r.Min, layout_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_r); +} + +void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window) +{ + window->ParentWindow = parent_window; + window->RootWindow = window->RootWindowPopupTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; + if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) + window->RootWindow = parent_window->RootWindow; + if (parent_window && (flags & ImGuiWindowFlags_Popup)) + window->RootWindowPopupTree = parent_window->RootWindowPopupTree; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight; + while (window->RootWindowForNav->ChildFlags & ImGuiChildFlags_NavFlattened) + { + IM_ASSERT(window->RootWindowForNav->ParentWindow != NULL); + window->RootWindowForNav = window->RootWindowForNav->ParentWindow; + } +} + +// [EXPERIMENTAL] Called by Begin(). NextWindowData is valid at this point. +// This is designed as a toy/test-bed for +void ImGui::UpdateWindowSkipRefresh(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + window->SkipRefresh = false; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasRefreshPolicy) == 0) + return; + if (g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_TryToAvoidRefresh) + { + // FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. + if (window->Appearing) // If currently appearing + return; + if (window->Hidden) // If was hidden (previous frame) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnHover) && g.HoveredWindow) + if (window->RootWindow == g.HoveredWindow->RootWindow || IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, window)) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnFocus) && g.NavWindow) + if (window->RootWindow == g.NavWindow->RootWindow || IsWindowWithinBeginStackOf(g.NavWindow->RootWindow, window)) + return; + window->DrawList = NULL; + window->SkipRefresh = true; + } +} + +static void SetWindowActiveForSkipRefresh(ImGuiWindow* window) +{ + window->Active = true; + for (ImGuiWindow* child : window->DC.ChildWindows) + if (!child->Hidden) + { + child->Active = child->SkipRefresh = true; + SetWindowActiveForSkipRefresh(child); + } +} + +// Push a new Dear ImGui window to add widgets to. +// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. +// - Begin/End can be called multiple times during the frame with the same window name to append content. +// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). +// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. +// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. +// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. +bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + IM_ASSERT(name != NULL && name[0] != '\0'); // Window name required + IM_ASSERT(g.WithinFrameScope); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet + + // Find or create + ImGuiWindow* window = FindWindowByName(name); + const bool window_just_created = (window == NULL); + if (window_just_created) + window = CreateNewWindow(name, flags); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInWindow == window->ID) + IM_DEBUG_BREAK(); + + // Automatically disable manual moving/resizing when NoInputs is set + if ((flags & ImGuiWindowFlags_NoInputs) == ImGuiWindowFlags_NoInputs) + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + + const int current_frame = g.FrameCount; + const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + window->IsFallbackWindow = (g.CurrentWindowStack.Size == 0 && g.WithinFrameScopeWithImplicitWindow); + + // Update the Appearing flag + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed + window_just_activated_by_user |= (window != popup_ref.Window); + } + window->Appearing = window_just_activated_by_user; + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + + // Update Flags, LastFrameActive, BeginOrderXXX fields + if (first_begin_of_the_frame) + { + UpdateWindowInFocusOrderList(window, window_just_created, flags); + window->Flags = (ImGuiWindowFlags)flags; + window->ChildFlags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : 0; + window->LastFrameActive = current_frame; + window->LastTimeActive = (float)g.Time; + window->BeginOrderWithinParent = 0; + window->BeginOrderWithinContext = (short)(g.WindowsActiveCount++); + } + else + { + flags = window->Flags; + } + + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack + ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; + ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; + IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); + + // We allow window memory to be compacted so recreate the base stack when needed. + if (window->IDStack.Size == 0) + window->IDStack.push_back(window->ID); + + // Add to stack + g.CurrentWindow = window; + g.CurrentWindowStack.resize(g.CurrentWindowStack.Size + 1); + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); + window_stack_data.Window = window; + window_stack_data.ParentLastItemDataBackup = g.LastItemData; + window_stack_data.DisabledOverrideReenable = (flags & ImGuiWindowFlags_Tooltip) && (g.CurrentItemFlags & ImGuiItemFlags_Disabled); + window_stack_data.DisabledOverrideReenableAlphaBackup = 0.0f; + ErrorRecoveryStoreState(&window_stack_data.StackSizesInBegin); + g.StackSizesInBeginForCurrentWindow = &window_stack_data.StackSizesInBegin; + if (flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth++; + + // Update ->RootWindow and others pointers (before any possible call to FocusWindow) + if (first_begin_of_the_frame) + { + UpdateWindowParentAndRootLinks(window, flags, parent_window); + window->ParentWindowInBeginStack = parent_window_in_stack; + + // There's little point to expose a flag to set this: because the interesting cases won't be using parent_window_in_stack, + // e.g. linking a tool window in a standalone viewport to a document window, regardless of their Begin() stack parenting. (#6798) + window->ParentWindowForFocusRoute = (flags & ImGuiWindowFlags_ChildWindow) ? parent_window_in_stack : NULL; + + // Inherent SetWindowFontScale() from parent until we fix this system... + window->FontWindowScaleParents = parent_window ? parent_window->FontWindowScaleParents * parent_window->FontWindowScale : 1.0f; + } + + // Add to focus scope stack + PushFocusScope((window->ChildFlags & ImGuiChildFlags_NavFlattened) ? g.CurrentFocusScopeId : window->ID); + window->NavRootFocusScopeId = g.CurrentFocusScopeId; + + // Add to popup stacks: update OpenPopupStack[] data, push to BeginPopupStack[] + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; + popup_ref.Window = window; + popup_ref.ParentNavLayer = parent_window_in_stack->DC.NavLayerCurrent; + g.BeginPopupStack.push_back(popup_ref); + window->PopupId = popup_ref.PopupId; + } + + // Process SetNextWindow***() calls + // (FIXME: Consider splitting the HasXXX flags into X/Y components + bool window_pos_set_by_api = false; + bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) + { + window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; + if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) + { + // May be processed on the next frame if this is our first frame and we are measuring size + // FIXME: Look into removing the branch so everything can go through this same code path for consistency. + window->SetWindowPosVal = g.NextWindowData.PosVal; + window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + } + else + { + SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); + } + } + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) + { + window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); + window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) // Axis-specific conditions for BeginChild() + g.NextWindowData.SizeVal.x = window->SizeFull.x; + if ((window->ChildFlags & ImGuiChildFlags_ResizeY) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) + g.NextWindowData.SizeVal.y = window->SizeFull.y; + SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); + } + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasScroll) + { + if (g.NextWindowData.ScrollVal.x >= 0.0f) + { + window->ScrollTarget.x = g.NextWindowData.ScrollVal.x; + window->ScrollTargetCenterRatio.x = 0.0f; + } + if (g.NextWindowData.ScrollVal.y >= 0.0f) + { + window->ScrollTarget.y = g.NextWindowData.ScrollVal.y; + window->ScrollTargetCenterRatio.y = 0.0f; + } + } + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasContentSize) + window->ContentSizeExplicit = g.NextWindowData.ContentSizeVal; + else if (first_begin_of_the_frame) + window->ContentSizeExplicit = ImVec2(0.0f, 0.0f); + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasCollapsed) + SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasFocus) + FocusWindow(window); + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + + // [EXPERIMENTAL] Skip Refresh mode + UpdateWindowSkipRefresh(window); + + // Nested root windows (typically tooltips) override disabled state + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + BeginDisabledOverrideReenable(); + + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() + g.CurrentWindow = NULL; + + // When reusing window again multiple times a frame, just append content (don't need to setup again) + if (first_begin_of_the_frame && !window->SkipRefresh) + { + // Initialize + const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFramesCannotSkipItems > 0); + window->Active = true; + window->HasCloseButton = (p_open != NULL); + window->ClipRect = ImVec4(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); + window->IDStack.resize(1); + window->DrawList->_ResetForNewFrame(); + window->DC.CurrentTableIdx = -1; + + // Restore buffer capacity when woken from a compacted state, to avoid + if (window->MemoryCompacted) + GcAwakeTransientWindowBuffers(window); + + // Update stored window name when it changes (which can _only_ happen with the "###" operator, so the ID would stay unchanged). + // The title bar always display the 'name' parameter, so we only update the string storage if it needs to be visible to the end-user elsewhere. + bool window_title_visible_elsewhere = false; + if (g.NavWindowingListWindow != NULL && (flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + window_title_visible_elsewhere = true; + if (flags & ImGuiWindowFlags_ChildMenu) + window_title_visible_elsewhere = true; + if (window_title_visible_elsewhere && !window_just_created && strcmp(name, window->Name) != 0) + { + size_t buf_len = (size_t)window->NameBufLen; + window->Name = ImStrdupcpy(window->Name, &buf_len, name); + window->NameBufLen = (int)buf_len; + } + + // UPDATE CONTENTS SIZE, UPDATE HIDDEN STATUS + + // Update contents size from last frame for auto-fitting (or use explicit size) + CalcWindowContentSizes(window, &window->ContentSize, &window->ContentSizeIdeal); + if (window->HiddenFramesCanSkipItems > 0) + window->HiddenFramesCanSkipItems--; + if (window->HiddenFramesCannotSkipItems > 0) + window->HiddenFramesCannotSkipItems--; + if (window->HiddenFramesForRenderOnly > 0) + window->HiddenFramesForRenderOnly--; + + // Hide new windows for one frame until they calculate their size + if (window_just_created && (!window_size_x_set_by_api || !window_size_y_set_by_api)) + window->HiddenFramesCannotSkipItems = 1; + + // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) + // We reset Size/ContentSize for reappearing popups/tooltips early in this function, so further code won't be tempted to use the old size. + if (window_just_activated_by_user && (flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0) + { + window->HiddenFramesCannotSkipItems = 1; + if (flags & ImGuiWindowFlags_AlwaysAutoResize) + { + if (!window_size_x_set_by_api) + window->Size.x = window->SizeFull.x = 0.f; + if (!window_size_y_set_by_api) + window->Size.y = window->SizeFull.y = 0.f; + window->ContentSize = window->ContentSizeIdeal = ImVec2(0.f, 0.f); + } + } + + // SELECT VIEWPORT + // FIXME-VIEWPORT: In the docking/viewport branch, this is the point where we select the current viewport (which may affect the style) + + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + SetWindowViewport(window, viewport); + SetCurrentWindow(window); + + // LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies) + + if (flags & ImGuiWindowFlags_ChildWindow) + window->WindowBorderSize = style.ChildBorderSize; + else + window->WindowBorderSize = ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; + window->WindowPadding = style.WindowPadding; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !(window->ChildFlags & ImGuiChildFlags_AlwaysUseWindowPadding) && window->WindowBorderSize == 0.0f) + window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. + window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); + window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; + window->TitleBarHeight = (flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : g.FontSize + g.Style.FramePadding.y * 2.0f; + window->MenuBarHeight = (flags & ImGuiWindowFlags_MenuBar) ? window->DC.MenuBarOffset.y + g.FontSize + g.Style.FramePadding.y * 2.0f : 0.0f; + window->FontRefSize = g.FontSize; // Lock this to discourage calling window->CalcFontSize() outside of current window. + + // Depending on condition we use previous or current window size to compare against contents size to decide if a scrollbar should be visible. + // Those flags will be altered further down in the function depending on more conditions. + bool use_current_size_for_scrollbar_x = window_just_created; + bool use_current_size_for_scrollbar_y = window_just_created; + if (window_size_x_set_by_api && window->ContentSizeExplicit.x != 0.0f) + use_current_size_for_scrollbar_x = true; + if (window_size_y_set_by_api && window->ContentSizeExplicit.y != 0.0f) // #7252 + use_current_size_for_scrollbar_y = true; + + // Collapse window by double-clicking on title bar + // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + { + // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), + // so verify that we don't have items over the title bar. + ImRect title_bar_rect = window->TitleBarRect(); + if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && g.ActiveId == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max)) + if (g.IO.MouseClickedCount[0] == 2 && GetKeyOwner(ImGuiKey_MouseLeft) == ImGuiKeyOwner_NoOwner) + window->WantCollapseToggle = true; + if (window->WantCollapseToggle) + { + window->Collapsed = !window->Collapsed; + if (!window->Collapsed) + use_current_size_for_scrollbar_y = true; + MarkIniSettingsDirty(window); + } + } + else + { + window->Collapsed = false; + } + window->WantCollapseToggle = false; + + // SIZE + + // Outer Decoration Sizes + // (we need to clear ScrollbarSize immediately as CalcWindowAutoFitSize() needs it and can be called from other locations). + const ImVec2 scrollbar_sizes_from_last_frame = window->ScrollbarSizes; + window->DecoOuterSizeX1 = 0.0f; + window->DecoOuterSizeX2 = 0.0f; + window->DecoOuterSizeY1 = window->TitleBarHeight + window->MenuBarHeight; + window->DecoOuterSizeY2 = 0.0f; + window->ScrollbarSizes = ImVec2(0.0f, 0.0f); + + // Calculate auto-fit size, handle automatic resize + const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSizeIdeal); + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) && !window->Collapsed) + { + // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. + if (!window_size_x_set_by_api) + { + window->SizeFull.x = size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api) + { + window->SizeFull.y = size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + } + else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + { + // Auto-fit may only grow window during the first few frames + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. + if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) + { + window->SizeFull.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + use_current_size_for_scrollbar_x = true; + } + if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) + { + window->SizeFull.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + use_current_size_for_scrollbar_y = true; + } + if (!window->Collapsed) + MarkIniSettingsDirty(window); + } + + // Apply minimum/maximum window size constraints and final size + window->SizeFull = CalcWindowSizeAfterConstraint(window, window->SizeFull); + window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; + + // POSITION + + // Popup latch its initial position, will position itself when it appears next frame + if (window_just_activated_by_user) + { + window->AutoPosLastDirection = ImGuiDir_None; + if ((flags & ImGuiWindowFlags_Popup) != 0 && !(flags & ImGuiWindowFlags_Modal) && !window_pos_set_by_api) // FIXME: BeginPopup() could use SetNextWindowPos() + window->Pos = g.BeginPopupStack.back().OpenPopupPos; + } + + // Position child window + if (flags & ImGuiWindowFlags_ChildWindow) + { + IM_ASSERT(parent_window && parent_window->Active); + window->BeginOrderWithinParent = (short)parent_window->DC.ChildWindows.Size; + parent_window->DC.ChildWindows.push_back(window); + if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = parent_window->DC.CursorPos; + } + + const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFramesCannotSkipItems == 0); + if (window_pos_with_pivot) + SetWindowPos(window, window->SetWindowPosVal - window->Size * window->SetWindowPosPivot, 0); // Position given a pivot (e.g. for centering) + else if ((flags & ImGuiWindowFlags_ChildMenu) != 0) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) + window->Pos = FindBestWindowPosForPopup(window); + else if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = FindBestWindowPosForPopup(window); + + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) + // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. + ImRect viewport_rect(viewport->GetMainRect()); + ImRect viewport_work_rect(viewport->GetWorkRect()); + ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + ImRect visibility_rect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding); + + // Clamp position/size so window stays visible within its viewport or monitor + // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow)) + if (viewport_rect.GetWidth() > 0.0f && viewport_rect.GetHeight() > 0.0f) + ClampWindowPos(window, visibility_rect); + window->Pos = ImTrunc(window->Pos); + + // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) + // Large values tend to lead to variety of artifacts and are not recommended. + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + + // For windows with title bar or menu bar, we clamp to FrameHeight(FontSize + FramePadding.y * 2.0f) to completely hide artifacts. + //if ((window->Flags & ImGuiWindowFlags_MenuBar) || !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + // window->WindowRounding = ImMin(window->WindowRounding, g.FontSize + style.FramePadding.y * 2.0f); + + // Apply window focus (new and reactivated windows are moved to front) + bool want_focus = false; + if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) + { + if (flags & ImGuiWindowFlags_Popup) + want_focus = true; + else if ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) == 0) + want_focus = true; + } + + // [Test Engine] Register whole window in the item system (before submitting further decorations) +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (g.TestEngineHookItems) + { + IM_ASSERT(window->IDStack.Size == 1); + window->IDStack.Size = 0; // As window->IDStack[0] == window->ID here, make sure TestEngine doesn't erroneously see window as parent of itself. + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + IMGUI_TEST_ENGINE_ITEM_ADD(window->ID, window->Rect(), NULL); + IMGUI_TEST_ENGINE_ITEM_INFO(window->ID, window->Name, (g.HoveredWindow == window) ? ImGuiItemStatusFlags_HoveredRect : 0); + window->IDStack.Size = 1; + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + + } +#endif + + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_hovered = -1, border_held = -1; + ImU32 resize_grip_col[4] = {}; + const int resize_grip_count = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) ? 0 : g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + if (!window->Collapsed) + if (int auto_fit_mask = UpdateWindowManualResize(window, size_auto_fit, &border_hovered, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) + { + if (auto_fit_mask & (1 << ImGuiAxis_X)) + use_current_size_for_scrollbar_x = true; + if (auto_fit_mask & (1 << ImGuiAxis_Y)) + use_current_size_for_scrollbar_y = true; + } + window->ResizeBorderHovered = (signed char)border_hovered; + window->ResizeBorderHeld = (signed char)border_held; + + // SCROLLBAR VISIBILITY + + // Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size). + if (!window->Collapsed) + { + // When reading the current size we need to read it after size constraints have been applied. + // Intentionally use previous frame values for InnerRect and ScrollbarSizes. + // And when we use window->DecorationUp here it doesn't have ScrollbarSizes.y applied yet. + ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2)); + ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + scrollbar_sizes_from_last_frame; + ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f; + float size_x_for_scrollbars = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x; + float size_y_for_scrollbars = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y; + bool scrollbar_x_prev = window->ScrollbarX; + //bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons? + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + + // Track when ScrollbarX visibility keeps toggling, which is a sign of a feedback loop, and stabilize by enforcing visibility (#3285, #8488) + // (Feedback loops of this sort can manifest in various situations, but combining horizontal + vertical scrollbar + using a clipper with varying width items is one frequent cause. + // The better solution is to, either (1) enforce visibility by using ImGuiWindowFlags_AlwaysHorizontalScrollbar or (2) declare stable contents width with SetNextWindowContentSize(), if you can compute it) + window->ScrollbarXStabilizeToggledHistory <<= 1; + window->ScrollbarXStabilizeToggledHistory |= (scrollbar_x_prev != window->ScrollbarX) ? 0x01 : 0x00; + const bool scrollbar_x_stabilize = (window->ScrollbarXStabilizeToggledHistory != 0) && ImCountSetBits(window->ScrollbarXStabilizeToggledHistory) >= 4; // 4 == half of bits in our U8 history. + if (scrollbar_x_stabilize) + window->ScrollbarX = true; + //if (scrollbar_x_stabilize && !window->ScrollbarXStabilizeEnabled) + // IMGUI_DEBUG_LOG("[scroll] Stabilize ScrollbarX for Window '%s'\n", window->Name); + window->ScrollbarXStabilizeEnabled = scrollbar_x_stabilize; + + if (window->ScrollbarX && !window->ScrollbarY) + window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + + // Amend the partially filled window->DecorationXXX values. + window->DecoOuterSizeX2 += window->ScrollbarSizes.x; + window->DecoOuterSizeY2 += window->ScrollbarSizes.y; + } + + // UPDATE RECTANGLES (1- THOSE NOT AFFECTED BY SCROLLING) + // Update various regions. Variables they depend on should be set above in this function. + // We set this up after processing the resize grip so that our rectangles doesn't lag by a frame. + + // Outer rectangle + // Not affected by window border size. Used by: + // - FindHoveredWindow() (w/ extra padding when border resize is enabled) + // - Begin() initial clipping rect for drawing window background and borders. + // - Begin() clipping whole child + const ImRect host_rect = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) ? parent_window->ClipRect : viewport_rect; + const ImRect outer_rect = window->Rect(); + const ImRect title_bar_rect = window->TitleBarRect(); + window->OuterRectClipped = outer_rect; + window->OuterRectClipped.ClipWith(host_rect); + + // Inner rectangle + // Not affected by window border size. Used by: + // - InnerClipRect + // - ScrollToRectEx() + // - NavUpdatePageUpPageDown() + // - Scrollbar() + window->InnerRect.Min.x = window->Pos.x + window->DecoOuterSizeX1; + window->InnerRect.Min.y = window->Pos.y + window->DecoOuterSizeY1; + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->DecoOuterSizeX2; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->DecoOuterSizeY2; + + // Inner clipping rectangle. + // - Extend a outside of normal work region up to borders. + // - This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. + // - It also makes clipped items be more noticeable. + // - And is consistent on both axis (prior to 2024/05/03 ClipRect used WindowPadding.x * 0.5f on left and right edge), see #3312 + // - Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + // Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior. + // Affected by window/frame border size. Used by: + // - Begin() initial clip rect + float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); + + // Try to match the fact that our border is drawn centered over the window rectangle, rather than inner. + // This is why we do a *0.5f here. We don't currently even technically support large values for WindowBorderSize, + // see e.g #7887 #7888, but may do after we move the window border to become an inner border (and then we can remove the 0.5f here). + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + window->WindowBorderSize * 0.5f); + window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size * 0.5f); + window->InnerClipRect.Max.x = ImFloor(window->InnerRect.Max.x - window->WindowBorderSize * 0.5f); + window->InnerClipRect.Max.y = ImFloor(window->InnerRect.Max.y - window->WindowBorderSize * 0.5f); + window->InnerClipRect.ClipWithFull(host_rect); + + // Default item width. Make it proportional to window size if window manually resizes + if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) + window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f); + else + window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f); + + // SCROLLING + + // Lock down maximum scrolling + // The value of ScrollMax are ahead from ScrollbarX/ScrollbarY which is intentionally using InnerRect from previous rect in order to accommodate + // for right/bottom aligned items without creating a scrollbar. + window->ScrollMax.x = ImMax(0.0f, window->ContentSize.x + window->WindowPadding.x * 2.0f - window->InnerRect.GetWidth()); + window->ScrollMax.y = ImMax(0.0f, window->ContentSize.y + window->WindowPadding.y * 2.0f - window->InnerRect.GetHeight()); + + // Apply scrolling + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); + window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + window->DecoInnerSizeX1 = window->DecoInnerSizeY1 = 0.0f; + + // DRAWING + + // Setup draw list and outer clipping rectangle + IM_ASSERT(window->DrawList->CmdBuffer.Size == 1 && window->DrawList->CmdBuffer[0].ElemCount == 0); + window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); + PushClipRect(host_rect.Min, host_rect.Max, false); + + // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71) + // When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order. + // FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493) + { + bool render_decorations_in_parent = false; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) + { + // - We test overlap with the previous child window only (testing all would end up being O(log N) not a good investment here) + // - We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping childs + ImGuiWindow* previous_child = parent_window->DC.ChildWindows.Size >= 2 ? parent_window->DC.ChildWindows[parent_window->DC.ChildWindows.Size - 2] : NULL; + bool previous_child_overlapping = previous_child ? previous_child->Rect().Overlaps(window->Rect()) : false; + bool parent_is_empty = (parent_window->DrawList->VtxBuffer.Size == 0); + if (window->DrawList->CmdBuffer.back().ElemCount == 0 && !parent_is_empty && !previous_child_overlapping) + render_decorations_in_parent = true; + } + if (render_decorations_in_parent) + window->DrawList = parent_window->DrawList; + + // Handle title bar, scrollbar, resize grips and resize borders + const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; + const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight); + const bool handle_borders_and_resize_grips = true; // This exists to facilitate merge with 'docking' branch. + RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_count, resize_grip_col, resize_grip_draw_size); + + if (render_decorations_in_parent) + window->DrawList = &window->DrawListInst; + } + + // UPDATE RECTANGLES (2- THOSE AFFECTED BY SCROLLING) + + // Work rectangle. + // Affected by window padding and border size. Used by: + // - Columns() for right-most edge + // - TreeNode(), CollapsingHeader() for right-most edge + // - BeginTabBar() for right-most edge + const bool allow_scrollbar_x = !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar); + const bool allow_scrollbar_y = !(flags & ImGuiWindowFlags_NoScrollbar); + const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); + window->WorkRect.Min.x = ImTrunc(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); + window->WorkRect.Min.y = ImTrunc(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); + window->WorkRect.Max.x = window->WorkRect.Min.x + work_rect_size_x; + window->WorkRect.Max.y = window->WorkRect.Min.y + work_rect_size_y; + window->ParentWorkRect = window->WorkRect; + + // [LEGACY] Content Region + // FIXME-OBSOLETE: window->ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // Unless explicit content size is specified by user, this currently represent the region leading to no scrolling. + // Used by: + // - Mouse wheel scrolling + many other things + window->ContentRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x + window->DecoOuterSizeX1; + window->ContentRegionRect.Min.y = window->Pos.y - window->Scroll.y + window->WindowPadding.y + window->DecoOuterSizeY1; + window->ContentRegionRect.Max.x = window->ContentRegionRect.Min.x + (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : (window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + window->ContentRegionRect.Max.y = window->ContentRegionRect.Min.y + (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : (window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); + + // Setup drawing context + // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) + window->DC.Indent.x = window->DecoOuterSizeX1 + window->WindowPadding.x - window->Scroll.x; + window->DC.GroupOffset.x = 0.0f; + window->DC.ColumnsOffset.x = 0.0f; + + // Record the loss of precision of CursorStartPos which can happen due to really large scrolling amount. + // This is used by clipper to compensate and fix the most common use case of large scroll area. Easy and cheap, next best thing compared to switching everything to double or ImU64. + double start_pos_highp_x = (double)window->Pos.x + window->WindowPadding.x - (double)window->Scroll.x + window->DecoOuterSizeX1 + window->DC.ColumnsOffset.x; + double start_pos_highp_y = (double)window->Pos.y + window->WindowPadding.y - (double)window->Scroll.y + window->DecoOuterSizeY1; + window->DC.CursorStartPos = ImVec2((float)start_pos_highp_x, (float)start_pos_highp_y); + window->DC.CursorStartPosLossyness = ImVec2((float)(start_pos_highp_x - window->DC.CursorStartPos.x), (float)(start_pos_highp_y - window->DC.CursorStartPos.y)); + window->DC.CursorPos = window->DC.CursorStartPos; + window->DC.CursorPosPrevLine = window->DC.CursorPos; + window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.IdealMaxPos = window->DC.CursorStartPos; + window->DC.CurrLineSize = window->DC.PrevLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.NavLayersActiveMask = window->DC.NavLayersActiveMaskNext; + window->DC.NavLayersActiveMaskNext = 0x00; + window->DC.NavIsScrollPushableX = true; + window->DC.NavHideHighlightOneFrame = false; + window->DC.NavWindowHasScrollY = (window->ScrollMax.y > 0.0f); + + window->DC.MenuBarAppending = false; + window->DC.MenuColumns.Update(style.ItemSpacing.x, window_just_activated_by_user); + window->DC.TreeDepth = 0; + window->DC.TreeHasStackDataDepthMask = 0x00; + window->DC.ChildWindows.resize(0); + window->DC.StateStorage = &window->StateStorage; + window->DC.CurrentColumns = NULL; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; + + window->DC.ItemWidth = window->ItemWidthDefault; + window->DC.TextWrapPos = -1.0f; // disabled + window->DC.ItemWidthStack.resize(0); + window->DC.TextWrapPosStack.resize(0); + if (flags & ImGuiWindowFlags_Modal) + window->DC.ModalDimBgColor = ColorConvertFloat4ToU32(GetStyleColorVec4(ImGuiCol_ModalWindowDimBg)); + + if (window->AutoFitFramesX > 0) + window->AutoFitFramesX--; + if (window->AutoFitFramesY > 0) + window->AutoFitFramesY--; + + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + // We ImGuiFocusRequestFlags_UnlessBelowModal to: + // - Avoid focusing a window that is created outside of a modal. This will prevent active modal from being closed. + // - Position window behind the modal that is not a begin-parent of this window. + if (want_focus) + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + if (want_focus && window == g.NavWindow) + NavInitWindow(window, false); // <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls + + // Pressing CTRL+C copy window content into the clipboard + // [EXPERIMENTAL] Breaks on nested Begin/End pairs. We need to work that out and add better logging scope. + // [EXPERIMENTAL] Text outputs has many issues. + if (g.IO.ConfigWindowsCopyContentsWithCtrlC) + if (g.NavWindow && g.NavWindow->RootWindow == window && g.ActiveId == 0 && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C)) + LogToClipboard(0); + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window->WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window->WindowBorderSize, title_bar_rect.Max.y), name, p_open); + + // Clear hit test shape every frame + window->HitTestHoleSize.x = window->HitTestHoleSize.y = 0; + + if (flags & ImGuiWindowFlags_Tooltip) + g.TooltipPreviousWindow = window; + + // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). + // This is useful to allow creating context menus on title bar only, etc. + window->DC.WindowItemStatusFlags = ImGuiItemStatusFlags_None; + window->DC.WindowItemStatusFlags |= IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; + SetLastItemDataForWindow(window, title_bar_rect); + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugLocateId != 0 && (window->ID == g.DebugLocateId || window->MoveId == g.DebugLocateId)) + DebugLocateItemResolveWithLastItem(); +#endif + + // [Test Engine] Register title bar / tab with MoveId. +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (!(window->Flags & ImGuiWindowFlags_NoTitleBar)) + { + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + IMGUI_TEST_ENGINE_ITEM_ADD(g.LastItemData.ID, g.LastItemData.Rect, &g.LastItemData); + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + } +#endif + } + else + { + // Skip refresh always mark active + if (window->SkipRefresh) + SetWindowActiveForSkipRefresh(window); + + // Append + SetCurrentWindow(window); + SetLastItemDataForWindow(window, window->TitleBarRect()); + } + + if (!window->SkipRefresh) + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + + // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) + window->WriteAccessed = false; + window->BeginCount++; + g.NextWindowData.ClearFlags(); + + // Update visibility + if (first_begin_of_the_frame && !window->SkipRefresh) + { + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_ChildMenu)) + { + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); + const bool nav_request = (window->ChildFlags & ImGuiChildFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (!g.LogEnabled && !nav_request) + if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + { + if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + window->HiddenFramesCannotSkipItems = 1; + else + window->HiddenFramesCanSkipItems = 1; + } + + // Hide along with parent or if parent is collapsed + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) + window->HiddenFramesCanSkipItems = 1; + if (parent_window && parent_window->HiddenFramesCannotSkipItems > 0) + window->HiddenFramesCannotSkipItems = 1; + } + + // Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point) + if (style.Alpha <= 0.0f) + window->HiddenFramesCanSkipItems = 1; + + // Update the Hidden flag + bool hidden_regular = (window->HiddenFramesCanSkipItems > 0) || (window->HiddenFramesCannotSkipItems > 0); + window->Hidden = hidden_regular || (window->HiddenFramesForRenderOnly > 0); + + // Disable inputs for requested number of frames + if (window->DisableInputsFrames > 0) + { + window->DisableInputsFrames--; + window->Flags |= ImGuiWindowFlags_NoInputs; + } + + // Update the SkipItems flag, used to early out of all items functions (no layout required) + bool skip_items = false; + if (window->Collapsed || !window->Active || hidden_regular) + if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) + skip_items = true; + window->SkipItems = skip_items; + } + else if (first_begin_of_the_frame) + { + // Skip refresh mode + window->SkipItems = true; + } + + // [DEBUG] io.ConfigDebugBeginReturnValue override return value to test Begin/End and BeginChild/EndChild behaviors. + // (The implicit fallback window is NOT automatically ended allowing it to always be able to receive commands without crashing) +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (!window->IsFallbackWindow) + if ((g.IO.ConfigDebugBeginReturnValueOnce && window_just_created) || (g.IO.ConfigDebugBeginReturnValueLoop && g.DebugBeginReturnValueCullDepth == g.CurrentWindowStack.Size)) + { + if (window->AutoFitFramesX > 0) { window->AutoFitFramesX++; } + if (window->AutoFitFramesY > 0) { window->AutoFitFramesY++; } + return false; + } +#endif + + return !window->SkipItems; +} + +void ImGui::End() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Error checking: verify that user hasn't called End() too many times! + if (g.CurrentWindowStack.Size <= 1 && g.WithinFrameScopeWithImplicitWindow) + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size > 1, "Calling End() too many times!"); + return; + } + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); + + // Error checking: verify that user doesn't directly call End() on a child window. + if (window->Flags & ImGuiWindowFlags_ChildWindow) + IM_ASSERT_USER_ERROR(g.WithinEndChildID == window->ID, "Must call EndChild() and not End()!"); + + // Close anything that is open + if (window->DC.CurrentColumns) + EndColumns(); + if (!window->SkipRefresh) + PopClipRect(); // Inner window clip rectangle + PopFocusScope(); + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + EndDisabledOverrideReenable(); + + if (window->SkipRefresh) + { + IM_ASSERT(window->DrawList == NULL); + window->DrawList = &window->DrawListInst; + } + + // Stop logging + if (g.LogWindow == window) // FIXME: add more options for scope of logging + LogFinish(); + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Pop from window stack + g.LastItemData = window_stack_data.ParentLastItemDataBackup; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth--; + if (window->Flags & ImGuiWindowFlags_Popup) + g.BeginPopupStack.pop_back(); + + // Error handling, state recovery + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverWindowState(&window_stack_data.StackSizesInBegin); + + g.CurrentWindowStack.pop_back(); + SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); +} + +// Important: this alone doesn't alter current ImDrawList state. This is called by PushFont/PopFont only. +void ImGui::SetCurrentFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(font && font->IsLoaded()); // Font Atlas not created. Did you call io.Fonts->GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ? + IM_ASSERT(font->Scale > 0.0f); + g.Font = font; + g.FontBaseSize = ImMax(1.0f, g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale); + g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; + g.FontScale = g.FontSize / g.Font->FontSize; + + ImFontAtlas* atlas = g.Font->ContainerAtlas; + g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.TexUvLines = atlas->TexUvLines; + g.DrawListSharedData.Font = g.Font; + g.DrawListSharedData.FontSize = g.FontSize; + g.DrawListSharedData.FontScale = g.FontScale; +} + +// Use ImDrawList::_SetTextureID(), making our shared g.FontStack[] authorative against window-local ImDrawList. +// - Whereas ImDrawList::PushTextureID()/PopTextureID() is not to be used across Begin() calls. +// - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did... +// - Some code paths never really fully worked with multiple atlas textures. +// - The right-ish solution may be to remove _SetTextureID() and make AddText/RenderText lazily call PushTextureID()/PopTextureID() +// the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem +// because we have a concrete need and a test bed for multiple atlas textures. +void ImGui::PushFont(ImFont* font) +{ + ImGuiContext& g = *GImGui; + if (font == NULL) + font = GetDefaultFont(); + g.FontStack.push_back(font); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); +} + +void ImGui::PopFont() +{ + ImGuiContext& g = *GImGui; + if (g.FontStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!"); + return; + } + g.FontStack.pop_back(); + ImFont* font = g.FontStack.Size == 0 ? GetDefaultFont() : g.FontStack.back(); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); +} + +void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) +{ + ImGuiContext& g = *GImGui; + ImGuiItemFlags item_flags = g.CurrentItemFlags; + IM_ASSERT(item_flags == g.ItemFlagsStack.back()); + if (enabled) + item_flags |= option; + else + item_flags &= ~option; + g.CurrentItemFlags = item_flags; + g.ItemFlagsStack.push_back(item_flags); +} + +void ImGui::PopItemFlag() +{ + ImGuiContext& g = *GImGui; + if (g.ItemFlagsStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemFlag() too many times!"); + return; + } + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); +} + +// BeginDisabled()/EndDisabled() +// - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) +// - Visually this is currently altering alpha, but it is expected that in a future styling system this would work differently. +// - Feedback welcome at https://github.com/ocornut/imgui/issues/211 +// - BeginDisabled(false)/EndDisabled() essentially does nothing but is provided to facilitate use of boolean expressions. +// (as a micro-optimization: if you have tens of thousands of BeginDisabled(false)/EndDisabled() pairs, you might want to reformulate your code to avoid making those calls) +// - Note: mixing up BeginDisabled() and PushItemFlag(ImGuiItemFlags_Disabled) is currently NOT SUPPORTED. +void ImGui::BeginDisabled(bool disabled) +{ + ImGuiContext& g = *GImGui; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (!was_disabled && disabled) + { + g.DisabledAlphaBackup = g.Style.Alpha; + g.Style.Alpha *= g.Style.DisabledAlpha; // PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * g.Style.DisabledAlpha); + } + if (was_disabled || disabled) + g.CurrentItemFlags |= ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); // FIXME-OPT: can we simply skip this and use DisabledStackSize? + g.DisabledStackSize++; +} + +void ImGui::EndDisabled() +{ + ImGuiContext& g = *GImGui; + if (g.DisabledStackSize <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling EndDisabled() too many times!"); + return; + } + g.DisabledStackSize--; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + //PopItemFlag(); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + if (was_disabled && (g.CurrentItemFlags & ImGuiItemFlags_Disabled) == 0) + g.Style.Alpha = g.DisabledAlphaBackup; //PopStyleVar(); +} + +// Could have been called BeginDisabledDisable() but it didn't want to be award nominated for most awkward function name. +// Ideally we would use a shared e.g. BeginDisabled()->BeginDisabledEx() but earlier needs to be optimal. +// The whole code for this is awkward, will reevaluate if we find a way to implement SetNextItemDisabled(). +void ImGui::BeginDisabledOverrideReenable() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentItemFlags & ImGuiItemFlags_Disabled); + g.CurrentWindowStack.back().DisabledOverrideReenableAlphaBackup = g.Style.Alpha; + g.Style.Alpha = g.DisabledAlphaBackup; + g.CurrentItemFlags &= ~ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); + g.DisabledStackSize++; +} + +void ImGui::EndDisabledOverrideReenable() +{ + ImGuiContext& g = *GImGui; + g.DisabledStackSize--; + IM_ASSERT(g.DisabledStackSize > 0); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.Style.Alpha = g.CurrentWindowStack.back().DisabledOverrideReenableAlphaBackup; +} + +void ImGui::PushTextWrapPos(float wrap_pos_x) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.TextWrapPosStack.push_back(window->DC.TextWrapPos); + window->DC.TextWrapPos = wrap_pos_x; +} + +void ImGui::PopTextWrapPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.TextWrapPosStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopTextWrapPos() too many times!"); + return; + } + window->DC.TextWrapPos = window->DC.TextWrapPosStack.back(); + window->DC.TextWrapPosStack.pop_back(); +} + +static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy) +{ + ImGuiWindow* last_window = NULL; + while (last_window != window) + { + last_window = window; + window = window->RootWindow; + if (popup_hierarchy) + window = window->RootWindowPopupTree; + } + return window; +} + +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy) +{ + ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy); + if (window_root == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + if (window == window_root) // end of chain + return false; + window = window->ParentWindow; + } + return false; +} + +bool ImGui::IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent) +{ + if (window->RootWindow == potential_parent) + return true; + while (window != NULL) + { + if (window == potential_parent) + return true; + window = window->ParentWindowInBeginStack; + } + return false; +} + +bool ImGui::IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below) +{ + ImGuiContext& g = *GImGui; + + // It would be saner to ensure that display layer is always reflected in the g.Windows[] order, which would likely requires altering all manipulations of that array + const int display_layer_delta = GetWindowDisplayLayer(potential_above) - GetWindowDisplayLayer(potential_below); + if (display_layer_delta != 0) + return display_layer_delta > 0; + + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* candidate_window = g.Windows[i]; + if (candidate_window == potential_above) + return true; + if (candidate_window == potential_below) + return false; + } + return false; +} + +// Is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. +bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR((flags & ~ImGuiHoveredFlags_AllowedMaskForIsWindowHovered) == 0, "Invalid flags for IsWindowHovered()!"); + + ImGuiWindow* ref_window = g.HoveredWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + if (ref_window == NULL) + return false; + + if ((flags & ImGuiHoveredFlags_AnyWindow) == 0) + { + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiHoveredFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + + bool result; + if (flags & ImGuiHoveredFlags_ChildWindows) + result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + result = (ref_window == cur_window); + if (!result) + return false; + } + + if (!IsWindowContentHoverable(ref_window, flags)) + return false; + if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != ref_window->MoveId) + return false; + + // When changing hovered window we requires a bit of stationary delay before activating hover timer. + // FIXME: We don't support delay other than stationary one for now, other delay would need a way + // to fulfill the possibility that multiple IsWindowHovered() with varying flag could return true + // for different windows of the hierarchy. Possibly need a Hash(Current+Flags) ==> (Timer) cache. + // We can implement this for _Stationary because the data is linked to HoveredWindow rather than CurrentWindow. + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverWindowUnlockedStationaryId != ref_window->ID) + return false; + + return true; +} + +float ImGui::GetWindowWidth() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.x; +} + +float ImGui::GetWindowHeight() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.y; +} + +ImVec2 ImGui::GetWindowPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + return window->Pos; +} + +void ImGui::SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowPosAllowFlags & cond) == 0) + return; + + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + window->SetWindowPosVal = ImVec2(FLT_MAX, FLT_MAX); + + // Set + const ImVec2 old_pos = window->Pos; + window->Pos = ImTrunc(pos); + ImVec2 offset = window->Pos - old_pos; + if (offset.x == 0.0f && offset.y == 0.0f) + return; + MarkIniSettingsDirty(window); + window->DC.CursorPos += offset; // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor + window->DC.CursorMaxPos += offset; // And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected. + window->DC.IdealMaxPos += offset; + window->DC.CursorStartPos += offset; +} + +void ImGui::SetWindowPos(const ImVec2& pos, ImGuiCond cond) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); +} + +void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); +} + +ImVec2 ImGui::GetWindowSize() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Size; +} + +void ImGui::SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) + return; + + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Enable auto-fit (not done in BeginChild() path unless appearing or combined with ImGuiChildFlags_AlwaysAutoResize) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesX = (size.x <= 0.0f) ? 2 : 0; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesY = (size.y <= 0.0f) ? 2 : 0; + + // Set + ImVec2 old_size = window->SizeFull; + if (size.x <= 0.0f) + window->AutoFitOnlyGrows = false; + else + window->SizeFull.x = IM_TRUNC(size.x); + if (size.y <= 0.0f) + window->AutoFitOnlyGrows = false; + else + window->SizeFull.y = IM_TRUNC(size.y); + if (old_size.x != window->SizeFull.x || old_size.y != window->SizeFull.y) + MarkIniSettingsDirty(window); +} + +void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) +{ + SetWindowSize(GImGui->CurrentWindow, size, cond); +} + +void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowSize(window, size, cond); +} + +void ImGui::SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowCollapsedAllowFlags & cond) == 0) + return; + window->SetWindowCollapsedAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // Set + window->Collapsed = collapsed; +} + +void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size) +{ + IM_ASSERT(window->HitTestHoleSize.x == 0); // We don't support multiple holes/hit test filters + window->HitTestHoleSize = ImVec2ih(size); + window->HitTestHoleOffset = ImVec2ih(pos - window->Pos); +} + +void ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window) +{ + window->Hidden = window->SkipItems = true; + window->HiddenFramesCanSkipItems = 1; +} + +void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); +} + +bool ImGui::IsWindowCollapsed() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Collapsed; +} + +bool ImGui::IsWindowAppearing() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Appearing; +} + +void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); +} + +void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasPos; + g.NextWindowData.PosVal = pos; + g.NextWindowData.PosPivotVal = pivot; + g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasSize; + g.NextWindowData.SizeVal = size; + g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; +} + +// For each axis: +// - Use 0.0f as min or FLT_MAX as max if you don't want limits, e.g. size_min = (500.0f, 0.0f), size_max = (FLT_MAX, FLT_MAX) sets a minimum width. +// - Use -1 for both min and max of same axis to preserve current size which itself is a constraint. +// - See "Demo->Examples->Constrained-resizing window" for examples. +void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasSizeConstraint; + g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); + g.NextWindowData.SizeCallback = custom_callback; + g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; +} + +// Content size = inner scrollable rectangle, padded with WindowPadding. +// SetNextWindowContentSize(ImVec2(100,100) + ImGuiWindowFlags_AlwaysAutoResize will always allow submitting a 100x100 item. +void ImGui::SetNextWindowContentSize(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasContentSize; + g.NextWindowData.ContentSizeVal = ImTrunc(size); +} + +void ImGui::SetNextWindowScroll(const ImVec2& scroll) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasScroll; + g.NextWindowData.ScrollVal = scroll; +} + +void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasCollapsed; + g.NextWindowData.CollapsedVal = collapsed; + g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; +} + +void ImGui::SetNextWindowBgAlpha(float alpha) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasBgAlpha; + g.NextWindowData.BgAlphaVal = alpha; +} + +// This is experimental and meant to be a toy for exploring a future/wider range of features. +void ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasRefreshPolicy; + g.NextWindowData.RefreshFlagsVal = flags; +} + +ImDrawList* ImGui::GetWindowDrawList() +{ + ImGuiWindow* window = GetCurrentWindow(); + return window->DrawList; +} + +ImFont* ImGui::GetFont() +{ + return GImGui->Font; +} + +float ImGui::GetFontSize() +{ + return GImGui->FontSize; +} + +ImVec2 ImGui::GetFontTexUvWhitePixel() +{ + return GImGui->DrawListSharedData.TexUvWhitePixel; +} + +void ImGui::SetWindowFontScale(float scale) +{ + IM_ASSERT(scale > 0.0f); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->FontWindowScale = scale; + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; +} + +void ImGui::PushFocusScope(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiFocusScopeData data; + data.ID = id; + data.WindowID = g.CurrentWindow->ID; + g.FocusScopeStack.push_back(data); + g.CurrentFocusScopeId = id; +} + +void ImGui::PopFocusScope() +{ + ImGuiContext& g = *GImGui; + if (g.FocusScopeStack.Size <= g.StackSizesInBeginForCurrentWindow->SizeOfFocusScopeStack) + { + IM_ASSERT_USER_ERROR(0, "Calling PopFocusScope() too many times!"); + return; + } + g.FocusScopeStack.pop_back(); + g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; +} + +void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) +{ + ImGuiContext& g = *GImGui; + g.NavFocusScopeId = focus_scope_id; + g.NavFocusRoute.resize(0); // Invalidate + if (focus_scope_id == 0) + return; + IM_ASSERT(g.NavWindow != NULL); + + // Store current path (in reverse order) + if (focus_scope_id == g.CurrentFocusScopeId) + { + // Top of focus stack contains local focus scopes inside current window + for (int n = g.FocusScopeStack.Size - 1; n >= 0 && g.FocusScopeStack.Data[n].WindowID == g.CurrentWindow->ID; n--) + g.NavFocusRoute.push_back(g.FocusScopeStack.Data[n]); + } + else if (focus_scope_id == g.NavWindow->NavRootFocusScopeId) + g.NavFocusRoute.push_back({ focus_scope_id, g.NavWindow->ID }); + else + return; + + // Then follow on manually set ParentWindowForFocusRoute field (#6798) + for (ImGuiWindow* window = g.NavWindow->ParentWindowForFocusRoute; window != NULL; window = window->ParentWindowForFocusRoute) + g.NavFocusRoute.push_back({ window->NavRootFocusScopeId, window->ID }); + IM_ASSERT(g.NavFocusRoute.Size < 100); // Maximum depth is technically 251 as per CalcRoutingScore(): 254 - 3 +} + +// Focus = move navigation cursor, set scrolling, set focus window. +void ImGui::FocusItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_FOCUS("FocusItem(0x%08x) in window \"%s\"\n", g.LastItemData.ID, window->Name); + if (g.DragDropActive || g.MovingWindow != NULL) // FIXME: Opt-in flags for this? + { + IMGUI_DEBUG_LOG_FOCUS("FocusItem() ignored while DragDropActive!\n"); + return; + } + + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavCursorVisible | ImGuiNavMoveFlags_NoSelect; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + SetNavWindow(window); + NavMoveRequestSubmit(ImGuiDir_None, ImGuiDir_Up, move_flags, scroll_flags); + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); +} + +void ImGui::ActivateItemByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; + g.NavNextActivateFlags = ImGuiActivateFlags_None; +} + +// Note: this will likely be called ActivateItem() once we rework our Focus/Activation system! +// But ActivateItem() should function without altering scroll/focus? +void ImGui::SetKeyboardFocusHere(int offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(offset >= -1); // -1 is allowed but not below + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere(%d) in window \"%s\"\n", offset, window->Name); + + // It makes sense in the vast majority of cases to never interrupt a drag and drop. + // When we refactor this function into ActivateItem() we may want to make this an option. + // MovingWindow is protected from most user inputs using SetActiveIdUsingNavAndKeys(), but + // is also automatically dropped in the event g.ActiveId is stolen. + if (g.DragDropActive || g.MovingWindow != NULL) + { + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere() ignored while DragDropActive!\n"); + return; + } + + SetNavWindow(window); + + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavCursorVisible; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + if (offset == -1) + { + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); + } + else + { + g.NavTabbingDir = 1; + g.NavTabbingCounter = offset + 1; + } +} + +void ImGui::SetItemDefaultFocus() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!window->Appearing) + return; + if (g.NavWindow != window->RootWindowForNav || (!g.NavInitRequest && g.NavInitResult.ID == 0) || g.NavLayer != window->DC.NavLayerCurrent) + return; + + g.NavInitRequest = false; + NavApplyItemToResult(&g.NavInitResult); + NavUpdateAnyRequestFlag(); + + // Scroll could be done in NavInitRequestApplyResult() via an opt-in flag (we however don't want regular init requests to scroll) + if (!window->ClipRect.Contains(g.LastItemData.Rect)) + ScrollToRectEx(window, g.LastItemData.Rect, ImGuiScrollFlags_None); +} + +void ImGui::SetStateStorage(ImGuiStorage* tree) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + window->DC.StateStorage = tree ? tree : &window->StateStorage; +} + +ImGuiStorage* ImGui::GetStateStorage() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->DC.StateStorage; +} + +bool ImGui::IsRectVisible(const ImVec2& size) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} + +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} + +//----------------------------------------------------------------------------- +// [SECTION] ID STACK +//----------------------------------------------------------------------------- + +// This is one of the very rare legacy case where we use ImGuiWindow methods, +// it should ideally be flattened at some point but it's been used a lots by widgets. +IM_MSVC_RUNTIME_CHECKS_OFF +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(int n) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; +} + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +// FIXME: Consider instead storing last non-zero ID + count of successive zero-ID, and combine those? +ImGuiID ImGuiWindow::GetIDFromPos(const ImVec2& p_abs) +{ + ImGuiID seed = IDStack.back(); + ImVec2 p_rel = ImGui::WindowPosAbsToRel(this, p_abs); + ImGuiID id = ImHashData(&p_rel, sizeof(p_rel), seed); + return id; +} + +// " +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +{ + ImGuiID seed = IDStack.back(); + ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); + ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); + return id; +} + +void ImGui::PushID(const char* str_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id_begin, str_id_end); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const void* ptr_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(ptr_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(int int_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(int_id); + window->IDStack.push_back(id); +} + +// Push a given id value ignoring the ID stack as a seed. +void ImGui::PushOverrideID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL); +#endif + window->IDStack.push_back(id); +} + +// Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call +// (note that when using this pattern, ID Stack Tool will tend to not display the intermediate stack level. +// for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more) +ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) +{ + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; +} + +ImGuiID ImGui::GetIDWithSeed(int n, ImGuiID seed) +{ + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; +} + +void ImGui::PopID() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + if (window->IDStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopID() too many times!"); + return; + } + window->IDStack.pop_back(); +} + +ImGuiID ImGui::GetID(const char* str_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id); +} + +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id_begin, str_id_end); +} + +ImGuiID ImGui::GetID(const void* ptr_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(ptr_id); +} + +ImGuiID ImGui::GetID(int int_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(int_id); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] INPUTS +//----------------------------------------------------------------------------- +// - GetModForLRModKey() [Internal] +// - FixupKeyChord() [Internal] +// - GetKeyData() [Internal] +// - GetKeyIndex() [Internal] +// - GetKeyName() +// - GetKeyChordName() [Internal] +// - CalcTypematicRepeatAmount() [Internal] +// - GetTypematicRepeatRate() [Internal] +// - GetKeyPressedAmount() [Internal] +// - GetKeyMagnitude2d() [Internal] +//----------------------------------------------------------------------------- +// - UpdateKeyRoutingTable() [Internal] +// - GetRoutingIdFromOwnerId() [Internal] +// - GetShortcutRoutingData() [Internal] +// - CalcRoutingScore() [Internal] +// - SetShortcutRouting() [Internal] +// - TestShortcutRouting() [Internal] +//----------------------------------------------------------------------------- +// - IsKeyDown() +// - IsKeyPressed() +// - IsKeyReleased() +//----------------------------------------------------------------------------- +// - IsMouseDown() +// - IsMouseClicked() +// - IsMouseReleased() +// - IsMouseDoubleClicked() +// - GetMouseClickedCount() +// - IsMouseHoveringRect() [Internal] +// - IsMouseDragPastThreshold() [Internal] +// - IsMouseDragging() +// - GetMousePos() +// - SetMousePos() [Internal] +// - GetMousePosOnOpeningCurrentPopup() +// - IsMousePosValid() +// - IsAnyMouseDown() +// - GetMouseDragDelta() +// - ResetMouseDragDelta() +// - GetMouseCursor() +// - SetMouseCursor() +//----------------------------------------------------------------------------- +// - UpdateAliasKey() +// - GetMergedModsFromKeys() +// - UpdateKeyboardInputs() +// - UpdateMouseInputs() +//----------------------------------------------------------------------------- +// - LockWheelingWindow [Internal] +// - FindBestWheelingWindow [Internal] +// - UpdateMouseWheel() [Internal] +//----------------------------------------------------------------------------- +// - SetNextFrameWantCaptureKeyboard() +// - SetNextFrameWantCaptureMouse() +//----------------------------------------------------------------------------- +// - GetInputSourceName() [Internal] +// - DebugPrintInputEvent() [Internal] +// - UpdateInputEvents() [Internal] +//----------------------------------------------------------------------------- +// - GetKeyOwner() [Internal] +// - TestKeyOwner() [Internal] +// - SetKeyOwner() [Internal] +// - SetItemKeyOwner() [Internal] +// - Shortcut() [Internal] +//----------------------------------------------------------------------------- + +static ImGuiKeyChord GetModForLRModKey(ImGuiKey key) +{ + if (key == ImGuiKey_LeftCtrl || key == ImGuiKey_RightCtrl) + return ImGuiMod_Ctrl; + if (key == ImGuiKey_LeftShift || key == ImGuiKey_RightShift) + return ImGuiMod_Shift; + if (key == ImGuiKey_LeftAlt || key == ImGuiKey_RightAlt) + return ImGuiMod_Alt; + if (key == ImGuiKey_LeftSuper || key == ImGuiKey_RightSuper) + return ImGuiMod_Super; + return ImGuiMod_None; +} + +ImGuiKeyChord ImGui::FixupKeyChord(ImGuiKeyChord key_chord) +{ + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord |= GetModForLRModKey(key); + return key_chord; +} + +ImGuiKeyData* ImGui::GetKeyData(ImGuiContext* ctx, ImGuiKey key) +{ + ImGuiContext& g = *ctx; + + // Special storage location for mods + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + + IM_ASSERT(IsNamedKey(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend & user code."); + return &g.IO.KeysData[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Those names are provided for debugging purpose and are not meant to be saved persistently nor compared. +static const char* const GKeyNames[] = +{ + "Tab", "LeftArrow", "RightArrow", "UpArrow", "DownArrow", "PageUp", "PageDown", + "Home", "End", "Insert", "Delete", "Backspace", "Space", "Enter", "Escape", + "LeftCtrl", "LeftShift", "LeftAlt", "LeftSuper", "RightCtrl", "RightShift", "RightAlt", "RightSuper", "Menu", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", + "Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket", + "Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen", + "Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6", + "Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply", + "KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual", + "AppBack", "AppForward", "Oem102", + "GamepadStart", "GamepadBack", + "GamepadFaceLeft", "GamepadFaceRight", "GamepadFaceUp", "GamepadFaceDown", + "GamepadDpadLeft", "GamepadDpadRight", "GamepadDpadUp", "GamepadDpadDown", + "GamepadL1", "GamepadR1", "GamepadL2", "GamepadR2", "GamepadL3", "GamepadR3", + "GamepadLStickLeft", "GamepadLStickRight", "GamepadLStickUp", "GamepadLStickDown", + "GamepadRStickLeft", "GamepadRStickRight", "GamepadRStickUp", "GamepadRStickDown", + "MouseLeft", "MouseRight", "MouseMiddle", "MouseX1", "MouseX2", "MouseWheelX", "MouseWheelY", + "ModCtrl", "ModShift", "ModAlt", "ModSuper", // ReservedForModXXX are showing the ModXXX names. +}; +IM_STATIC_ASSERT(ImGuiKey_NamedKey_COUNT == IM_ARRAYSIZE(GKeyNames)); + +const char* ImGui::GetKeyName(ImGuiKey key) +{ + if (key == ImGuiKey_None) + return "None"; + IM_ASSERT(IsNamedKeyOrMod(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + if (!IsNamedKey(key)) + return "Unknown"; + + return GKeyNames[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Return untranslated names: on macOS, Cmd key will show as Ctrl, Ctrl key will show as super. +// Lifetime of return value: valid until next call to same function. +const char* ImGui::GetKeyChordName(ImGuiKeyChord key_chord) +{ + ImGuiContext& g = *GImGui; + + const ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord &= ~GetModForLRModKey(key); // Return "Ctrl+LeftShift" instead of "Ctrl+Shift+LeftShift" + ImFormatString(g.TempKeychordName, IM_ARRAYSIZE(g.TempKeychordName), "%s%s%s%s%s", + (key_chord & ImGuiMod_Ctrl) ? "Ctrl+" : "", + (key_chord & ImGuiMod_Shift) ? "Shift+" : "", + (key_chord & ImGuiMod_Alt) ? "Alt+" : "", + (key_chord & ImGuiMod_Super) ? "Super+" : "", + (key != ImGuiKey_None || key_chord == ImGuiKey_None) ? GetKeyName(key) : ""); + size_t len; + if (key == ImGuiKey_None && key_chord != 0) + if ((len = ImStrlen(g.TempKeychordName)) != 0) // Remove trailing '+' + g.TempKeychordName[len - 1] = 0; + return g.TempKeychordName; +} + +// t0 = previous time (e.g.: g.Time - g.IO.DeltaTime) +// t1 = current time (e.g.: g.Time) +// An event is triggered at: +// t = 0.0f t = repeat_delay, t = repeat_delay + repeat_rate*N +int ImGui::CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate) +{ + if (t1 == 0.0f) + return 1; + if (t0 >= t1) + return 0; + if (repeat_rate <= 0.0f) + return (t0 < repeat_delay) && (t1 >= repeat_delay); + const int count_t0 = (t0 < repeat_delay) ? -1 : (int)((t0 - repeat_delay) / repeat_rate); + const int count_t1 = (t1 < repeat_delay) ? -1 : (int)((t1 - repeat_delay) / repeat_rate); + const int count = count_t1 - count_t0; + return count; +} + +void ImGui::GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate) +{ + ImGuiContext& g = *GImGui; + switch (flags & ImGuiInputFlags_RepeatRateMask_) + { + case ImGuiInputFlags_RepeatRateNavMove: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.80f; return; + case ImGuiInputFlags_RepeatRateNavTweak: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.30f; return; + case ImGuiInputFlags_RepeatRateDefault: default: *repeat_delay = g.IO.KeyRepeatDelay * 1.00f; *repeat_rate = g.IO.KeyRepeatRate * 1.00f; return; + } +} + +// Return value representing the number of presses in the last time period, for the given repeat rate +// (most often returns 0 or 1. The result is generally only >1 when RepeatRate is smaller than DeltaTime, aka large DeltaTime or fast RepeatRate) +int ImGui::GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float repeat_rate) +{ + ImGuiContext& g = *GImGui; + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return 0; + const float t = key_data->DownDuration; + return CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, repeat_delay, repeat_rate); +} + +// Return 2D vector representing the combination of four cardinal direction, with analog value support (for e.g. ImGuiKey_GamepadLStick* values). +ImVec2 ImGui::GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down) +{ + return ImVec2( + GetKeyData(key_right)->AnalogValue - GetKeyData(key_left)->AnalogValue, + GetKeyData(key_down)->AnalogValue - GetKeyData(key_up)->AnalogValue); +} + +// Rewrite routing data buffers to strip old entries + sort by key to make queries not touch scattered data. +// Entries D,A,B,B,A,C,B --> A,A,B,B,B,C,D +// Index A:1 B:2 C:5 D:0 --> A:0 B:2 C:5 D:6 +// See 'Metrics->Key Owners & Shortcut Routing' to visualize the result of that operation. +static void ImGui::UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt) +{ + ImGuiContext& g = *GImGui; + rt->EntriesNext.resize(0); + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + const int new_routing_start_idx = rt->EntriesNext.Size; + ImGuiKeyRoutingData* routing_entry; + for (int old_routing_idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; old_routing_idx != -1; old_routing_idx = routing_entry->NextEntryIndex) + { + routing_entry = &rt->Entries[old_routing_idx]; + routing_entry->RoutingCurrScore = routing_entry->RoutingNextScore; + routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry + routing_entry->RoutingNext = ImGuiKeyOwner_NoOwner; + routing_entry->RoutingNextScore = 255; + if (routing_entry->RoutingCurr == ImGuiKeyOwner_NoOwner) + continue; + rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer + + // Apply routing to owner if there's no owner already (RoutingCurr == None at this point) + // This is the result of previous frame's SetShortcutRouting() call. + if (routing_entry->Mods == g.IO.KeyMods) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + { + owner_data->OwnerCurr = routing_entry->RoutingCurr; + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X) via Routing\n", GetKeyName(key), routing_entry->RoutingCurr); + } + } + } + + // Rewrite linked-list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = (ImGuiKeyRoutingIndex)(new_routing_start_idx < rt->EntriesNext.Size ? new_routing_start_idx : -1); + for (int n = new_routing_start_idx; n < rt->EntriesNext.Size; n++) + rt->EntriesNext[n].NextEntryIndex = (ImGuiKeyRoutingIndex)((n + 1 < rt->EntriesNext.Size) ? n + 1 : -1); + } + rt->Entries.swap(rt->EntriesNext); // Swap new and old indexes +} + +// owner_id may be None/Any, but routing_id needs to be always be set, so we default to GetCurrentFocusScope(). +static inline ImGuiID GetRoutingIdFromOwnerId(ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + return (owner_id != ImGuiKeyOwner_NoOwner && owner_id != ImGuiKeyOwner_Any) ? owner_id : g.CurrentFocusScopeId; +} + +ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord) +{ + // Majority of shortcuts will be Key + any number of Mods + // We accept _Single_ mod with ImGuiKey_None. + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl | ImGuiMod_Shift); // Legal + // - Shortcut(ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift); // Not legal + ImGuiContext& g = *GImGui; + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + ImGuiKeyRoutingData* routing_data; + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + IM_ASSERT(IsNamedKey(key)); + + // Get (in the majority of case, the linked list will have one element so this should be 2 reads. + // Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame). + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; idx = routing_data->NextEntryIndex) + { + routing_data = &rt->Entries[idx]; + if (routing_data->Mods == mods) + return routing_data; + } + + // Add to linked-list + ImGuiKeyRoutingIndex routing_data_idx = (ImGuiKeyRoutingIndex)rt->Entries.Size; + rt->Entries.push_back(ImGuiKeyRoutingData()); + routing_data = &rt->Entries[routing_data_idx]; + routing_data->Mods = (ImU16)mods; + routing_data->NextEntryIndex = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; // Setup linked list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = routing_data_idx; + return routing_data; +} + +// Current score encoding (lower is highest priority): +// - 0: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverActive +// - 1: ImGuiInputFlags_ActiveItem or ImGuiInputFlags_RouteFocused (if item active) +// - 2: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused +// - 3+: ImGuiInputFlags_RouteFocused (if window in focus-stack) +// - 254: ImGuiInputFlags_RouteGlobal +// - 255: never route +// 'flags' should include an explicit routing policy +static int CalcRoutingScore(ImGuiID focus_scope_id, ImGuiID owner_id, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiInputFlags_RouteFocused) + { + // ActiveID gets top priority + // (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it) + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + + // Score based on distance to focused window (lower is better) + // Assuming both windows are submitting a routing request, + // - When Window....... is focused -> Window scores 3 (best), Window/ChildB scores 255 (no match) + // - When Window/ChildB is focused -> Window scores 4, Window/ChildB scores 3 (best) + // Assuming only WindowA is submitting a routing request, + // - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score. + // This essentially follow the window->ParentWindowForFocusRoute chain. + if (focus_scope_id == 0) + return 255; + for (int index_in_focus_path = 0; index_in_focus_path < g.NavFocusRoute.Size; index_in_focus_path++) + if (g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id) + return 3 + index_in_focus_path; + return 255; + } + else if (flags & ImGuiInputFlags_RouteActive) + { + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + return 255; + } + else if (flags & ImGuiInputFlags_RouteGlobal) + { + if (flags & ImGuiInputFlags_RouteOverActive) + return 0; + if (flags & ImGuiInputFlags_RouteOverFocused) + return 2; + return 254; + } + IM_ASSERT(0); + return 0; +} + +// - We need this to filter some Shortcut() routes when an item e.g. an InputText() is active +// e.g. ImGuiKey_G won't be considered a shortcut when item is active, but ImGuiMod|ImGuiKey_G can be. +// - This is also used by UpdateInputEvents() to avoid trickling in the most common case of e.g. pressing ImGuiKey_G also emitting a G character. +static bool IsKeyChordPotentiallyCharInput(ImGuiKeyChord key_chord) +{ + // Mimic 'ignore_char_inputs' logic in InputText() + ImGuiContext& g = *GImGui; + + // When the right mods are pressed it cannot be a char input so we won't filter the shortcut out. + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + const bool ignore_char_inputs = ((mods & ImGuiMod_Ctrl) && !(mods & ImGuiMod_Alt)) || (g.IO.ConfigMacOSXBehaviors && (mods & ImGuiMod_Ctrl)); + if (ignore_char_inputs) + return false; + + // Return true for A-Z, 0-9 and other keys associated to char inputs. Other keys such as F1-F12 won't be filtered. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + return false; + return g.KeysMayBeCharInput.TestBit(key); +} + +// Request a desired route for an input chord (key + mods). +// Return true if the route is available this frame. +// - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state. +// (Conceptually this does a "Submit for next frame" + "Test for current frame". +// As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.) +bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive; // IMPORTANT: This is the default for SetShortcutRouting() but NOT Shortcut() + else + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiInputFlags_RouteTypeMask_)); // Check that only 1 routing flag is used + IM_ASSERT(owner_id != ImGuiKeyOwner_Any && owner_id != ImGuiKeyOwner_NoOwner); + if (flags & (ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused)) + IM_ASSERT(flags & ImGuiInputFlags_RouteGlobal); + + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + key_chord = FixupKeyChord(key_chord); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInShortcutRouting == key_chord) + IM_DEBUG_BREAK(); + + if (flags & ImGuiInputFlags_RouteUnlessBgFocused) + if (g.NavWindow == NULL) + return false; + + // Note how ImGuiInputFlags_RouteAlways won't set routing and thus won't set owner. May want to rework this? + if (flags & ImGuiInputFlags_RouteAlways) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> always, no register\n", GetKeyChordName(key_chord), flags, owner_id); + return true; + } + + // Specific culling when there's an active item. + if (g.ActiveId != 0 && g.ActiveId != owner_id) + { + if (flags & ImGuiInputFlags_RouteActive) + return false; + + // Cull shortcuts with no modifiers when it could generate a character. + // e.g. Shortcut(ImGuiKey_G) also generates 'g' character, should not trigger when InputText() is active. + // but Shortcut(Ctrl+G) should generally trigger when InputText() is active. + // TL;DR: lettered shortcut with no mods or with only Alt mod will not trigger while an item reading text input is active. + // (We cannot filter based on io.InputQueueCharacters[] contents because of trickling and key<>chars submission order are undefined) + if (g.IO.WantTextInput && IsKeyChordPotentiallyCharInput(key_chord)) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> filtered as potential char input\n", GetKeyChordName(key_chord), flags, owner_id); + return false; + } + + // ActiveIdUsingAllKeyboardKeys trumps all for ActiveId + if ((flags & ImGuiInputFlags_RouteOverActive) == 0 && g.ActiveIdUsingAllKeyboardKeys) + { + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey((ImGuiKey)(key_chord & ImGuiMod_Mask_)); + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + } + } + + // Where do we evaluate route for? + ImGuiID focus_scope_id = g.CurrentFocusScopeId; + if (flags & ImGuiInputFlags_RouteFromRootWindow) + focus_scope_id = g.CurrentWindow->RootWindow->ID; // See PushFocusScope() call in Begin() + + const int score = CalcRoutingScore(focus_scope_id, owner_id, flags); + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> score %d\n", GetKeyChordName(key_chord), flags, owner_id, score); + if (score == 255) + return false; + + // Submit routing for NEXT frame (assuming score is sufficient) + // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <). + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); + //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore); + if (score < routing_data->RoutingNextScore) + { + routing_data->RoutingNext = owner_id; + routing_data->RoutingNextScore = (ImU8)score; + } + + // Return routing state for CURRENT frame + if (routing_data->RoutingCurr == owner_id) + IMGUI_DEBUG_LOG_INPUTROUTING("--> granting current route\n"); + return routing_data->RoutingCurr == owner_id; +} + +// Currently unused by core (but used by tests) +// Note: this cannot be turned into GetShortcutRouting() because we do the owner_id->routing_id translation, name would be more misleading. +bool ImGui::TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id) +{ + const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); + key_chord = FixupKeyChord(key_chord); + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); // FIXME: Could avoid creating entry. + return routing_data->RoutingCurr == routing_id; +} + +// Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes. +// Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey_A) (>= 1.87) +bool ImGui::IsKeyDown(ImGuiKey key) +{ + return IsKeyDown(key, ImGuiKeyOwner_Any); +} + +bool ImGui::IsKeyDown(ImGuiKey key, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsKeyPressed(ImGuiKey key, bool repeat) +{ + return IsKeyPressed(key, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +// Important: unlike legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat. +bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = key_data->DownDuration; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function! + if (flags & (ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_)) // Setting any _RepeatXXX option enables _Repeat + flags |= ImGuiInputFlags_Repeat; + + bool pressed = (t == 0.0f); + if (!pressed && (flags & ImGuiInputFlags_Repeat) != 0) + { + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(flags, &repeat_delay, &repeat_rate); + pressed = (t > repeat_delay) && GetKeyPressedAmount(key, repeat_delay, repeat_rate) > 0; + if (pressed && (flags & ImGuiInputFlags_RepeatUntilMask_)) + { + // Slightly bias 'key_pressed_time' as DownDuration is an accumulation of DeltaTime which we compare to an absolute time value. + // Ideally we'd replace DownDuration with KeyPressedTime but it would break user's code. + ImGuiContext& g = *GImGui; + double key_pressed_time = g.Time - t + 0.00001f; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChange) && (g.LastKeyModsChangeTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone) && (g.LastKeyModsChangeFromNoneTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilOtherKeyPress) && (g.LastKeyboardKeyPressTime > key_pressed_time)) + pressed = false; + } + } + if (!pressed) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsKeyReleased(ImGuiKey key) +{ + return IsKeyReleased(key, ImGuiKeyOwner_Any); +} + +bool ImGui::IsKeyReleased(ImGuiKey key, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (key_data->DownDurationPrev < 0.0f || key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsMouseDown(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // should be same as IsKeyDown(MouseButtonToKey(button), ImGuiKeyOwner_Any), but this allows legacy code hijacking the io.Mousedown[] array. +} + +bool ImGui::IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyDown(MouseButtonToKey(button), owner_id), but this allows legacy code hijacking the io.Mousedown[] array. +} + +bool ImGui::IsMouseClicked(ImGuiMouseButton button, bool repeat) +{ + return IsMouseClicked(button, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +bool ImGui::IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = g.IO.MouseDownDuration[button]; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsMouseClicked) == 0); // Passing flags not supported by this function! // FIXME: Could support RepeatRate and RepeatUntil flags here. + + const bool repeat = (flags & ImGuiInputFlags_Repeat) != 0; + const bool pressed = (t == 0.0f) || (repeat && t > g.IO.KeyRepeatDelay && CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0); + if (!pressed) + return false; + + if (!TestKeyOwner(MouseButtonToKey(button), owner_id)) + return false; + + return true; +} + +bool ImGui::IsMouseReleased(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // Should be same as IsKeyReleased(MouseButtonToKey(button), ImGuiKeyOwner_Any) +} + +bool ImGui::IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyReleased(MouseButtonToKey(button), owner_id) +} + +// Use if you absolutely need to distinguish single-click from double-click by introducing a delay. +// Generally use with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount == 1' test. +// This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. +bool ImGui::IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + const float time_since_release = (float)(g.Time - g.IO.MouseReleasedTime[button]); + return !IsMouseDown(button) && (time_since_release - g.IO.DeltaTime < delay) && (time_since_release >= delay); +} + +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); +} + +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), owner_id); +} + +int ImGui::GetMouseClickedCount(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button]; +} + +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) +{ + ImGuiContext& g = *GImGui; + + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(g.CurrentWindow->ClipRect); + + // Hit testing, expanded for touch input + if (!rect_clipped.ContainsWithPad(g.IO.MousePos, g.Style.TouchExtraPadding)) + return false; + return true; +} + +// Return if a mouse click/drag went past the given threshold. Valid to call during the MouseReleased frame. +// [Internal] This doesn't test if the button is pressed +bool ImGui::IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; +} + +bool ImGui::IsMouseDragging(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) + return false; + return IsMouseDragPastThreshold(button, lock_threshold); +} + +ImVec2 ImGui::GetMousePos() +{ + ImGuiContext& g = *GImGui; + return g.IO.MousePos; +} + +// This is called TeleportMousePos() and not SetMousePos() to emphasis that setting MousePosPrev will effectively clear mouse delta as well. +// It is expected you only call this if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) is set and supported by backend. +void ImGui::TeleportMousePos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + g.IO.MousePos = g.IO.MousePosPrev = pos; + g.IO.MouseDelta = ImVec2(0.0f, 0.0f); + g.IO.WantSetMousePos = true; + //IMGUI_DEBUG_LOG_IO("TeleportMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); +} + +// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! +ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() +{ + ImGuiContext& g = *GImGui; + if (g.BeginPopupStack.Size > 0) + return g.OpenPopupStack[g.BeginPopupStack.Size - 1].OpenMousePos; + return g.IO.MousePos; +} + +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position. +bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +{ + // The assert is only to silence a false-positive in XCode Static Analysis. + // Because GImGui is not dereferenced in every code path, the static analyzer assume that it may be NULL (which it doesn't for other functions). + IM_ASSERT(GImGui != NULL); + const float MOUSE_INVALID = -256000.0f; + ImVec2 p = mouse_pos ? *mouse_pos : GImGui->IO.MousePos; + return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID; +} + +// [WILL OBSOLETE] This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. +bool ImGui::IsAnyMouseDown() +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; +} + +// Return the delta from the initial clicking position while the mouse button is clicked or was just released. +// This is locked and return 0.0f until the mouse moves past a distance threshold at least once. +// NB: This is only valid if IsMousePosValid(). backends in theory should always keep mouse position valid when dragging even outside the client window. +ImVec2 ImGui::GetMouseDragDelta(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + if (g.IO.MouseDown[button] || g.IO.MouseReleased[button]) + if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) + if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MouseClickedPos[button])) + return g.IO.MousePos - g.IO.MouseClickedPos[button]; + return ImVec2(0.0f, 0.0f); +} + +void ImGui::ResetMouseDragDelta(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr + g.IO.MouseClickedPos[button] = g.IO.MousePos; +} + +// Get desired mouse cursor shape. +// Important: this is meant to be used by a platform backend, it is reset in ImGui::NewFrame(), +// updated during the frame, and locked in EndFrame()/Render(). +// If you use software rendering by setting io.MouseDrawCursor then Dear ImGui will render those for you +ImGuiMouseCursor ImGui::GetMouseCursor() +{ + ImGuiContext& g = *GImGui; + return g.MouseCursor; +} + +// We intentionally accept values of ImGuiMouseCursor that are outside our bounds, in case users needs to hack-in a custom cursor value. +// Custom cursors may be handled by custom backends. If you are using a standard backend and want to hack in a custom cursor, you may +// handle it before the backend _NewFrame() call and temporarily set ImGuiConfigFlags_NoMouseCursorChange during the backend _NewFrame() call. +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +{ + ImGuiContext& g = *GImGui; + g.MouseCursor = cursor_type; +} + +static void UpdateAliasKey(ImGuiKey key, bool v, float analog_value) +{ + IM_ASSERT(ImGui::IsAliasKey(key)); + ImGuiKeyData* key_data = ImGui::GetKeyData(key); + key_data->Down = v; + key_data->AnalogValue = analog_value; +} + +// [Internal] Do not use directly +static ImGuiKeyChord GetMergedModsFromKeys() +{ + ImGuiKeyChord mods = 0; + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) { mods |= ImGuiMod_Ctrl; } + if (ImGui::IsKeyDown(ImGuiMod_Shift)) { mods |= ImGuiMod_Shift; } + if (ImGui::IsKeyDown(ImGuiMod_Alt)) { mods |= ImGuiMod_Alt; } + if (ImGui::IsKeyDown(ImGuiMod_Super)) { mods |= ImGuiMod_Super; } + return mods; +} + +static void ImGui::UpdateKeyboardInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + io.ClearInputKeys(); + + // Update aliases + for (int n = 0; n < ImGuiMouseButton_COUNT; n++) + UpdateAliasKey(MouseButtonToKey(n), io.MouseDown[n], io.MouseDown[n] ? 1.0f : 0.0f); + UpdateAliasKey(ImGuiKey_MouseWheelX, io.MouseWheelH != 0.0f, io.MouseWheelH); + UpdateAliasKey(ImGuiKey_MouseWheelY, io.MouseWheel != 0.0f, io.MouseWheel); + + // Synchronize io.KeyMods and io.KeyCtrl/io.KeyShift/etc. values. + // - New backends (1.87+): send io.AddKeyEvent(ImGuiMod_XXX) -> -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // - Legacy backends: set io.KeyXXX bools -> (above) set key array from io.KeyXXX -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // So with legacy backends the 4 values will do a unnecessary back-and-forth but it makes the code simpler and future facing. + const ImGuiKeyChord prev_key_mods = io.KeyMods; + io.KeyMods = GetMergedModsFromKeys(); + io.KeyCtrl = (io.KeyMods & ImGuiMod_Ctrl) != 0; + io.KeyShift = (io.KeyMods & ImGuiMod_Shift) != 0; + io.KeyAlt = (io.KeyMods & ImGuiMod_Alt) != 0; + io.KeySuper = (io.KeyMods & ImGuiMod_Super) != 0; + if (prev_key_mods != io.KeyMods) + g.LastKeyModsChangeTime = g.Time; + if (prev_key_mods != io.KeyMods && prev_key_mods == 0) + g.LastKeyModsChangeFromNoneTime = g.Time; + + // Clear gamepad data if disabled + if ((io.BackendFlags & ImGuiBackendFlags_HasGamepad) == 0) + for (int key = ImGuiKey_Gamepad_BEGIN; key < ImGuiKey_Gamepad_END; key++) + { + io.KeysData[key - ImGuiKey_NamedKey_BEGIN].Down = false; + io.KeysData[key - ImGuiKey_NamedKey_BEGIN].AnalogValue = 0.0f; + } + + // Update keys + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key++) + { + ImGuiKeyData* key_data = &io.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->DownDurationPrev = key_data->DownDuration; + key_data->DownDuration = key_data->Down ? (key_data->DownDuration < 0.0f ? 0.0f : key_data->DownDuration + io.DeltaTime) : -1.0f; + if (key_data->DownDuration == 0.0f) + { + if (IsKeyboardKey((ImGuiKey)key)) + g.LastKeyboardKeyPressTime = g.Time; + else if (key == ImGuiKey_ReservedForModCtrl || key == ImGuiKey_ReservedForModShift || key == ImGuiKey_ReservedForModAlt || key == ImGuiKey_ReservedForModSuper) + g.LastKeyboardKeyPressTime = g.Time; + } + } + + // Update keys/input owner (named keys only): one entry per key + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &io.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + ImGuiKeyOwnerData* owner_data = &g.KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; + owner_data->OwnerCurr = owner_data->OwnerNext; + if (!key_data->Down) // Important: ownership is released on the frame after a release. Ensure a 'MouseDown -> CloseWindow -> MouseUp' chain doesn't lead to someone else seeing the MouseUp. + owner_data->OwnerNext = ImGuiKeyOwner_NoOwner; + owner_data->LockThisFrame = owner_data->LockUntilRelease = owner_data->LockUntilRelease && key_data->Down; // Clear LockUntilRelease when key is not Down anymore + } + + // Update key routing (for e.g. shortcuts) + UpdateKeyRoutingTable(&g.KeysRoutingTable); +} + +static void ImGui::UpdateMouseInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Mouse Wheel swapping flag + // As a standard behavior holding SHIFT while using Vertical Mouse Wheel triggers Horizontal scroll instead + // - We avoid doing it on OSX as it the OS input layer handles this already. + // - FIXME: However this means when running on OSX over Emscripten, Shift+WheelY will incur two swapping (1 in OS, 1 here), canceling the feature. + // - FIXME: When we can distinguish e.g. touchpad scroll events from mouse ones, we'll set this accordingly based on input source. + io.MouseWheelRequestAxisSwap = io.KeyShift && !io.ConfigMacOSXBehaviors; + + // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) + if (IsMousePosValid(&io.MousePos)) + io.MousePos = g.MouseLastValidPos = ImFloor(io.MousePos); + + // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta + if (IsMousePosValid(&io.MousePos) && IsMousePosValid(&io.MousePosPrev)) + io.MouseDelta = io.MousePos - io.MousePosPrev; + else + io.MouseDelta = ImVec2(0.0f, 0.0f); + + // Update stationary timer. + // FIXME: May need to rework again to have some tolerance for occasional small movement, while being functional on high-framerates. + const float mouse_stationary_threshold = (io.MouseSource == ImGuiMouseSource_Mouse) ? 2.0f : 3.0f; // Slightly higher threshold for ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen, may need rework. + const bool mouse_stationary = (ImLengthSqr(io.MouseDelta) <= mouse_stationary_threshold * mouse_stationary_threshold); + g.MouseStationaryTimer = mouse_stationary ? (g.MouseStationaryTimer + io.DeltaTime) : 0.0f; + //IMGUI_DEBUG_LOG("%.4f\n", g.MouseStationaryTimer); + + // If mouse moved we re-enable mouse hovering in case it was disabled by keyboard/gamepad. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true. + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) + g.NavHighlightItemUnderNav = false; + + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f; + io.MouseClickedCount[i] = 0; // Will be filled below + io.MouseReleased[i] = !io.MouseDown[i] && io.MouseDownDuration[i] >= 0.0f; + if (io.MouseReleased[i]) + io.MouseReleasedTime[i] = g.Time; + io.MouseDownDurationPrev[i] = io.MouseDownDuration[i]; + io.MouseDownDuration[i] = io.MouseDown[i] ? (io.MouseDownDuration[i] < 0.0f ? 0.0f : io.MouseDownDuration[i] + io.DeltaTime) : -1.0f; + if (io.MouseClicked[i]) + { + bool is_repeated_click = false; + if ((float)(g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime) + { + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + if (ImLengthSqr(delta_from_click_pos) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist) + is_repeated_click = true; + } + if (is_repeated_click) + io.MouseClickedLastCount[i]++; + else + io.MouseClickedLastCount[i] = 1; + io.MouseClickedTime[i] = g.Time; + io.MouseClickedPos[i] = io.MousePos; + io.MouseClickedCount[i] = io.MouseClickedLastCount[i]; + io.MouseDragMaxDistanceSqr[i] = 0.0f; + } + else if (io.MouseDown[i]) + { + // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold + float delta_sqr_click_pos = IsMousePosValid(&io.MousePos) ? ImLengthSqr(io.MousePos - io.MouseClickedPos[i]) : 0.0f; + io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], delta_sqr_click_pos); + } + + // We provide io.MouseDoubleClicked[] as a legacy service + io.MouseDoubleClicked[i] = (io.MouseClickedCount[i] == 2); + + // Clicking any mouse button reactivate mouse hovering which may have been deactivated by keyboard/gamepad navigation + if (io.MouseClicked[i]) + g.NavHighlightItemUnderNav = false; + } +} + +static void LockWheelingWindow(ImGuiWindow* window, float wheel_amount) +{ + ImGuiContext& g = *GImGui; + if (window) + g.WheelingWindowReleaseTimer = ImMin(g.WheelingWindowReleaseTimer + ImAbs(wheel_amount) * WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER, WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER); + else + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindow == window) + return; + IMGUI_DEBUG_LOG_IO("[io] LockWheelingWindow() \"%s\"\n", window ? window->Name : "NULL"); + g.WheelingWindow = window; + g.WheelingWindowRefMousePos = g.IO.MousePos; + if (window == NULL) + { + g.WheelingWindowStartFrame = -1; + g.WheelingAxisAvg = ImVec2(0.0f, 0.0f); + } +} + +static ImGuiWindow* FindBestWheelingWindow(const ImVec2& wheel) +{ + // For each axis, find window in the hierarchy that may want to use scrolling + ImGuiContext& g = *GImGui; + ImGuiWindow* windows[2] = { NULL, NULL }; + for (int axis = 0; axis < 2; axis++) + if (wheel[axis] != 0.0f) + for (ImGuiWindow* window = windows[axis] = g.HoveredWindow; window->Flags & ImGuiWindowFlags_ChildWindow; window = windows[axis] = window->ParentWindow) + { + // Bubble up into parent window if: + // - a child window doesn't allow any scrolling. + // - a child window has the ImGuiWindowFlags_NoScrollWithMouse flag. + //// - a child window doesn't need scrolling because it is already at the edge for the direction we are going in (FIXME-WIP) + const bool has_scrolling = (window->ScrollMax[axis] != 0.0f); + const bool inputs_disabled = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs); + //const bool scrolling_past_limits = (wheel_v < 0.0f) ? (window->Scroll[axis] <= 0.0f) : (window->Scroll[axis] >= window->ScrollMax[axis]); + if (has_scrolling && !inputs_disabled) // && !scrolling_past_limits) + break; // select this window + } + if (windows[0] == NULL && windows[1] == NULL) + return NULL; + + // If there's only one window or only one axis then there's no ambiguity + if (windows[0] == windows[1] || windows[0] == NULL || windows[1] == NULL) + return windows[1] ? windows[1] : windows[0]; + + // If candidate are different windows we need to decide which one to prioritize + // - First frame: only find a winner if one axis is zero. + // - Subsequent frames: only find a winner when one is more than the other. + if (g.WheelingWindowStartFrame == -1) + g.WheelingWindowStartFrame = g.FrameCount; + if ((g.WheelingWindowStartFrame == g.FrameCount && wheel.x != 0.0f && wheel.y != 0.0f) || (g.WheelingAxisAvg.x == g.WheelingAxisAvg.y)) + { + g.WheelingWindowWheelRemainder = wheel; + return NULL; + } + return (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? windows[0] : windows[1]; +} + +// Called by NewFrame() +void ImGui::UpdateMouseWheel() +{ + // Reset the locked window if we move the mouse or after the timer elapses. + // FIXME: Ideally we could refactor to have one timer for "changing window w/ same axis" and a shorter timer for "changing window or axis w/ other axis" (#3795) + ImGuiContext& g = *GImGui; + if (g.WheelingWindow != NULL) + { + g.WheelingWindowReleaseTimer -= g.IO.DeltaTime; + if (IsMousePosValid() && ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindowReleaseTimer <= 0.0f) + LockWheelingWindow(NULL, 0.0f); + } + + ImVec2 wheel; + wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheelH : 0.0f; + wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheel : 0.0f; + + //IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y); + ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; + if (!mouse_window || mouse_window->Collapsed) + return; + + // Zoom / Scale window + // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. + if (wheel.y != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) + { + LockWheelingWindow(mouse_window, wheel.y); + ImGuiWindow* window = mouse_window; + const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); + const float scale = new_font_scale / window->FontWindowScale; + window->FontWindowScale = new_font_scale; + if (window == window->RootWindow) + { + const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; + SetWindowPos(window, window->Pos + offset, 0); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + } + return; + } + if (g.IO.KeyCtrl) + return; + + // Mouse wheel scrolling + // Read about io.MouseWheelRequestAxisSwap and its issue on Mac+Emscripten in UpdateMouseInputs() + if (g.IO.MouseWheelRequestAxisSwap) + wheel = ImVec2(wheel.y, 0.0f); + + // Maintain a rough average of moving magnitude on both axes + // FIXME: should by based on wall clock time rather than frame-counter + g.WheelingAxisAvg.x = ImExponentialMovingAverage(g.WheelingAxisAvg.x, ImAbs(wheel.x), 30); + g.WheelingAxisAvg.y = ImExponentialMovingAverage(g.WheelingAxisAvg.y, ImAbs(wheel.y), 30); + + // In the rare situation where FindBestWheelingWindow() had to defer first frame of wheeling due to ambiguous main axis, reinject it now. + wheel += g.WheelingWindowWheelRemainder; + g.WheelingWindowWheelRemainder = ImVec2(0.0f, 0.0f); + if (wheel.x == 0.0f && wheel.y == 0.0f) + return; + + // Mouse wheel scrolling: find target and apply + // - don't renew lock if axis doesn't apply on the window. + // - select a main axis when both axes are being moved. + if (ImGuiWindow* window = (g.WheelingWindow ? g.WheelingWindow : FindBestWheelingWindow(wheel))) + if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) + { + bool do_scroll[2] = { wheel.x != 0.0f && window->ScrollMax.x != 0.0f, wheel.y != 0.0f && window->ScrollMax.y != 0.0f }; + if (do_scroll[ImGuiAxis_X] && do_scroll[ImGuiAxis_Y]) + do_scroll[(g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? ImGuiAxis_Y : ImGuiAxis_X] = false; + if (do_scroll[ImGuiAxis_X]) + { + LockWheelingWindow(window, wheel.x); + float max_step = window->InnerRect.GetWidth() * 0.67f; + float scroll_step = ImTrunc(ImMin(2 * window->FontRefSize, max_step)); + SetScrollX(window, window->Scroll.x - wheel.x * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + if (do_scroll[ImGuiAxis_Y]) + { + LockWheelingWindow(window, wheel.y); + float max_step = window->InnerRect.GetHeight() * 0.67f; + float scroll_step = ImTrunc(ImMin(5 * window->FontRefSize, max_step)); + SetScrollY(window, window->Scroll.y - wheel.y * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + } +} + +void ImGui::SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureKeyboardNextFrame = want_capture_keyboard ? 1 : 0; +} + +void ImGui::SetNextFrameWantCaptureMouse(bool want_capture_mouse) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureMouseNextFrame = want_capture_mouse ? 1 : 0; +} + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +static const char* GetInputSourceName(ImGuiInputSource source) +{ + const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad" }; + IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT); + return input_source_names[source]; +} +static const char* GetMouseSourceName(ImGuiMouseSource source) +{ + const char* mouse_source_names[] = { "Mouse", "TouchScreen", "Pen" }; + IM_ASSERT(IM_ARRAYSIZE(mouse_source_names) == ImGuiMouseSource_COUNT && source >= 0 && source < ImGuiMouseSource_COUNT); + return mouse_source_names[source]; +} +static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e) +{ + ImGuiContext& g = *GImGui; + if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (%.1f, %.1f) (%s)\n", prefix, e->MousePos.PosX, e->MousePos.PosY, GetMouseSourceName(e->MousePos.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseButton %d %s (%s)\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up", GetMouseSourceName(e->MouseButton.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseWheel (%.3f, %.3f) (%s)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY, GetMouseSourceName(e->MouseWheel.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_Key) { IMGUI_DEBUG_LOG_IO("[io] %s: Key \"%s\" %s\n", prefix, ImGui::GetKeyName(e->Key.Key), e->Key.Down ? "Down" : "Up"); return; } + if (e->Type == ImGuiInputEventType_Text) { IMGUI_DEBUG_LOG_IO("[io] %s: Text: %c (U+%08X)\n", prefix, e->Text.Char, e->Text.Char); return; } + if (e->Type == ImGuiInputEventType_Focus) { IMGUI_DEBUG_LOG_IO("[io] %s: AppFocused %d\n", prefix, e->AppFocused.Focused); return; } +} +#endif + +// Process input queue +// We always call this with the value of 'bool g.IO.ConfigInputTrickleEventQueue'. +// - trickle_fast_inputs = false : process all events, turn into flattened input state (e.g. successive down/up/down/up will be lost) +// - trickle_fast_inputs = true : process as many events as possible (successive down/up/down/up will be trickled over several frames so nothing is lost) (new feature in 1.87) +void ImGui::UpdateInputEvents(bool trickle_fast_inputs) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Only trickle chars<>key when working with InputText() + // FIXME: InputText() could parse event trail? + // FIXME: Could specialize chars<>keys trickling rules for control keys (those not typically associated to characters) + const bool trickle_interleaved_nonchar_keys_and_text = (trickle_fast_inputs && g.WantTextInputNextFrame == 1); + + bool mouse_moved = false, mouse_wheeled = false, key_changed = false, key_changed_nonchar = false, text_inputted = false; + int mouse_button_changed = 0x00; + ImBitArray key_changed_mask; + + int event_n = 0; + for (; event_n < g.InputEventsQueue.Size; event_n++) + { + ImGuiInputEvent* e = &g.InputEventsQueue[event_n]; + if (e->Type == ImGuiInputEventType_MousePos) + { + if (g.IO.WantSetMousePos) + continue; + // Trickling Rule: Stop processing queued events if we already handled a mouse button change + ImVec2 event_pos(e->MousePos.PosX, e->MousePos.PosY); + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_wheeled || key_changed || text_inputted)) + break; + io.MousePos = event_pos; + io.MouseSource = e->MousePos.MouseSource; + mouse_moved = true; + } + else if (e->Type == ImGuiInputEventType_MouseButton) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + const ImGuiMouseButton button = e->MouseButton.Button; + IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); + if (trickle_fast_inputs && ((mouse_button_changed & (1 << button)) || mouse_wheeled)) + break; + if (trickle_fast_inputs && e->MouseButton.MouseSource == ImGuiMouseSource_TouchScreen && mouse_moved) // #2702: TouchScreen have no initial hover. + break; + io.MouseDown[button] = e->MouseButton.Down; + io.MouseSource = e->MouseButton.MouseSource; + mouse_button_changed |= (1 << button); + } + else if (e->Type == ImGuiInputEventType_MouseWheel) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the event + if (trickle_fast_inputs && (mouse_moved || mouse_button_changed != 0)) + break; + io.MouseWheelH += e->MouseWheel.WheelX; + io.MouseWheel += e->MouseWheel.WheelY; + io.MouseSource = e->MouseWheel.MouseSource; + mouse_wheeled = true; + } + else if (e->Type == ImGuiInputEventType_Key) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + ImGuiKey key = e->Key.Key; + IM_ASSERT(key != ImGuiKey_None); + ImGuiKeyData* key_data = GetKeyData(key); + const int key_data_index = (int)(key_data - g.IO.KeysData); + if (trickle_fast_inputs && key_data->Down != e->Key.Down && (key_changed_mask.TestBit(key_data_index) || mouse_button_changed != 0)) + break; + + const bool key_is_potentially_for_char_input = IsKeyChordPotentiallyCharInput(GetMergedModsFromKeys() | key); + if (trickle_interleaved_nonchar_keys_and_text && (text_inputted && !key_is_potentially_for_char_input)) + break; + + key_data->Down = e->Key.Down; + key_data->AnalogValue = e->Key.AnalogValue; + key_changed = true; + key_changed_mask.SetBit(key_data_index); + if (trickle_interleaved_nonchar_keys_and_text && !key_is_potentially_for_char_input) + key_changed_nonchar = true; + } + else if (e->Type == ImGuiInputEventType_Text) + { + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + // Trickling Rule: Stop processing queued events if keys/mouse have been interacted with + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_moved || mouse_wheeled)) + break; + if (trickle_interleaved_nonchar_keys_and_text && key_changed_nonchar) + break; + unsigned int c = e->Text.Char; + io.InputQueueCharacters.push_back(c <= IM_UNICODE_CODEPOINT_MAX ? (ImWchar)c : IM_UNICODE_CODEPOINT_INVALID); + if (trickle_interleaved_nonchar_keys_and_text) + text_inputted = true; + } + else if (e->Type == ImGuiInputEventType_Focus) + { + // We intentionally overwrite this and process in NewFrame(), in order to give a chance + // to multi-viewports backends to queue AddFocusEvent(false) + AddFocusEvent(true) in same frame. + const bool focus_lost = !e->AppFocused.Focused; + io.AppFocusLost = focus_lost; + } + else + { + IM_ASSERT(0 && "Unknown event!"); + } + } + + // Record trail (for domain-specific applications wanting to access a precise trail) + //if (event_n != 0) IMGUI_DEBUG_LOG_IO("Processed: %d / Remaining: %d\n", event_n, g.InputEventsQueue.Size - event_n); + for (int n = 0; n < event_n; n++) + g.InputEventsTrail.push_back(g.InputEventsQueue[n]); + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (event_n != 0 && (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO)) + for (int n = 0; n < g.InputEventsQueue.Size; n++) + DebugPrintInputEvent(n < event_n ? "Processed" : "Remaining", &g.InputEventsQueue[n]); +#endif + + // Remaining events will be processed on the next frame + if (event_n == g.InputEventsQueue.Size) + g.InputEventsQueue.resize(0); + else + g.InputEventsQueue.erase(g.InputEventsQueue.Data, g.InputEventsQueue.Data + event_n); + + // Clear buttons state when focus is lost + // - this is useful so e.g. releasing Alt after focus loss on Alt-Tab doesn't trigger the Alt menu toggle. + // - we clear in EndFrame() and not now in order allow application/user code polling this flag + // (e.g. custom backend may want to clear additional data, custom widgets may want to react with a "canceling" event). + if (g.IO.AppFocusLost) + { + g.IO.ClearInputKeys(); + g.IO.ClearInputMouse(); + } +} + +ImGuiID ImGui::GetKeyOwner(ImGuiKey key) +{ + if (!IsNamedKeyOrMod(key)) + return ImGuiKeyOwner_NoOwner; + + ImGuiContext& g = *GImGui; + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + ImGuiID owner_id = owner_data->OwnerCurr; + + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return ImGuiKeyOwner_NoOwner; + + return owner_id; +} + +// TestKeyOwner(..., ID) : (owner == None || owner == ID) +// TestKeyOwner(..., None) : (owner == None) +// TestKeyOwner(..., Any) : no owner test +// All paths are also testing for key not being locked, for the rare cases that key have been locked with using ImGuiInputFlags_LockXXX flags. +bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id) +{ + if (!IsNamedKeyOrMod(key)) + return true; + + ImGuiContext& g = *GImGui; + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_id == ImGuiKeyOwner_Any) + return (owner_data->LockThisFrame == false); + + // Note: SetKeyOwner() sets OwnerCurr. It is not strictly required for most mouse routing overlap (because of ActiveId/HoveredId + // are acting as filter before this has a chance to filter), but sane as soon as user tries to look into things. + // Setting OwnerCurr in SetKeyOwner() is more consistent than testing OwnerNext here: would be inconsistent with getter and other functions. + if (owner_data->OwnerCurr != owner_id) + { + if (owner_data->LockThisFrame) + return false; + if (owner_data->OwnerCurr != ImGuiKeyOwner_NoOwner) + return false; + } + + return true; +} + +// _LockXXX flags are useful to lock keys away from code which is not input-owner aware. +// When using _LockXXX flags, you can use ImGuiKeyOwner_Any to lock keys from everyone. +// - SetKeyOwner(..., None) : clears owner +// - SetKeyOwner(..., Any, !Lock) : illegal (assert) +// - SetKeyOwner(..., Any or None, Lock) : set lock +void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(IsNamedKeyOrMod(key) && (owner_id != ImGuiKeyOwner_Any || (flags & (ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease)))); // Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it) + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetKeyOwner) == 0); // Passing flags not supported by this function! + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X, flags=%08X)\n", GetKeyName(key), owner_id, flags); + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + owner_data->OwnerCurr = owner_data->OwnerNext = owner_id; + + // We cannot lock by default as it would likely break lots of legacy code. + // In the case of using LockUntilRelease while key is not down we still lock during the frame (no key_data->Down test) + owner_data->LockUntilRelease = (flags & ImGuiInputFlags_LockUntilRelease) != 0; + owner_data->LockThisFrame = (flags & ImGuiInputFlags_LockThisFrame) != 0 || (owner_data->LockUntilRelease); +} + +// Rarely used helper +void ImGui::SetKeyOwnersForKeyChord(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) +{ + if (key_chord & ImGuiMod_Ctrl) { SetKeyOwner(ImGuiMod_Ctrl, owner_id, flags); } + if (key_chord & ImGuiMod_Shift) { SetKeyOwner(ImGuiMod_Shift, owner_id, flags); } + if (key_chord & ImGuiMod_Alt) { SetKeyOwner(ImGuiMod_Alt, owner_id, flags); } + if (key_chord & ImGuiMod_Super) { SetKeyOwner(ImGuiMod_Super, owner_id, flags); } + if (key_chord & ~ImGuiMod_Mask_) { SetKeyOwner((ImGuiKey)(key_chord & ~ImGuiMod_Mask_), owner_id, flags); } +} + +// This is more or less equivalent to: +// if (IsItemHovered() || IsItemActive()) +// SetKeyOwner(key, GetItemID()); +// Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times. +// More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition. +// Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority. +void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (id == 0 || (g.HoveredId != id && g.ActiveId != id)) + return; + if ((flags & ImGuiInputFlags_CondMask_) == 0) + flags |= ImGuiInputFlags_CondDefault_; + if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive))) + { + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function! + SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_); + } +} + +void ImGui::SetItemKeyOwner(ImGuiKey key) +{ + SetItemKeyOwner(key, ImGuiInputFlags_None); +} + +// This is the only public API until we expose owner_id versions of the API as replacements. +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord) +{ + return IsKeyChordPressed(key_chord, ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +// This is equivalent to comparing KeyMods + doing a IsKeyPressed() +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + key_chord = FixupKeyChord(key_chord); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (g.IO.KeyMods != mods) + return false; + + // Special storage location for mods + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + if (!IsKeyPressed(key, (flags & ImGuiInputFlags_RepeatMask_), owner_id)) + return false; + return true; +} + +void ImGui::SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasShortcut; + g.NextItemData.Shortcut = key_chord; + g.NextItemData.ShortcutFlags = flags; +} + +// Called from within ItemAdd: at this point we can read from NextItemData and write to LastItemData +void ImGui::ItemHandleShortcut(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiInputFlags flags = g.NextItemData.ShortcutFlags; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetNextItemShortcut) == 0); // Passing flags not supported by SetNextItemShortcut()! + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) + return; + if (flags & ImGuiInputFlags_Tooltip) + { + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasShortcut; + g.LastItemData.Shortcut = g.NextItemData.Shortcut; + } + if (!Shortcut(g.NextItemData.Shortcut, flags & ImGuiInputFlags_SupportedByShortcut, id) || g.NavActivateId != 0) + return; + + // FIXME: Generalize Activation queue? + g.NavActivateId = id; // Will effectively disable clipping. + g.NavActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_FromShortcut; + //if (g.ActiveId == 0 || g.ActiveId == id) + g.NavActivateDownId = g.NavActivatePressedId = id; + NavHighlightActivated(id); +} + +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) +{ + return Shortcut(key_chord, flags, ImGuiKeyOwner_Any); +} + +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + //IMGUI_DEBUG_LOG("Shortcut(%s, flags=%X, owner_id=0x%08X)\n", GetKeyChordName(key_chord, g.TempBuffer.Data, g.TempBuffer.Size), flags, owner_id); + + // When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any. + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteFocused; + + // Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default) + // Effectively makes Shortcut() always input-owner aware. + if (owner_id == ImGuiKeyOwner_Any || owner_id == ImGuiKeyOwner_NoOwner) + owner_id = GetRoutingIdFromOwnerId(owner_id); + + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + return false; + + // Submit route + if (!SetShortcutRouting(key_chord, flags, owner_id)) + return false; + + // Default repeat behavior for Shortcut() + // So e.g. pressing Ctrl+W and releasing Ctrl while holding W will not trigger the W shortcut. + if ((flags & ImGuiInputFlags_Repeat) != 0 && (flags & ImGuiInputFlags_RepeatUntilMask_) == 0) + flags |= ImGuiInputFlags_RepeatUntilKeyModsChange; + + if (!IsKeyChordPressed(key_chord, flags, owner_id)) + return false; + + // Claim mods during the press + SetKeyOwnersForKeyChord(key_chord & ImGuiMod_Mask_, owner_id); + + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByShortcut) == 0); // Passing flags not supported by this function! + return true; +} + +//----------------------------------------------------------------------------- +// [SECTION] ERROR CHECKING, STATE RECOVERY +//----------------------------------------------------------------------------- +// - DebugCheckVersionAndDataLayout() (called via IMGUI_CHECKVERSION() macros) +// - ErrorCheckUsingSetCursorPosToExtendParentBoundaries() +// - ErrorCheckNewFrameSanityChecks() +// - ErrorCheckEndFrameSanityChecks() +// - ErrorRecoveryStoreState() +// - ErrorRecoveryTryToRecoverState() +// - ErrorRecoveryTryToRecoverWindowState() +// - ErrorLog() +//----------------------------------------------------------------------------- + +// Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues. +// Called by IMGUI_CHECKVERSION(). +// Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit +// If this triggers you have mismatched headers and compiled code versions. +// - It could be because of a build issue (using new headers with old compiled code) +// - It could be because of mismatched configuration #define, compilation settings, packing pragma etc. +// THE CONFIGURATION SETTINGS MENTIONED IN imconfig.h MUST BE SET FOR ALL COMPILATION UNITS INVOLVED WITH DEAR IMGUI. +// Which is why it is required you put them in your imconfig file (and NOT only before including imgui.h). +// Otherwise it is possible that different compilation units would see different structure layout. +// If you don't want to modify imconfig.h you can use the IMGUI_USER_CONFIG define to change filename. +bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) +{ + bool error = false; + if (strcmp(version, IMGUI_VERSION) != 0) { error = true; IM_ASSERT(strcmp(version, IMGUI_VERSION) == 0 && "Mismatched version string!"); } + if (sz_io != sizeof(ImGuiIO)) { error = true; IM_ASSERT(sz_io == sizeof(ImGuiIO) && "Mismatched struct layout!"); } + if (sz_style != sizeof(ImGuiStyle)) { error = true; IM_ASSERT(sz_style == sizeof(ImGuiStyle) && "Mismatched struct layout!"); } + if (sz_vec2 != sizeof(ImVec2)) { error = true; IM_ASSERT(sz_vec2 == sizeof(ImVec2) && "Mismatched struct layout!"); } + if (sz_vec4 != sizeof(ImVec4)) { error = true; IM_ASSERT(sz_vec4 == sizeof(ImVec4) && "Mismatched struct layout!"); } + if (sz_vert != sizeof(ImDrawVert)) { error = true; IM_ASSERT(sz_vert == sizeof(ImDrawVert) && "Mismatched struct layout!"); } + if (sz_idx != sizeof(ImDrawIdx)) { error = true; IM_ASSERT(sz_idx == sizeof(ImDrawIdx) && "Mismatched struct layout!"); } + return !error; +} + +// Until 1.89 (IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos() to extend the boundary of a parent (e.g. window or table cell) +// This is causing issues and ambiguity and we need to retire that. +// See https://github.com/ocornut/imgui/issues/5548 for more details. +// [Scenario 1] +// Previously this would make the window content size ~200x200: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK +// Instead, please submit an item: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); // OK +// Alternative: +// Begin(...) + Dummy(ImVec2(200,200)) + End(); // OK +// [Scenario 2] +// For reference this is one of the issue what we aim to fix with this change: +// BeginGroup() + SomeItem("foobar") + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup() +// The previous logic made SetCursorScreenPos(GetCursorScreenPos()) have a side-effect! It would erroneously incorporate ItemSpacing.y after the item into content size, making the group taller! +// While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect. Using vertical alignment patterns could trigger this issue. +void ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->DC.IsSetPos); + window->DC.IsSetPos = false; +#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window->DC.CursorPos.x <= window->DC.CursorMaxPos.x && window->DC.CursorPos.y <= window->DC.CursorMaxPos.y) + return; + if (window->SkipItems) + return; + IM_ASSERT(0 && "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries. Please submit an item e.g. Dummy() to validate extent."); +#else + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +#endif +} + +static void ImGui::ErrorCheckNewFrameSanityChecks() +{ + ImGuiContext& g = *GImGui; + + // Check user IM_ASSERT macro + // (IF YOU GET A WARNING OR COMPILE ERROR HERE: it means your assert macro is incorrectly defined! + // If your macro uses multiple statements, it NEEDS to be surrounded by a 'do { ... } while (0)' block. + // This is a common C/C++ idiom to allow multiple statements macros to be used in control flow blocks.) + // #define IM_ASSERT(EXPR) if (SomeCode(EXPR)) SomeMoreCode(); // Wrong! + // #define IM_ASSERT(EXPR) do { if (SomeCode(EXPR)) SomeMoreCode(); } while (0) // Correct! + if (true) IM_ASSERT(1); else IM_ASSERT(0); + + // Emscripten backends are often imprecise in their submission of DeltaTime. (#6114, #3644) + // Ideally the Emscripten app/backend should aim to fix or smooth this value and avoid feeding zero, but we tolerate it. +#ifdef __EMSCRIPTEN__ + if (g.IO.DeltaTime <= 0.0f && g.FrameCount > 0) + g.IO.DeltaTime = 0.00001f; +#endif + + // Check user data + // (We pass an error message in the assert expression to make it visible to programmers who are not using a debugger, as most assert handlers display their argument) + IM_ASSERT(g.Initialized); + IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!"); + IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); + IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value!"); + IM_ASSERT(g.IO.Fonts->IsBuilt() && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()"); + IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations + IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.WindowBorderHoverPadding > 0.0f && "Invalid style setting!"); // Required otherwise cannot resize from borders. + IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); + IM_ASSERT(g.Style.ColorButtonPosition == ImGuiDir_Left || g.Style.ColorButtonPosition == ImGuiDir_Right); + + // Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way. + if (g.IO.ConfigErrorRecovery) + IM_ASSERT(g.IO.ConfigErrorRecoveryEnableAssert || g.IO.ConfigErrorRecoveryEnableDebugLog || g.IO.ConfigErrorRecoveryEnableTooltip || g.ErrorCallback != NULL); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Remap legacy names + if (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) + { + g.IO.ConfigNavMoveSetMousePos = true; + g.IO.ConfigFlags &= ~ImGuiConfigFlags_NavEnableSetMousePos; + } + if (g.IO.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) + { + g.IO.ConfigNavCaptureKeyboard = false; + g.IO.ConfigFlags &= ~ImGuiConfigFlags_NavNoCaptureKeyboard; + } + + // Remap legacy clipboard handlers (OBSOLETED in 1.91.1, August 2024) + if (g.IO.GetClipboardTextFn != NULL && (g.PlatformIO.Platform_GetClipboardTextFn == NULL || g.PlatformIO.Platform_GetClipboardTextFn == Platform_GetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_GetClipboardTextFn = [](ImGuiContext* ctx) { return ctx->IO.GetClipboardTextFn(ctx->IO.ClipboardUserData); }; + if (g.IO.SetClipboardTextFn != NULL && (g.PlatformIO.Platform_SetClipboardTextFn == NULL || g.PlatformIO.Platform_SetClipboardTextFn == Platform_SetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_SetClipboardTextFn = [](ImGuiContext* ctx, const char* text) { return ctx->IO.SetClipboardTextFn(ctx->IO.ClipboardUserData, text); }; +#endif +} + +static void ImGui::ErrorCheckEndFrameSanityChecks() +{ + // Verify that io.KeyXXX fields haven't been tampered with. Key mods should not be modified between NewFrame() and EndFrame() + // One possible reason leading to this assert is that your backends update inputs _AFTER_ NewFrame(). + // It is known that when some modal native windows called mid-frame takes focus away, some backends such as GLFW will + // send key release events mid-frame. This would normally trigger this assertion and lead to sheared inputs. + // We silently accommodate for this case by ignoring the case where all io.KeyXXX modifiers were released (aka key_mod_flags == 0), + // while still correctly asserting on mid-frame key press events. + ImGuiContext& g = *GImGui; + const ImGuiKeyChord key_mods = GetMergedModsFromKeys(); + IM_UNUSED(g); + IM_UNUSED(key_mods); + IM_ASSERT((key_mods == 0 || g.IO.KeyMods == key_mods) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); + IM_UNUSED(key_mods); + + IM_ASSERT(g.CurrentWindowStack.Size == 1); + IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow); +} + +// Save current stack sizes. Called e.g. by NewFrame() and by Begin() but may be called for manual recovery. +void ImGui::ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out) +{ + ImGuiContext& g = *GImGui; + state_out->SizeOfWindowStack = (short)g.CurrentWindowStack.Size; + state_out->SizeOfIDStack = (short)g.CurrentWindow->IDStack.Size; + state_out->SizeOfTreeStack = (short)g.CurrentWindow->DC.TreeDepth; // NOT g.TreeNodeStack.Size which is a partial stack! + state_out->SizeOfColorStack = (short)g.ColorStack.Size; + state_out->SizeOfStyleVarStack = (short)g.StyleVarStack.Size; + state_out->SizeOfFontStack = (short)g.FontStack.Size; + state_out->SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; + state_out->SizeOfGroupStack = (short)g.GroupStack.Size; + state_out->SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; + state_out->SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; + state_out->SizeOfDisabledStack = (short)g.DisabledStackSize; +} + +// Chosen name "Try to recover" over e.g. "Restore" to suggest this is not a 100% guaranteed recovery. +// Called by e.g. EndFrame() but may be called for manual recovery. +// Attempt to recover full window stack. +void ImGui::ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in) +{ + // PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations" + ImGuiContext& g = *GImGui; + while (g.CurrentWindowStack.Size > state_in->SizeOfWindowStack) //-V1044 + { + // Recap: + // - Begin()/BeginChild() return false to indicate the window is collapsed or fully clipped. + // - Always call a matching End() for each Begin() call, regardless of its return value! + // - Begin/End and BeginChild/EndChild logic is KNOWN TO BE INCONSISTENT WITH ALL OTHER BEGIN/END FUNCTIONS. + // - We will fix that in a future major update. + ImGuiWindow* window = g.CurrentWindow; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + { + if (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow) + { + IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); + EndTable(); + } + else + { + IM_ASSERT_USER_ERROR(0, "Missing EndChild()"); + EndChild(); + } + } + else + { + IM_ASSERT_USER_ERROR(0, "Missing End()"); + End(); + } + } + if (g.CurrentWindowStack.Size == state_in->SizeOfWindowStack) + ErrorRecoveryTryToRecoverWindowState(state_in); +} + +// Called by e.g. End() but may be called for manual recovery. +// Read '// Error Handling [BETA]' block in imgui_internal.h for details. +// Attempt to recover from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. +void ImGui::ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in) +{ + ImGuiContext& g = *GImGui; + + while (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); + EndTable(); + } + + ImGuiWindow* window = g.CurrentWindow; + + // FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. + while (g.CurrentTabBar != NULL && g.CurrentTabBar->Window == window) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndTabBar()"); + EndTabBar(); + } + while (g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Storage->Window == window) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndMultiSelect()"); + EndMultiSelect(); + } + if (window->DC.MenuBarAppending) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndMenuBar()"); + EndMenuBar(); + } + while (window->DC.TreeDepth > state_in->SizeOfTreeStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing TreePop()"); + TreePop(); + } + while (g.GroupStack.Size > state_in->SizeOfGroupStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndGroup()"); + EndGroup(); + } + IM_ASSERT(g.GroupStack.Size == state_in->SizeOfGroupStack); + while (window->IDStack.Size > state_in->SizeOfIDStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopID()"); + PopID(); + } + while (g.DisabledStackSize > state_in->SizeOfDisabledStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndDisabled()"); + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + EndDisabled(); + else + { + EndDisabledOverrideReenable(); + g.CurrentWindowStack.back().DisabledOverrideReenable = false; + } + } + IM_ASSERT(g.DisabledStackSize == state_in->SizeOfDisabledStack); + while (g.ColorStack.Size > state_in->SizeOfColorStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopStyleColor()"); + PopStyleColor(); + } + while (g.ItemFlagsStack.Size > state_in->SizeOfItemFlagsStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopItemFlag()"); + PopItemFlag(); + } + while (g.StyleVarStack.Size > state_in->SizeOfStyleVarStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopStyleVar()"); + PopStyleVar(); + } + while (g.FontStack.Size > state_in->SizeOfFontStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopFont()"); + PopFont(); + } + while (g.FocusScopeStack.Size > state_in->SizeOfFocusScopeStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopFocusScope()"); + PopFocusScope(); + } + //IM_ASSERT(g.FocusScopeStack.Size == state_in->SizeOfFocusScopeStack); +} + +bool ImGui::ErrorLog(const char* msg) +{ + ImGuiContext& g = *GImGui; + + // Output to debug log +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiWindow* window = g.CurrentWindow; + + if (g.IO.ConfigErrorRecoveryEnableDebugLog) + { + if (g.ErrorFirst) + IMGUI_DEBUG_LOG_ERROR("[imgui-error] (current settings: Assert=%d, Log=%d, Tooltip=%d)\n", + g.IO.ConfigErrorRecoveryEnableAssert, g.IO.ConfigErrorRecoveryEnableDebugLog, g.IO.ConfigErrorRecoveryEnableTooltip); + IMGUI_DEBUG_LOG_ERROR("[imgui-error] In window '%s': %s\n", window ? window->Name : "NULL", msg); + } + g.ErrorFirst = false; + + // Output to tooltip + if (g.IO.ConfigErrorRecoveryEnableTooltip) + { + if (g.WithinFrameScope && BeginErrorTooltip()) + { + if (g.ErrorCountCurrentFrame < 20) + { + Text("In window '%s': %s", window ? window->Name : "NULL", msg); + if (window && (!window->IsFallbackWindow || window->WasActive)) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 0, 0, 255)); + } + if (g.ErrorCountCurrentFrame == 20) + Text("(and more errors)"); + // EndFrame() will amend debug buttons to this window, after all errors have been submitted. + EndErrorTooltip(); + } + g.ErrorCountCurrentFrame++; + } +#endif + + // Output to callback + if (g.ErrorCallback != NULL) + g.ErrorCallback(&g, g.ErrorCallbackUserData, msg); + + // Return whether we should assert + return g.IO.ConfigErrorRecoveryEnableAssert; +} + +void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip() +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugDrawIdConflicts != 0 && g.IO.KeyCtrl == false) + g.DebugDrawIdConflictsCount = g.HoveredIdPreviousFrameItemCount; + if (g.DebugDrawIdConflicts != 0 && g.DebugItemPickerActive == false && BeginErrorTooltip()) + { + Text("Programmer error: %d visible items with conflicting ID!", g.DebugDrawIdConflictsCount); + BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!"); + BulletText("Empty label e.g. Button(\"\") == same ID as parent widget/node. Use Button(\"##xx\") instead!"); + //BulletText("Code intending to use duplicate ID may use e.g. PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()"); // Not making this too visible for fear of it being abused. + BulletText("Set io.ConfigDebugHighlightIdConflicts=false to disable this warning in non-programmers builds."); + Separator(); + if (g.IO.ConfigDebugHighlightIdConflictsShowItemPicker) + { + Text("(Hold CTRL to: use "); + SameLine(0.0f, 0.0f); + if (SmallButton("Item Picker")) + DebugStartItemPicker(); + SameLine(0.0f, 0.0f); + Text(" to break in item call-stack, or "); + } + else + { + Text("(Hold CTRL to "); + } + SameLine(0.0f, 0.0f); + if (SmallButton("Open FAQ->About ID Stack System") && g.PlatformIO.Platform_OpenInShellFn != NULL) + g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage"); + SameLine(0.0f, 0.0f); + Text(")"); + EndErrorTooltip(); + } + + if (g.ErrorCountCurrentFrame > 0 && BeginErrorTooltip()) // Amend at end of frame + { + Separator(); + Text("(Hold CTRL to:"); + SameLine(); + if (SmallButton("Enable Asserts")) + g.IO.ConfigErrorRecoveryEnableAssert = true; + //SameLine(); + //if (SmallButton("Hide Error Tooltips")) + // g.IO.ConfigErrorRecoveryEnableTooltip = false; // Too dangerous + SameLine(0, 0); + Text(")"); + EndErrorTooltip(); + } +#endif +} + +// Pseudo-tooltip. Follow mouse until CTRL is held. When CTRL is held we lock position, allowing to click it. +bool ImGui::BeginErrorTooltip() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = FindWindowByName("##Tooltip_Error"); + const bool use_locked_pos = (g.IO.KeyCtrl && window && window->WasActive); + PushStyleColor(ImGuiCol_PopupBg, ImLerp(g.Style.Colors[ImGuiCol_PopupBg], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.15f)); + if (use_locked_pos) + SetNextWindowPos(g.ErrorTooltipLockedPos); + bool is_visible = Begin("##Tooltip_Error", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + PopStyleColor(); + if (is_visible && g.CurrentWindow->BeginCount == 1) + { + SeparatorText("MESSAGE FROM DEAR IMGUI"); + BringWindowToDisplayFront(g.CurrentWindow); + BringWindowToFocusFront(g.CurrentWindow); + g.ErrorTooltipLockedPos = GetWindowPos(); + } + else if (!is_visible) + { + End(); + } + return is_visible; +} + +void ImGui::EndErrorTooltip() +{ + End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ITEM SUBMISSION +//----------------------------------------------------------------------------- +// - KeepAliveID() +// - ItemAdd() +//----------------------------------------------------------------------------- + +// Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID(). +void ImGui::KeepAliveID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = id; + if (g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.IsAlive = true; +} + +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. +// THIS IS IN THE PERFORMANCE CRITICAL PATH (UNTIL THE CLIPPING TEST AND EARLY-RETURN) +IM_MSVC_RUNTIME_CHECKS_OFF +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Set item data + // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) + g.LastItemData.ID = id; + g.LastItemData.Rect = bb; + g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; + g.LastItemData.ItemFlags = g.CurrentItemFlags | g.NextItemData.ItemFlags | extra_flags; + g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; + // Note: we don't copy 'g.NextItemData.SelectionUserData' to an hypothetical g.LastItemData.SelectionUserData: since the former is not cleared. + + if (id != 0) + { + KeepAliveID(id); + + // Directional navigation processing + // Runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests + // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of + // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able + // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). + // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. + // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. + if (!(g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav)) + { + // FIMXE-NAV: investigate changing the window tests into a simple 'if (g.NavFocusScopeId == g.CurrentFocusScopeId)' test. + window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened)) + NavProcessItem(); + } + + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasShortcut) + ItemHandleShortcut(id); + } + + // Lightweight clear of SetNextItemXXX data. + g.NextItemData.HasFlags = ImGuiNextItemDataFlags_None; + g.NextItemData.ItemFlags = ImGuiItemFlags_None; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0) + IMGUI_TEST_ENGINE_ITEM_ADD(id, g.LastItemData.NavRect, &g.LastItemData); +#endif + + // Clipping test + // (this is an inline copy of IsClippedEx() so we can reuse the is_rect_visible value, otherwise we'd do 'if (IsClippedEx(bb, id)) return false') + // g.NavActivateId is not necessarily == g.NavId, in the case of remote activation (e.g. shortcuts) + const bool is_rect_visible = bb.Overlaps(window->ClipRect); + if (!is_rect_visible) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return false; + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + if (id == g.DebugLocateId) + DebugLocateItemResolveWithLastItem(); + + // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". + // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". + // READ THE FAQ: https://dearimgui.com/faq + IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); + } + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + //if ((g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav) == 0) + // window->DrawList->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, IM_COL32(255,255,0,255)); // [DEBUG] +#endif + + if (id != 0 && g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.ElapseFrame = g.FrameCount; + + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (is_rect_visible) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Visible; + if (IsMouseHoveringRect(bb.Min, bb.Max)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] LAYOUT +//----------------------------------------------------------------------------- +// - ItemSize() +// - SameLine() +// - GetCursorScreenPos() +// - SetCursorScreenPos() +// - GetCursorPos(), GetCursorPosX(), GetCursorPosY() +// - SetCursorPos(), SetCursorPosX(), SetCursorPosY() +// - GetCursorStartPos() +// - Indent() +// - Unindent() +// - SetNextItemWidth() +// - PushItemWidth() +// - PushMultiItemsWidths() +// - PopItemWidth() +// - CalcItemWidth() +// - CalcItemSize() +// - GetTextLineHeight() +// - GetTextLineHeightWithSpacing() +// - GetFrameHeight() +// - GetFrameHeightWithSpacing() +// - GetContentRegionMax() +// - GetContentRegionAvail(), +// - BeginGroup() +// - EndGroup() +// Also see in imgui_widgets: tab bars, and in imgui_tables: tables, columns. +//----------------------------------------------------------------------------- + +// Advance cursor given item size for layout. +// Register minimum needed size so it can extend the bounding box used for auto-fit calculation. +// See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different. +// THIS IS IN THE PERFORMANCE CRITICAL PATH. +IM_MSVC_RUNTIME_CHECKS_OFF +void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // We increase the height in this function to accommodate for baseline offset. + // In theory we should be offsetting the starting position (window->DC.CursorPos), that will be the topic of a larger refactor, + // but since ItemSize() is not yet an API that moves the cursor (to handle e.g. wrapping) enlarging the height has the same effect. + const float offset_to_match_baseline_y = (text_baseline_y >= 0) ? ImMax(0.0f, window->DC.CurrLineTextBaseOffset - text_baseline_y) : 0.0f; + + const float line_y1 = window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y; + const float line_height = ImMax(window->DC.CurrLineSize.y, /*ImMax(*/window->DC.CursorPos.y - line_y1/*, 0.0f)*/ + size.y + offset_to_match_baseline_y); + + // Always align ourselves on pixel boundaries + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; + window->DC.CursorPosPrevLine.y = line_y1; + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line + window->DC.CursorPos.y = IM_TRUNC(line_y1 + line_height + g.Style.ItemSpacing.y); // Next line + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineSize.y = line_height; + window->DC.CurrLineSize.y = 0.0f; + window->DC.PrevLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, text_baseline_y); + window->DC.CurrLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Gets back to previous line and continue with horizontal layout +// offset_from_start_x == 0 : follow right after previous item +// offset_from_start_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if offset_from_start_x == 0, no spacing if offset_from_start_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float offset_from_start_x, float spacing_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + if (offset_from_start_x != 0.0f) + { + if (spacing_w < 0.0f) + spacing_w = 0.0f; + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + offset_from_start_x + spacing_w + window->DC.GroupOffset.x + window->DC.ColumnsOffset.x; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + else + { + if (spacing_w < 0.0f) + spacing_w = g.Style.ItemSpacing.x; + window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + window->DC.CurrLineSize = window->DC.PrevLineSize; + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + window->DC.IsSameLine = true; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +void ImGui::SetCursorScreenPos(const ImVec2& pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = pos; + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; +} + +// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. +// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. +ImVec2 ImGui::GetCursorPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos - window->Pos + window->Scroll; +} + +float ImGui::GetCursorPosX() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; +} + +float ImGui::GetCursorPosY() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; +} + +void ImGui::SetCursorPos(const ImVec2& local_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; +} + +void ImGui::SetCursorPosX(float x) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; + //window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); + window->DC.IsSetPos = true; +} + +void ImGui::SetCursorPosY(float y) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; + //window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); + window->DC.IsSetPos = true; +} + +ImVec2 ImGui::GetCursorStartPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; +} + +void ImGui::Indent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x += (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +void ImGui::Unindent(float indent_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + window->DC.Indent.x -= (indent_w != 0.0f) ? indent_w : g.Style.IndentSpacing; + window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; +} + +// Affect large frame+labels widgets only. +void ImGui::SetNextItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasWidth; + g.NextItemData.Width = item_width; +} + +// FIXME: Remove the == 0.0f behavior? +void ImGui::PushItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + g.NextItemData.HasFlags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PushMultiItemsWidths(int components, float w_full) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(components > 0); + const ImGuiStyle& style = g.Style; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + float w_items = w_full - style.ItemInnerSpacing.x * (components - 1); + float prev_split = w_items; + for (int i = components - 1; i > 0; i--) + { + float next_split = IM_TRUNC(w_items * i / components); + window->DC.ItemWidthStack.push_back(ImMax(prev_split - next_split, 1.0f)); + prev_split = next_split; + } + window->DC.ItemWidth = ImMax(prev_split, 1.0f); + g.NextItemData.HasFlags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PopItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.ItemWidthStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemWidth() too many times!"); + return; + } + window->DC.ItemWidth = window->DC.ItemWidthStack.back(); + window->DC.ItemWidthStack.pop_back(); +} + +// Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth(). +// The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags() +float ImGui::CalcItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float w; + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth) + w = g.NextItemData.Width; + else + w = window->DC.ItemWidth; + if (w < 0.0f) + { + float region_avail_x = GetContentRegionAvail().x; + w = ImMax(1.0f, region_avail_x + w); + } + w = IM_TRUNC(w); + return w; +} + +// [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth(). +// Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical. +// Note that only CalcItemWidth() is publicly exposed. +// The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable) +ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_w, float default_h) +{ + ImVec2 avail; + if (size.x < 0.0f || size.y < 0.0f) + avail = GetContentRegionAvail(); + + if (size.x == 0.0f) + size.x = default_w; + else if (size.x < 0.0f) + size.x = ImMax(4.0f, avail.x + size.x); // <-- size.x is negative here so we are subtracting + + if (size.y == 0.0f) + size.y = default_h; + else if (size.y < 0.0f) + size.y = ImMax(4.0f, avail.y + size.y); // <-- size.y is negative here so we are subtracting + + return size; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 mx = (window->DC.CurrentColumns || g.CurrentTable) ? window->WorkRect.Max : window->ContentRegionRect.Max; + return mx - window->DC.CursorPos; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// You should never need those functions. Always use GetCursorScreenPos() and GetContentRegionAvail()! +// They are bizarre local-coordinates which don't play well with scrolling. +ImVec2 ImGui::GetContentRegionMax() +{ + return GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos(); +} + +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Min - window->Pos; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Max - window->Pos; +} +#endif + +// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) +// Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. +// FIXME-OPT: Could we safely early out on ->SkipItems? +void ImGui::BeginGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.GroupStack.resize(g.GroupStack.Size + 1); + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.WindowID = window->ID; + group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; + group_data.BackupIndent = window->DC.Indent; + group_data.BackupGroupOffset = window->DC.GroupOffset; + group_data.BackupCurrLineSize = window->DC.CurrLineSize; + group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; + group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; + group_data.BackupHoveredIdIsAlive = g.HoveredId != 0; + group_data.BackupIsSameLine = window->DC.IsSameLine; + group_data.BackupDeactivatedIdIsAlive = g.DeactivatedItemData.IsAlive; + group_data.EmitItem = true; + + window->DC.GroupOffset.x = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffset.x; + window->DC.Indent = window->DC.GroupOffset; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.GroupStack.Size > 0); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = g.GroupStack.back(); + IM_ASSERT(group_data.WindowID == window->ID); // EndGroup() in wrong window? + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Include LastItemData.Rect.Max as a workaround for e.g. EndTable() undershooting with CursorMaxPos report. (#7543) + ImRect group_bb(group_data.BackupCursorPos, ImMax(ImMax(window->DC.CursorMaxPos, g.LastItemData.Rect.Max), group_data.BackupCursorPos)); + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorPosPrevLine = group_data.BackupCursorPosPrevLine; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, group_bb.Max); + window->DC.Indent = group_data.BackupIndent; + window->DC.GroupOffset = group_data.BackupGroupOffset; + window->DC.CurrLineSize = group_data.BackupCurrLineSize; + window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; + window->DC.IsSameLine = group_data.BackupIsSameLine; + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return + + if (!group_data.EmitItem) + { + g.GroupStack.pop_back(); + return; + } + + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize()); + ItemAdd(group_bb, 0, NULL, ImGuiItemFlags_NoTabStop); + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group. + // It would be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets. + // Also if you grep for LastItemId you'll notice it is only used in that context. + // (The two tests not the same because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.) + const bool group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive != g.ActiveId) && (g.ActiveIdIsAlive == g.ActiveId) && g.ActiveId; + const bool group_contains_deactivated_id = (group_data.BackupDeactivatedIdIsAlive == false) && (g.DeactivatedItemData.IsAlive == true); + if (group_contains_curr_active_id) + g.LastItemData.ID = g.ActiveId; + else if (group_contains_deactivated_id) + g.LastItemData.ID = g.DeactivatedItemData.ID; + g.LastItemData.Rect = group_bb; + + // Forward Hovered flag + const bool group_contains_curr_hovered_id = (group_data.BackupHoveredIdIsAlive == false) && g.HoveredId != 0; + if (group_contains_curr_hovered_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + // Forward Edited flag + if (group_contains_curr_active_id && g.ActiveIdHasBeenEditedThisFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; + + // Forward Deactivated flag + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated; + if (group_contains_deactivated_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Deactivated; + + g.GroupStack.pop_back(); + if (g.DebugShowGroupRects) + window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + + +//----------------------------------------------------------------------------- +// [SECTION] SCROLLING +//----------------------------------------------------------------------------- + +// Helper to snap on edges when aiming at an item very close to the edge, +// So the difference between WindowPadding and ItemSpacing will be in the visible area after scrolling. +// When we refactor the scrolling API this may be configurable with a flag? +// Note that the effect for this won't be visible on X axis with default Style settings as WindowPadding.x == ItemSpacing.x by default. +static float CalcScrollEdgeSnap(float target, float snap_min, float snap_max, float snap_threshold, float center_ratio) +{ + if (target <= snap_min + snap_threshold) + return ImLerp(snap_min, target, center_ratio); + if (target >= snap_max - snap_threshold) + return ImLerp(target, snap_max, center_ratio); + return target; +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) +{ + ImVec2 scroll = window->Scroll; + ImVec2 decoration_size(window->DecoOuterSizeX1 + window->DecoInnerSizeX1 + window->DecoOuterSizeX2, window->DecoOuterSizeY1 + window->DecoInnerSizeY1 + window->DecoOuterSizeY2); + for (int axis = 0; axis < 2; axis++) + { + if (window->ScrollTarget[axis] < FLT_MAX) + { + float center_ratio = window->ScrollTargetCenterRatio[axis]; + float scroll_target = window->ScrollTarget[axis]; + if (window->ScrollTargetEdgeSnapDist[axis] > 0.0f) + { + float snap_min = 0.0f; + float snap_max = window->ScrollMax[axis] + window->SizeFull[axis] - decoration_size[axis]; + scroll_target = CalcScrollEdgeSnap(scroll_target, snap_min, snap_max, window->ScrollTargetEdgeSnapDist[axis], center_ratio); + } + scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]); + } + scroll[axis] = IM_ROUND(ImMax(scroll[axis], 0.0f)); + if (!window->Collapsed && !window->SkipItems) + scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]); + } + return scroll; +} + +void ImGui::ScrollToItem(ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ScrollToRectEx(window, g.LastItemData.NavRect, flags); +} + +void ImGui::ScrollToRect(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ScrollToRectEx(window, item_rect, flags); +} + +// Scroll to keep newly navigated item fully into view +ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImRect scroll_rect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1)); + scroll_rect.Min.x = ImMin(scroll_rect.Min.x + window->DecoInnerSizeX1, scroll_rect.Max.x); + scroll_rect.Min.y = ImMin(scroll_rect.Min.y + window->DecoInnerSizeY1, scroll_rect.Max.y); + //GetForegroundDrawList(window)->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255,0,0,255), 0.0f, 0, 5.0f); // [DEBUG] + //GetForegroundDrawList(window)->AddRect(scroll_rect.Min, scroll_rect.Max, IM_COL32_WHITE); // [DEBUG] + + // Check that only one behavior is selected per axis + IM_ASSERT((flags & ImGuiScrollFlags_MaskX_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskX_)); + IM_ASSERT((flags & ImGuiScrollFlags_MaskY_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskY_)); + + // Defaults + ImGuiScrollFlags in_flags = flags; + if ((flags & ImGuiScrollFlags_MaskX_) == 0 && window->ScrollbarX) + flags |= ImGuiScrollFlags_KeepVisibleEdgeX; + if ((flags & ImGuiScrollFlags_MaskY_) == 0) + flags |= window->Appearing ? ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeY; + + const bool fully_visible_x = item_rect.Min.x >= scroll_rect.Min.x && item_rect.Max.x <= scroll_rect.Max.x; + const bool fully_visible_y = item_rect.Min.y >= scroll_rect.Min.y && item_rect.Max.y <= scroll_rect.Max.y; + const bool can_be_fully_visible_x = (item_rect.GetWidth() + g.Style.ItemSpacing.x * 2.0f) <= scroll_rect.GetWidth() || (window->AutoFitFramesX > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + const bool can_be_fully_visible_y = (item_rect.GetHeight() + g.Style.ItemSpacing.y * 2.0f) <= scroll_rect.GetHeight() || (window->AutoFitFramesY > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeX) && !fully_visible_x) + { + if (item_rect.Min.x < scroll_rect.Min.x || !can_be_fully_visible_x) + SetScrollFromPosX(window, item_rect.Min.x - g.Style.ItemSpacing.x - window->Pos.x, 0.0f); + else if (item_rect.Max.x >= scroll_rect.Max.x) + SetScrollFromPosX(window, item_rect.Max.x + g.Style.ItemSpacing.x - window->Pos.x, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterX) && !fully_visible_x) || (flags & ImGuiScrollFlags_AlwaysCenterX)) + { + if (can_be_fully_visible_x) + SetScrollFromPosX(window, ImTrunc((item_rect.Min.x + item_rect.Max.x) * 0.5f) - window->Pos.x, 0.5f); + else + SetScrollFromPosX(window, item_rect.Min.x - window->Pos.x, 0.0f); + } + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeY) && !fully_visible_y) + { + if (item_rect.Min.y < scroll_rect.Min.y || !can_be_fully_visible_y) + SetScrollFromPosY(window, item_rect.Min.y - g.Style.ItemSpacing.y - window->Pos.y, 0.0f); + else if (item_rect.Max.y >= scroll_rect.Max.y) + SetScrollFromPosY(window, item_rect.Max.y + g.Style.ItemSpacing.y - window->Pos.y, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterY) && !fully_visible_y) || (flags & ImGuiScrollFlags_AlwaysCenterY)) + { + if (can_be_fully_visible_y) + SetScrollFromPosY(window, ImTrunc((item_rect.Min.y + item_rect.Max.y) * 0.5f) - window->Pos.y, 0.5f); + else + SetScrollFromPosY(window, item_rect.Min.y - window->Pos.y, 0.0f); + } + + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ImVec2 delta_scroll = next_scroll - window->Scroll; + + // Also scroll parent window to keep us into view if necessary + if (!(flags & ImGuiScrollFlags_NoScrollParent) && (window->Flags & ImGuiWindowFlags_ChildWindow)) + { + // FIXME-SCROLL: May be an option? + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterX | ImGuiScrollFlags_KeepVisibleCenterX)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskX_) | ImGuiScrollFlags_KeepVisibleEdgeX; + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterY | ImGuiScrollFlags_KeepVisibleCenterY)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskY_) | ImGuiScrollFlags_KeepVisibleEdgeY; + delta_scroll += ScrollToRectEx(window->ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll), in_flags); + } + + return delta_scroll; +} + +float ImGui::GetScrollX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.x; +} + +float ImGui::GetScrollY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Scroll.y; +} + +float ImGui::GetScrollMaxX() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.x; +} + +float ImGui::GetScrollMaxY() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ScrollMax.y; +} + +void ImGui::SetScrollX(ImGuiWindow* window, float scroll_x) +{ + window->ScrollTarget.x = scroll_x; + window->ScrollTargetCenterRatio.x = 0.0f; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollY(ImGuiWindow* window, float scroll_y) +{ + window->ScrollTarget.y = scroll_y; + window->ScrollTargetCenterRatio.y = 0.0f; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollX(float scroll_x) +{ + ImGuiContext& g = *GImGui; + SetScrollX(g.CurrentWindow, scroll_x); +} + +void ImGui::SetScrollY(float scroll_y) +{ + ImGuiContext& g = *GImGui; + SetScrollY(g.CurrentWindow, scroll_y); +} + +// Note that a local position will vary depending on initial scroll value, +// This is a little bit confusing so bear with us: +// - local_pos = (absolution_pos - window->Pos) +// - So local_x/local_y are 0.0f for a position at the upper-left corner of a window, +// and generally local_x/local_y are >(padding+decoration) && <(size-padding-decoration) when in the visible area. +// - They mostly exist because of legacy API. +// Following the rules above, when trying to work with scrolling code, consider that: +// - SetScrollFromPosY(0.0f) == SetScrollY(0.0f + scroll.y) == has no effect! +// - SetScrollFromPosY(-scroll.y) == SetScrollY(-scroll.y + scroll.y) == SetScrollY(0.0f) == reset scroll. Of course writing SetScrollY(0.0f) directly then makes more sense +// We store a target position so centering and clamping can occur on the next frame when we are guaranteed to have a known window size +void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio) +{ + IM_ASSERT(center_x_ratio >= 0.0f && center_x_ratio <= 1.0f); + window->ScrollTarget.x = IM_TRUNC(local_x - window->DecoOuterSizeX1 - window->DecoInnerSizeX1 + window->Scroll.x); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.x = center_x_ratio; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio) +{ + IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); + window->ScrollTarget.y = IM_TRUNC(local_y - window->DecoOuterSizeY1 - window->DecoInnerSizeY1 + window->Scroll.y); // Convert local position to scroll offset + window->ScrollTargetCenterRatio.y = center_y_ratio; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollFromPosX(float local_x, float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosX(g.CurrentWindow, local_x, center_x_ratio); +} + +void ImGui::SetScrollFromPosY(float local_y, float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + SetScrollFromPosY(g.CurrentWindow, local_y, center_y_ratio); +} + +// center_x_ratio: 0.0f left of last item, 0.5f horizontal center of last item, 1.0f right of last item. +void ImGui::SetScrollHereX(float center_x_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_x = ImMax(window->WindowPadding.x, g.Style.ItemSpacing.x); + float target_pos_x = ImLerp(g.LastItemData.Rect.Min.x - spacing_x, g.LastItemData.Rect.Max.x + spacing_x, center_x_ratio); + SetScrollFromPosX(window, target_pos_x - window->Pos.x, center_x_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.x = ImMax(0.0f, window->WindowPadding.x - spacing_x); +} + +// center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. +void ImGui::SetScrollHereY(float center_y_ratio) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float spacing_y = ImMax(window->WindowPadding.y, g.Style.ItemSpacing.y); + float target_pos_y = ImLerp(window->DC.CursorPosPrevLine.y - spacing_y, window->DC.CursorPosPrevLine.y + window->DC.PrevLineSize.y + spacing_y, center_y_ratio); + SetScrollFromPosY(window, target_pos_y - window->Pos.y, center_y_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.y = ImMax(0.0f, window->WindowPadding.y - spacing_y); +} + +//----------------------------------------------------------------------------- +// [SECTION] TOOLTIPS +//----------------------------------------------------------------------------- + +bool ImGui::BeginTooltip() +{ + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +bool ImGui::BeginItemTooltip() +{ + if (!IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + return false; + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +bool ImGui::BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + + const bool is_dragdrop_tooltip = g.DragDropWithinSource || g.DragDropWithinTarget; + if (is_dragdrop_tooltip) + { + // Drag and Drop tooltips are positioning differently than other tooltips: + // - offset visibility to increase visibility around mouse. + // - never clamp within outer viewport boundary. + // We call SetNextWindowPos() to enforce position and disable clamping. + // See FindBestWindowPosForPopup() for positionning logic of other tooltips (not drag and drop ones). + //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; + const bool is_touchscreen = (g.IO.MouseSource == ImGuiMouseSource_TouchScreen); + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + ImVec2 tooltip_pos = is_touchscreen ? (g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_TOUCH * g.Style.MouseCursorScale) : (g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_MOUSE * g.Style.MouseCursorScale); + ImVec2 tooltip_pivot = is_touchscreen ? TOOLTIP_DEFAULT_PIVOT_TOUCH : ImVec2(0.0f, 0.0f); + SetNextWindowPos(tooltip_pos, ImGuiCond_None, tooltip_pivot); + } + + SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); + //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( + tooltip_flags |= ImGuiTooltipFlags_OverridePrevious; + } + + const char* window_name_template = is_dragdrop_tooltip ? "##Tooltip_DragDrop_%02d" : "##Tooltip_%02d"; + char window_name[32]; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), window_name_template, g.TooltipOverrideCount); + if ((tooltip_flags & ImGuiTooltipFlags_OverridePrevious) && g.TooltipPreviousWindow != NULL && g.TooltipPreviousWindow->Active) + { + // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. + //IMGUI_DEBUG_LOG("[tooltip] '%s' already active, using +1 for this frame\n", window_name); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.TooltipPreviousWindow); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), window_name_template, ++g.TooltipOverrideCount); + } + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize; + Begin(window_name, NULL, flags | extra_window_flags); + // 2023-03-09: Added bool return value to the API, but currently always returning true. + // If this ever returns false we need to update BeginDragDropSource() accordingly. + //if (!ret) + // End(); + //return ret; + return true; +} + +void ImGui::EndTooltip() +{ + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); +} + +void ImGui::SetTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; + TextV(fmt, args); + EndTooltip(); +} + +// Shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav'. +// Defaults to == ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort when using the mouse. +void ImGui::SetItemTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetItemTooltipV(const char* fmt, va_list args) +{ + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); +} + + +//----------------------------------------------------------------------------- +// [SECTION] POPUPS +//----------------------------------------------------------------------------- + +// Supported flags: ImGuiPopupFlags_AnyPopupId, ImGuiPopupFlags_AnyPopupLevel +bool ImGui::IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + if (popup_flags & ImGuiPopupFlags_AnyPopupId) + { + // Return true if any popup is open at the current BeginPopup() level of the popup stack + // This may be used to e.g. test for another popups already opened to handle popups priorities at the same level. + IM_ASSERT(id == 0); + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + return g.OpenPopupStack.Size > 0; + else + return g.OpenPopupStack.Size > g.BeginPopupStack.Size; + } + else + { + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + { + // Return true if the popup is open anywhere in the popup stack + for (int n = 0; n < g.OpenPopupStack.Size; n++) + if (g.OpenPopupStack[n].PopupId == id) + return true; + return false; + } + else + { + // Return true if the popup is open at the current BeginPopup() level of the popup stack (this is the most-common query) + return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == id; + } + } +} + +bool ImGui::IsPopupOpen(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = (popup_flags & ImGuiPopupFlags_AnyPopupId) ? 0 : g.CurrentWindow->GetID(str_id); + if ((popup_flags & ImGuiPopupFlags_AnyPopupLevel) && id != 0) + IM_ASSERT(0 && "Cannot use IsPopupOpen() with a string id and ImGuiPopupFlags_AnyPopupLevel."); // But non-string version is legal and used internally + return IsPopupOpen(id, popup_flags); +} + +// Also see FindBlockingModal(NULL) +ImGuiWindow* ImGui::GetTopMostPopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if (popup->Flags & ImGuiWindowFlags_Modal) + return popup; + return NULL; +} + +// See Demo->Stacked Modal to confirm what this is for. +ImGuiWindow* ImGui::GetTopMostAndVisiblePopupModal() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if ((popup->Flags & ImGuiWindowFlags_Modal) && IsWindowActiveAndVisible(popup)) + return popup; + return NULL; +} + + +// When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) +// should be positioned behind that modal window, unless the window was created inside the modal begin-stack. +// In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. +// - WindowA // FindBlockingModal() returns Modal1 +// - WindowB // .. returns Modal1 +// - Modal1 // .. returns Modal2 +// - WindowC // .. returns Modal2 +// - WindowD // .. returns Modal2 +// - Modal2 // .. returns Modal2 +// - WindowE // .. returns NULL +// Notes: +// - FindBlockingModal(NULL) == NULL is generally equivalent to GetTopMostPopupModal() == NULL. +// Only difference is here we check for ->Active/WasActive but it may be unnecessary. +ImGuiWindow* ImGui::FindBlockingModal(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= 0) + return NULL; + + // Find a modal that has common parent with specified window. Specified window should be positioned behind that modal. + for (ImGuiPopupData& popup_data : g.OpenPopupStack) + { + ImGuiWindow* popup_window = popup_data.Window; + if (popup_window == NULL || !(popup_window->Flags & ImGuiWindowFlags_Modal)) + continue; + if (!popup_window->Active && !popup_window->WasActive) // Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows. + continue; + if (window == NULL) // FindBlockingModal(NULL) test for if FocusWindow(NULL) is naturally possible via a mouse click. + return popup_window; + if (IsWindowWithinBeginStackOf(window, popup_window)) // Window may be over modal + continue; + return popup_window; // Place window right below first block modal + } + return NULL; +} + +void ImGui::OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.CurrentWindow->GetID(str_id); + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopup(\"%s\" -> 0x%08X)\n", str_id, id); + OpenPopupEx(id, popup_flags); +} + +void ImGui::OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + OpenPopupEx(id, popup_flags); +} + +// Mark popup as open (toggle toward open state). +// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. +// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). +// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) +void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + const int current_stack_size = g.BeginPopupStack.Size; + + if (popup_flags & ImGuiPopupFlags_NoOpenOverExistingPopup) + if (IsPopupOpen((ImGuiID)0, ImGuiPopupFlags_AnyPopupId)) + return; + + ImGuiPopupData popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. + popup_ref.PopupId = id; + popup_ref.Window = NULL; + popup_ref.RestoreNavWindow = g.NavWindow; // When popup closes focus may be restored to NavWindow (depend on window type). + popup_ref.OpenFrameCount = g.FrameCount; + popup_ref.OpenParentId = parent_window->IDStack.back(); + popup_ref.OpenPopupPos = NavCalcPreferredRefPos(); + popup_ref.OpenMousePos = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : popup_ref.OpenPopupPos; + + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopupEx(0x%08X)\n", id); + if (g.OpenPopupStack.Size < current_stack_size + 1) + { + g.OpenPopupStack.push_back(popup_ref); + } + else + { + // Gently handle the user mistakenly calling OpenPopup() every frames: it is likely a programming mistake! + // However, if we were to run the regular code path, the ui would become completely unusable because the popup will always be + // in hidden-while-calculating-size state _while_ claiming focus. Which is extremely confusing situation for the programmer. + // Instead, for successive frames calls to OpenPopup(), we silently avoid reopening even if ImGuiPopupFlags_NoReopen is not specified. + bool keep_existing = false; + if (g.OpenPopupStack[current_stack_size].PopupId == id) + if ((g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) || (popup_flags & ImGuiPopupFlags_NoReopen)) + keep_existing = true; + if (keep_existing) + { + // No reopen + g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; + } + else + { + // Reopen: close child popups if any, then flag popup for open/reopen (set position, focus, init navigation) + ClosePopupToLevel(current_stack_size, true); + g.OpenPopupStack.push_back(popup_ref); + } + + // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). + // This is equivalent to what ClosePopupToLevel() does. + //if (g.OpenPopupStack[current_stack_size].PopupId == id) + // FocusWindow(parent_window); + } +} + +// When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. +// This function closes any popups that are over 'ref_window'. +void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size == 0) + return; + + // Don't close our own child popup windows. + //IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\") restore_under=%d\n", ref_window ? ref_window->Name : "", restore_focus_to_window_under_popup); + int popup_count_to_keep = 0; + if (ref_window) + { + // Find the highest popup which is a descendant of the reference window (generally reference window = NavWindow) + for (; popup_count_to_keep < g.OpenPopupStack.Size; popup_count_to_keep++) + { + ImGuiPopupData& popup = g.OpenPopupStack[popup_count_to_keep]; + if (!popup.Window) + continue; + IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); + + // Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow) + // - Clicking/Focusing Window2 won't close Popup1: + // Window -> Popup1 -> Window2(Ref) + // - Clicking/focusing Popup1 will close Popup2 and Popup3: + // Window -> Popup1(Ref) -> Popup2 -> Popup3 + // - Each popups may contain child windows, which is why we compare ->RootWindow! + // Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child + // We step through every popup from bottom to top to validate their position relative to reference window. + bool ref_window_is_descendent_of_popup = false; + for (int n = popup_count_to_keep; n < g.OpenPopupStack.Size; n++) + if (ImGuiWindow* popup_window = g.OpenPopupStack[n].Window) + if (IsWindowWithinBeginStackOf(ref_window, popup_window)) + { + ref_window_is_descendent_of_popup = true; + break; + } + if (!ref_window_is_descendent_of_popup) + break; + } + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + { + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\")\n", ref_window ? ref_window->Name : ""); + ClosePopupToLevel(popup_count_to_keep, restore_focus_to_window_under_popup); + } +} + +void ImGui::ClosePopupsExceptModals() +{ + ImGuiContext& g = *GImGui; + + int popup_count_to_keep; + for (popup_count_to_keep = g.OpenPopupStack.Size; popup_count_to_keep > 0; popup_count_to_keep--) + { + ImGuiWindow* window = g.OpenPopupStack[popup_count_to_keep - 1].Window; + if (!window || (window->Flags & ImGuiWindowFlags_Modal)) + break; + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + ClosePopupToLevel(popup_count_to_keep, true); +} + +void ImGui::ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_under=%d\n", remaining, restore_focus_to_window_under_popup); + IM_ASSERT(remaining >= 0 && remaining < g.OpenPopupStack.Size); + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) + for (int n = remaining; n < g.OpenPopupStack.Size; n++) + IMGUI_DEBUG_LOG_POPUP("[popup] - Closing PopupID 0x%08X Window \"%s\"\n", g.OpenPopupStack[n].PopupId, g.OpenPopupStack[n].Window ? g.OpenPopupStack[n].Window->Name : NULL); + + // Trim open popup stack + ImGuiPopupData prev_popup = g.OpenPopupStack[remaining]; + g.OpenPopupStack.resize(remaining); + + // Restore focus (unless popup window was not yet submitted, and didn't have a chance to take focus anyhow. See #7325 for an edge case) + if (restore_focus_to_window_under_popup && prev_popup.Window) + { + ImGuiWindow* popup_window = prev_popup.Window; + ImGuiWindow* focus_window = (popup_window->Flags & ImGuiWindowFlags_ChildMenu) ? popup_window->ParentWindow : prev_popup.RestoreNavWindow; + if (focus_window && !focus_window->WasActive) + FocusTopMostWindowUnderOne(popup_window, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); // Fallback + else + FocusWindow(focus_window, (g.NavLayer == ImGuiNavLayer_Main) ? ImGuiFocusRequestFlags_RestoreFocusedChild : ImGuiFocusRequestFlags_None); + } +} + +// Close the popup we have begin-ed into. +void ImGui::CloseCurrentPopup() +{ + ImGuiContext& g = *GImGui; + int popup_idx = g.BeginPopupStack.Size - 1; + if (popup_idx < 0 || popup_idx >= g.OpenPopupStack.Size || g.BeginPopupStack[popup_idx].PopupId != g.OpenPopupStack[popup_idx].PopupId) + return; + + // Closing a menu closes its top-most parent popup (unless a modal) + while (popup_idx > 0) + { + ImGuiWindow* popup_window = g.OpenPopupStack[popup_idx].Window; + ImGuiWindow* parent_popup_window = g.OpenPopupStack[popup_idx - 1].Window; + bool close_parent = false; + if (popup_window && (popup_window->Flags & ImGuiWindowFlags_ChildMenu)) + if (parent_popup_window && !(parent_popup_window->Flags & ImGuiWindowFlags_MenuBar)) + close_parent = true; + if (!close_parent) + break; + popup_idx--; + } + IMGUI_DEBUG_LOG_POPUP("[popup] CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size - 1, popup_idx); + ClosePopupToLevel(popup_idx, true); + + // A common pattern is to close a popup when selecting a menu item/selectable that will open another window. + // To improve this usage pattern, we avoid nav highlight for a single frame in the parent window. + // Similarly, we could avoid mouse hover highlight in this window but it is less visually problematic. + if (ImGuiWindow* window = g.NavWindow) + window->DC.NavHideHighlightOneFrame = true; +} + +// Attention! BeginPopup() adds default flags when calling BeginPopupEx()! +bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + char name[20]; + IM_ASSERT((extra_window_flags & ImGuiWindowFlags_ChildMenu) == 0); // Use BeginPopupMenuEx() + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // No recycling, so we can close/open during the same frame + + bool is_open = Begin(name, NULL, extra_window_flags | ImGuiWindowFlags_Popup); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; + return is_open; +} + +bool ImGui::BeginPopupMenuEx(ImGuiID id, const char* label, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + char name[128]; + IM_ASSERT(extra_window_flags & ImGuiWindowFlags_ChildMenu); + ImFormatString(name, IM_ARRAYSIZE(name), "%s###Menu_%02d", label, g.BeginMenuDepth); // Recycle windows based on depth + bool is_open = Begin(name, NULL, extra_window_flags | ImGuiWindowFlags_Popup); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; + return is_open; +} + +bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= g.BeginPopupStack.Size) // Early out for performance + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + flags |= ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings; + ImGuiID id = g.CurrentWindow->GetID(str_id); + return BeginPopupEx(id, flags); +} + +// If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup. +// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup). +// - *p_open set back to false in BeginPopupModal() when popup is not open. +// - if you set *p_open to false before calling BeginPopupModal(), it will close the popup. +bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = window->GetID(name); + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (p_open && *p_open) + *p_open = false; + return false; + } + + // Center modal windows by default for increased visibility + // (this won't really last as settings will kick in, and is mostly for backward compatibility. user may do the same themselves) + // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + } + + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse; + const bool is_open = Begin(name, p_open, flags); + if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + { + EndPopup(); + if (is_open) + ClosePopupToLevel(g.BeginPopupStack.Size, true); + return false; + } + return is_open; +} + +void ImGui::EndPopup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + IM_ASSERT(g.BeginPopupStack.Size > 0); + + // Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests) + if (g.NavWindow == window) + NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY); + + // Child-popups don't need to be laid out + const ImGuiID backup_within_end_child_id = g.WithinEndChildID; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + g.WithinEndChildID = window->ID; + End(); + g.WithinEndChildID = backup_within_end_child_id; +} + +// Helper to open a popup if mouse button is released over the item +// - This is essentially the same as BeginPopupContextItem() but without the trailing BeginPopup() +void ImGui::OpenPopupOnItemClick(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + OpenPopupEx(id, popup_flags); + } +} + +// This is a helper to handle the simplest case of associating one named popup to one given widget. +// - To create a popup associated to the last item, you generally want to pass a NULL value to str_id. +// - To create a popup with a specific identifier, pass it in str_id. +// - This is useful when using using BeginPopupContextItem() on an item which doesn't have an identifier, e.g. a Text() call. +// - This is useful when multiple code locations may want to manipulate/open the same popup, given an explicit id. +// - You may want to handle the whole on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). +// This is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// OpenPopupOnItemClick(str_id, ImGuiPopupFlags_MouseButtonRight); +// return BeginPopup(id); +// Which is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) +// OpenPopup(id); +// return BeginPopup(id); +// The main difference being that this is tweaked to avoid computing the ID twice. +bool ImGui::BeginPopupContextItem(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextWindow(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "window_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + if (!(popup_flags & ImGuiPopupFlags_NoOpenOverItems) || !IsAnyItemHovered()) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +bool ImGui::BeginPopupContextVoid(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!str_id) + str_id = "void_context"; + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) + if (GetTopMostPopupModal() == NULL) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); +} + +// r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) +// r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. +// (r_outer is usually equivalent to the viewport rectangle minus padding, but when multi-viewports are enabled and monitor +// information are available, it may represent the entire platform monitor from the frame of reference of the current viewport. +// this allows us to have tooltips/popups displayed out of the parent viewport.) +ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy) +{ + ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); + //GetForegroundDrawList()->AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + + // Combo Box policy (we want a connecting edge) + if (policy == ImGuiPopupPositionPolicy_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Tooltip and Default popup policy + // (Always first try the direction we used on the last frame, if any) + if (policy == ImGuiPopupPositionPolicy_Tooltip || policy == ImGuiPopupPositionPolicy_Default) + { + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + + const float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + const float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + + // If there's not enough room on one axis, there's no point in positioning on a side on this axis (e.g. when not enough width, use a top/bottom position to maximize available width) + if (avail_w < size.x && (dir == ImGuiDir_Left || dir == ImGuiDir_Right)) + continue; + if (avail_h < size.y && (dir == ImGuiDir_Up || dir == ImGuiDir_Down)) + continue; + + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + + // Clamp top-left corner of popup + pos.x = ImMax(pos.x, r_outer.Min.x); + pos.y = ImMax(pos.y, r_outer.Min.y); + + *last_dir = dir; + return pos; + } + } + + // Fallback when not enough room: + *last_dir = ImGuiDir_None; + + // For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + if (policy == ImGuiPopupPositionPolicy_Tooltip) + return ref_pos + ImVec2(2, 2); + + // Otherwise try to keep within display + ImVec2 pos = ref_pos; + pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); + pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); + return pos; +} + +// Note that this is used for popups, which can overlap the non work-area of individual viewports. +ImRect ImGui::GetPopupAllowedExtentRect(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(window); + ImRect r_screen = ((ImGuiViewportP*)(void*)GetMainViewport())->GetMainRect(); + ImVec2 padding = g.Style.DisplaySafeAreaPadding; + r_screen.Expand(ImVec2((r_screen.GetWidth() > padding.x * 2) ? -padding.x : 0.0f, (r_screen.GetHeight() > padding.y * 2) ? -padding.y : 0.0f)); + return r_screen; +} + +ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + ImRect r_outer = GetPopupAllowedExtentRect(window); + if (window->Flags & ImGuiWindowFlags_ChildMenu) + { + // Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds. + // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. + IM_ASSERT(g.CurrentWindow == window); + ImGuiWindow* parent_window = g.CurrentWindowStack[g.CurrentWindowStack.Size - 2].Window; + float horizontal_overlap = g.Style.ItemInnerSpacing.x; // We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x). + ImRect r_avoid; + if (parent_window->DC.MenuBarAppending) + r_avoid = ImRect(-FLT_MAX, parent_window->ClipRect.Min.y, FLT_MAX, parent_window->ClipRect.Max.y); // Avoid parent menu-bar. If we wanted multi-line menu-bar, we may instead want to have the calling window setup e.g. a NextWindowData.PosConstraintAvoidRect field + else + r_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Default); + } + if (window->Flags & ImGuiWindowFlags_Popup) + { + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, ImRect(window->Pos, window->Pos), ImGuiPopupPositionPolicy_Default); // Ideally we'd disable r_avoid here + } + if (window->Flags & ImGuiWindowFlags_Tooltip) + { + // Position tooltip (always follows mouse + clamp within outer boundaries) + // FIXME: + // - Too many paths. One problem is that FindBestWindowPosForPopupEx() doesn't allow passing a suggested position (so touch screen path doesn't use it by default). + // - Drag and drop tooltips are not using this path either: BeginTooltipEx() manually sets their position. + // - Require some tidying up. In theory we could handle both cases in same location, but requires a bit of shuffling + // as drag and drop tooltips are calling SetNextWindowPos() leading to 'window_pos_set_by_api' being set in Begin(). + IM_ASSERT(g.CurrentWindow == window); + const float scale = g.Style.MouseCursorScale; + const ImVec2 ref_pos = NavCalcPreferredRefPos(); + + if (g.IO.MouseSource == ImGuiMouseSource_TouchScreen && NavCalcPreferredRefPosSource() == ImGuiInputSource_Mouse) + { + ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_TOUCH * scale - (TOOLTIP_DEFAULT_PIVOT_TOUCH * window->Size); + if (r_outer.Contains(ImRect(tooltip_pos, tooltip_pos + window->Size))) + return tooltip_pos; + } + + ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_MOUSE * scale; + ImRect r_avoid; + if (g.NavCursorVisible && g.NavHighlightItemUnderNav && !g.IO.ConfigNavMoveSetMousePos) + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); + else + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * scale, ref_pos.y + 24 * scale); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255, 0, 255, 255)); + + return FindBestWindowPosForPopupEx(tooltip_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); + } + IM_ASSERT(0); + return window->Pos; +} + +//----------------------------------------------------------------------------- +// [SECTION] WINDOW FOCUS +//---------------------------------------------------------------------------- +// - SetWindowFocus() +// - SetNextWindowFocus() +// - IsWindowFocused() +// - UpdateWindowInFocusOrderList() [Internal] +// - BringWindowToFocusFront() [Internal] +// - BringWindowToDisplayFront() [Internal] +// - BringWindowToDisplayBack() [Internal] +// - BringWindowToDisplayBehind() [Internal] +// - FindWindowDisplayIndex() [Internal] +// - FocusWindow() [Internal] +// - FocusTopMostWindowUnderOne() [Internal] +//----------------------------------------------------------------------------- + +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} + +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} + +void ImGui::SetNextWindowFocus() +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasFocus; +} + +// Similar to IsWindowHovered() +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.NavWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + + if (ref_window == NULL) + return false; + if (flags & ImGuiFocusedFlags_AnyWindow) + return true; + + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy); + + if (flags & ImGuiHoveredFlags_ChildWindows) + return IsWindowChildOf(ref_window, cur_window, popup_hierarchy); + else + return (ref_window == cur_window); +} + +static int ImGui::FindWindowFocusIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(g); + int order = window->FocusOrder; + IM_ASSERT(window->RootWindow == window); // No child window (not testing _ChildWindow because of docking) + IM_ASSERT(g.WindowsFocusOrder[order] == window); + return order; +} + +static void ImGui::UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags) +{ + ImGuiContext& g = *GImGui; + + const bool new_is_explicit_child = (new_flags & ImGuiWindowFlags_ChildWindow) != 0 && ((new_flags & ImGuiWindowFlags_Popup) == 0 || (new_flags & ImGuiWindowFlags_ChildMenu) != 0); + const bool child_flag_changed = new_is_explicit_child != window->IsExplicitChild; + if ((just_created || child_flag_changed) && !new_is_explicit_child) + { + IM_ASSERT(!g.WindowsFocusOrder.contains(window)); + g.WindowsFocusOrder.push_back(window); + window->FocusOrder = (short)(g.WindowsFocusOrder.Size - 1); + } + else if (!just_created && child_flag_changed && new_is_explicit_child) + { + IM_ASSERT(g.WindowsFocusOrder[window->FocusOrder] == window); + for (int n = window->FocusOrder + 1; n < g.WindowsFocusOrder.Size; n++) + g.WindowsFocusOrder[n]->FocusOrder--; + g.WindowsFocusOrder.erase(g.WindowsFocusOrder.Data + window->FocusOrder); + window->FocusOrder = -1; + } + window->IsExplicitChild = new_is_explicit_child; +} + +void ImGui::BringWindowToFocusFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == window->RootWindow); + + const int cur_order = window->FocusOrder; + IM_ASSERT(g.WindowsFocusOrder[cur_order] == window); + if (g.WindowsFocusOrder.back() == window) + return; + + const int new_order = g.WindowsFocusOrder.Size - 1; + for (int n = cur_order; n < new_order; n++) + { + g.WindowsFocusOrder[n] = g.WindowsFocusOrder[n + 1]; + g.WindowsFocusOrder[n]->FocusOrder--; + IM_ASSERT(g.WindowsFocusOrder[n]->FocusOrder == n); + } + g.WindowsFocusOrder[new_order] = window; + window->FocusOrder = (short)new_order; +} + +// Note technically focus related but rather adjacent and close to BringWindowToFocusFront() +void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindow == window) // Cheap early out (could be better) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window + if (g.Windows[i] == window) + { + memmove(&g.Windows[i], &g.Windows[i + 1], (size_t)(g.Windows.Size - i - 1) * sizeof(ImGuiWindow*)); + g.Windows[g.Windows.Size - 1] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* behind_window) +{ + IM_ASSERT(window != NULL && behind_window != NULL); + ImGuiContext& g = *GImGui; + window = window->RootWindow; + behind_window = behind_window->RootWindow; + int pos_wnd = FindWindowDisplayIndex(window); + int pos_beh = FindWindowDisplayIndex(behind_window); + if (pos_wnd < pos_beh) + { + size_t copy_bytes = (pos_beh - pos_wnd - 1) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_wnd], &g.Windows.Data[pos_wnd + 1], copy_bytes); + g.Windows[pos_beh - 1] = window; + } + else + { + size_t copy_bytes = (pos_wnd - pos_beh) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_beh + 1], &g.Windows.Data[pos_beh], copy_bytes); + g.Windows[pos_beh] = window; + } +} + +int ImGui::FindWindowDisplayIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return g.Windows.index_from_ptr(g.Windows.find(window)); +} + +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags) +{ + ImGuiContext& g = *GImGui; + + // Modal check? + if ((flags & ImGuiFocusRequestFlags_UnlessBelowModal) && (g.NavWindow != window)) // Early out in common case. + if (ImGuiWindow* blocking_modal = FindBlockingModal(window)) + { + // This block would typically be reached in two situations: + // - API call to FocusWindow() with a window under a modal and ImGuiFocusRequestFlags_UnlessBelowModal flag. + // - User clicking on void or anything behind a modal while a modal is open (window == NULL) + IMGUI_DEBUG_LOG_FOCUS("[focus] FocusWindow(\"%s\", UnlessBelowModal): prevented by \"%s\".\n", window ? window->Name : "", blocking_modal->Name); + if (window && window == window->RootWindow && (window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayBehind(window, blocking_modal); // Still bring right under modal. (FIXME: Could move in focus list too?) + ClosePopupsOverWindow(GetTopMostPopupModal(), false); // Note how we need to use GetTopMostPopupModal() aad NOT blocking_modal, to handle nested modals + return; + } + + // Find last focused child (if any) and focus it instead. + if ((flags & ImGuiFocusRequestFlags_RestoreFocusedChild) && window != NULL) + window = NavRestoreLastChildNavWindow(window); + + // Apply focus + if (g.NavWindow != window) + { + SetNavWindow(window); + if (window && g.NavHighlightItemUnderNav) + g.NavMousePosDirty = true; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavLayer = ImGuiNavLayer_Main; + SetNavFocusScope(window ? window->NavRootFocusScopeId : 0); + g.NavIdIsAlive = false; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + + // Close popups if any + ClosePopupsOverWindow(window, false); + } + + // Move the root window to the top of the pile + IM_ASSERT(window == NULL || window->RootWindow != NULL); + ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; // NB: In docking branch this is window->RootWindowDockStop + ImGuiWindow* display_front_window = window ? window->RootWindow : NULL; + + // Steal active widgets. Some of the cases it triggers includes: + // - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run. + // - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId) + if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != focus_front_window) + if (!g.ActiveIdNoClearOnFocusLoss) + ClearActiveID(); + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + + // Bring to front + BringWindowToFocusFront(focus_front_window); + if (((window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayFront(display_front_window); +} + +void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(filter_viewport); // Unused in master branch. + int start_idx = g.WindowsFocusOrder.Size - 1; + if (under_this_window != NULL) + { + // Aim at root window behind us, if we are in a child window that's our own root (see #4640) + int offset = -1; + while (under_this_window->Flags & ImGuiWindowFlags_ChildWindow) + { + under_this_window = under_this_window->ParentWindow; + offset = 0; + } + start_idx = FindWindowFocusIndex(under_this_window) + offset; + } + for (int i = start_idx; i >= 0; i--) + { + // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. + ImGuiWindow* window = g.WindowsFocusOrder[i]; + if (window == ignore_window || !window->WasActive) + continue; + if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) + { + FocusWindow(window, flags); + return; + } + } + FocusWindow(NULL, flags); +} + +//----------------------------------------------------------------------------- +// [SECTION] KEYBOARD/GAMEPAD NAVIGATION +//----------------------------------------------------------------------------- + +// FIXME-NAV: The existence of SetNavID vs SetFocusID vs FocusWindow() needs to be clarified/reworked. +// In our terminology those should be interchangeable, yet right now this is super confusing. +// Those two functions are merely a legacy artifact, so at minimum naming should be clarified. + +void ImGui::SetNavCursorVisible(bool visible) +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigNavCursorVisibleAlways) + visible = true; + g.NavCursorVisible = visible; +} + +// (was called NavRestoreHighlightAfterMove() before 1.91.4) +void ImGui::SetNavCursorVisibleAfterMove() +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = true; + g.NavHighlightItemUnderNav = g.NavMousePosDirty = true; +} + +void ImGui::SetNavWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.NavWindow != window) + { + IMGUI_DEBUG_LOG_FOCUS("[focus] SetNavWindow(\"%s\")\n", window ? window->Name : ""); + g.NavWindow = window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + g.NavInitRequest = g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavHighlightActivated(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavHighlightActivatedId = id; + g.NavHighlightActivatedTimer = NAV_ACTIVATE_HIGHLIGHT_TIMER; +} + +void ImGui::NavClearPreferredPosForAxis(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer][axis] = FLT_MAX; +} + +void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + IM_ASSERT(nav_layer == ImGuiNavLayer_Main || nav_layer == ImGuiNavLayer_Menu); + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(focus_scope_id); + g.NavWindow->NavLastIds[nav_layer] = id; + g.NavWindow->NavRectRel[nav_layer] = rect_rel; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + if (g.NavWindow != window) + SetNavWindow(window); + + // Assume that SetFocusID() is called in the context where its window->DC.NavLayerCurrent and g.CurrentFocusScopeId are valid. + // Note that window may be != g.CurrentWindow (e.g. SetFocusID call in InputTextEx for multi-line text) + const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(g.CurrentFocusScopeId); + window->NavLastIds[nav_layer] = id; + if (g.LastItemData.ID == id) + window->NavRectRel[nav_layer] = WindowRectAbsToRel(window, g.LastItemData.NavRect); + + if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + g.NavHighlightItemUnderNav = true; + else if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +static ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy) +{ + if (ImFabs(dx) > ImFabs(dy)) + return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; + return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; +} + +static float inline NavScoreItemDistInterval(float cand_min, float cand_max, float curr_min, float curr_max) +{ + if (cand_max < curr_min) + return cand_max - curr_min; + if (curr_max < cand_min) + return cand_min - curr_max; + return 0.0f; +} + +// Scoring function for keyboard/gamepad directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool ImGui::NavScoreItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavLayer != window->DC.NavLayerCurrent) + return false; + + // FIXME: Those are not good variables names + ImRect cand = g.LastItemData.NavRect; // Current item nav rectangle + const ImRect curr = g.NavScoringRect; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringDebugCount++; + + // When entering through a NavFlattened border, we consider child window items as fully clipped for scoring + if (window->ParentWindow == g.NavWindow) + { + IM_ASSERT((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened); + if (!window->ClipRect.Overlaps(cand)) + return false; + cand.ClipWithFull(window->ClipRect); // This allows the scored item to not overlap other candidates in the parent window + } + + // Compute distance between boxes + // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. + float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); + float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items + if (dby != 0.0f && dbx != 0.0f) + dbx = (dbx / 1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + float dist_box = ImFabs(dbx) + ImFabs(dby); + + // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) + float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); + float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); + float dist_center = ImFabs(dcx) + ImFabs(dcy); // L1 metric (need this for our connectedness guarantee) + + // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance + ImGuiDir quadrant; + float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; + if (dbx != 0.0f || dby != 0.0f) + { + // For non-overlapping boxes, use distance between boxes + // FIXME-NAV: Quadrant may be incorrect because of (1) dbx bias and (2) curr.Max.y bias applied by NavBiasScoringRect() where typically curr.Max.y==curr.Min.y + // One typical case where this happens, with style.WindowMenuButtonPosition == ImGuiDir_Right, pressing Left to navigate from Close to Collapse tends to fail. + // Also see #6344. Calling ImGetDirQuadrantFromDelta() with unbiased values may be good but side-effects are plenty. + dax = dbx; + day = dby; + dist_axial = dist_box; + quadrant = ImGetDirQuadrantFromDelta(dbx, dby); + } + else if (dcx != 0.0f || dcy != 0.0f) + { + // For overlapping boxes with different centers, use distance between centers + dax = dcx; + day = dcy; + dist_axial = dist_center; + quadrant = ImGetDirQuadrantFromDelta(dcx, dcy); + } + else + { + // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) + quadrant = (g.LastItemData.ID < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + } + + const ImGuiDir move_dir = g.NavMoveDir; +#if IMGUI_DEBUG_NAV_SCORING + char buf[200]; + if (g.IO.KeyCtrl) // Hold CTRL to preview score in matching quadrant. CTRL+Arrow to rotate. + { + if (quadrant == move_dir) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 80)); + draw_list->AddRectFilled(cand.Min, cand.Min + CalcTextSize(buf), IM_COL32(255, 0, 0, 200)); + draw_list->AddText(cand.Min, IM_COL32(255, 255, 255, 255), buf); + } + } + const bool debug_hovering = IsMouseHoveringRect(cand.Min, cand.Max); + const bool debug_tty = (g.IO.KeyCtrl && IsKeyPressed(ImGuiKey_Space)); + if (debug_hovering || debug_tty) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), + "d-box (%7.3f,%7.3f) -> %7.3f\nd-center (%7.3f,%7.3f) -> %7.3f\nd-axial (%7.3f,%7.3f) -> %7.3f\nnav %c, quadrant %c", + dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "-WENS"[move_dir+1], "-WENS"[quadrant+1]); + if (debug_hovering) + { + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100)); + draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255, 255, 0, 200)); + draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40, 0, 0, 200)); + draw_list->AddText(cand.Max, ~0U, buf); + } + if (debug_tty) { IMGUI_DEBUG_LOG_NAV("id 0x%08X\n%s\n", g.LastItemData.ID, buf); } + } +#endif + + // Is it in the quadrant we're interested in moving to? + bool new_best = false; + if (quadrant == move_dir) + { + // Does it beat the current best candidate? + if (dist_box < result->DistBox) + { + result->DistBox = dist_box; + result->DistCenter = dist_center; + return true; + } + if (dist_box == result->DistBox) + { + // Try using distance between center points to break ties + if (dist_center < result->DistCenter) + { + result->DistCenter = dist_center; + new_best = true; + } + else if (dist_center == result->DistCenter) + { + // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items + // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), + // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. + if (((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance + new_best = true; + } + } + } + + // Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches + // are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness) + // This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too. + // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. + // Disabling it may lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? + if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match + if (g.NavLayer == ImGuiNavLayer_Menu && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + if ((move_dir == ImGuiDir_Left && dax < 0.0f) || (move_dir == ImGuiDir_Right && dax > 0.0f) || (move_dir == ImGuiDir_Up && day < 0.0f) || (move_dir == ImGuiDir_Down && day > 0.0f)) + { + result->DistAxial = dist_axial; + new_best = true; + } + + return new_best; +} + +static void ImGui::NavApplyItemToResult(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + result->Window = window; + result->ID = g.LastItemData.ID; + result->FocusScopeId = g.CurrentFocusScopeId; + result->ItemFlags = g.LastItemData.ItemFlags; + result->RectRel = WindowRectAbsToRel(window, g.LastItemData.NavRect); + if (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + result->SelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } +} + +// True when current work location may be scrolled horizontally when moving left / right. +// This is generally always true UNLESS within a column. We don't have a vertical equivalent. +void ImGui::NavUpdateCurrentWindowIsScrollPushableX() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.NavIsScrollPushableX = (g.CurrentTable == NULL && window->DC.CurrentColumns == NULL); +} + +// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) +// This is called after LastItemData is set, but NextItemData is also still valid. +static void ImGui::NavProcessItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = g.LastItemData.ID; + const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags; + + // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221) + if (window->DC.NavIsScrollPushableX == false) + { + g.LastItemData.NavRect.Min.x = ImClamp(g.LastItemData.NavRect.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + g.LastItemData.NavRect.Max.x = ImClamp(g.LastItemData.NavRect.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + } + const ImRect nav_bb = g.LastItemData.NavRect; + + // Process Init Request + if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent && (item_flags & ImGuiItemFlags_Disabled) == 0) + { + // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback + const bool candidate_for_nav_default_focus = (item_flags & ImGuiItemFlags_NoNavDefaultFocus) == 0; + if (candidate_for_nav_default_focus || g.NavInitResult.ID == 0) + { + NavApplyItemToResult(&g.NavInitResult); + } + if (candidate_for_nav_default_focus) + { + g.NavInitRequest = false; // Found a match, clear request + NavUpdateAnyRequestFlag(); + } + } + + // Process Move Request (scoring for navigation) + // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy) + if (g.NavMoveScoringItems && (item_flags & ImGuiItemFlags_Disabled) == 0) + { + if ((g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi) || (window->Flags & ImGuiWindowFlags_NoNavInputs) == 0) + { + const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + if (is_tabbing) + { + NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags); + } + else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) + { + ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (NavScoreItem(result)) + NavApplyItemToResult(result); + + // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. + const float VISIBLE_RATIO = 0.70f; + if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) + if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) + if (NavScoreItem(&g.NavMoveResultLocalVisible)) + NavApplyItemToResult(&g.NavMoveResultLocalVisible); + } + } + } + + // Update information for currently focused/navigated item + if (g.NavId == id) + { + if (g.NavWindow != window) + SetNavWindow(window); // Always refresh g.NavWindow, because some operations such as FocusItem() may not have a window. + g.NavLayer = window->DC.NavLayerCurrent; + SetNavFocusScope(g.CurrentFocusScopeId); // Will set g.NavFocusScopeId AND store g.NavFocusScopePath + g.NavFocusScopeId = g.CurrentFocusScopeId; + g.NavIdIsAlive = true; + if (g.LastItemData.ItemFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + g.NavLastValidSelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } + window->NavRectRel[window->DC.NavLayerCurrent] = WindowRectAbsToRel(window, nav_bb); // Store item bounding box (relative to window position) + } +} + +// Handle "scoring" of an item for a tabbing/focusing request initiated by NavUpdateCreateTabbingRequest(). +// Note that SetKeyboardFocusHere() API calls are considered tabbing requests! +// - Case 1: no nav/active id: set result to first eligible item, stop storing. +// - Case 2: tab forward: on ref id set counter, on counter elapse store result +// - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. // FIXME-TABBING: Could be done as a next-frame forwarded request +// - Case 4: tab backward: store all results, on ref id pick prev, stop storing +// - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last // FIXME-TABBING: Could be done as next-frame forwarded requested +void ImGui::NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags) +{ + ImGuiContext& g = *GImGui; + + if ((move_flags & ImGuiNavMoveFlags_FocusApi) == 0) + { + if (g.NavLayer != g.CurrentWindow->DC.NavLayerCurrent) + return; + if (g.NavFocusScopeId != g.CurrentFocusScopeId) + return; + } + + // - Can always land on an item when using API call. + // - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item. + // - Tabbing without _NavEnableKeyboard: goes through inputable items only. + bool can_stop; + if (move_flags & ImGuiNavMoveFlags_FocusApi) + can_stop = true; + else + can_stop = (item_flags & ImGuiItemFlags_NoTabStop) == 0 && ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) || (item_flags & ImGuiItemFlags_Inputable)); + + // Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows) + ImGuiNavItemData* result = &g.NavMoveResultLocal; + if (g.NavTabbingDir == +1) + { + // Tab Forward or SetKeyboardFocusHere() with >= 0 + if (can_stop && g.NavTabbingResultFirst.ID == 0) + NavApplyItemToResult(&g.NavTabbingResultFirst); + if (can_stop && g.NavTabbingCounter > 0 && --g.NavTabbingCounter == 0) + NavMoveRequestResolveWithLastItem(result); + else if (g.NavId == id) + g.NavTabbingCounter = 1; + } + else if (g.NavTabbingDir == -1) + { + // Tab Backward + if (g.NavId == id) + { + if (result->ID) + { + g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); + } + } + else if (can_stop) + { + // Keep applying until reaching NavId + NavApplyItemToResult(result); + } + } + else if (g.NavTabbingDir == 0) + { + if (can_stop && g.NavId == id) + NavMoveRequestResolveWithLastItem(result); + if (can_stop && g.NavTabbingResultFirst.ID == 0) // Tab init + NavApplyItemToResult(&g.NavTabbingResultFirst); + } +} + +bool ImGui::NavMoveRequestButNoResultYet() +{ + ImGuiContext& g = *GImGui; + return g.NavMoveScoringItems && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +// FIXME: ScoringRect is not set +void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + //IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestSubmit: dir %c, window \"%s\"\n", "-WENS"[move_dir + 1], g.NavWindow->Name); + + if (move_flags & ImGuiNavMoveFlags_IsTabbing) + move_flags |= ImGuiNavMoveFlags_AllowCurrentNavId; + + g.NavMoveSubmitted = g.NavMoveScoringItems = true; + g.NavMoveDir = move_dir; + g.NavMoveDirForDebug = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags; + g.NavMoveScrollFlags = scroll_flags; + g.NavMoveForwardToNextFrame = false; + g.NavMoveKeyMods = (move_flags & ImGuiNavMoveFlags_FocusApi) ? 0 : g.IO.KeyMods; + g.NavMoveResultLocal.Clear(); + g.NavMoveResultLocalVisible.Clear(); + g.NavMoveResultOther.Clear(); + g.NavTabbingCounter = 0; + g.NavTabbingResultFirst.Clear(); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; // Ensure request doesn't need more processing + NavApplyItemToResult(result); + NavUpdateAnyRequestFlag(); +} + +// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere +void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; + g.LastItemData.ID = tree_node_data->ID; + g.LastItemData.ItemFlags = tree_node_data->ItemFlags & ~ImGuiItemFlags_HasSelectionUserData; // Losing SelectionUserData, recovered next-frame (cheaper). + g.LastItemData.NavRect = tree_node_data->NavRect; + NavApplyItemToResult(result); // Result this instead of implementing a NavApplyPastTreeNodeToResult() + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestCancel() +{ + ImGuiContext& g = *GImGui; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +// Forward will reuse the move request again on the next frame (generally with modifications done to it) +void ImGui::NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavMoveForwardToNextFrame == false); + NavMoveRequestCancel(); + g.NavMoveForwardToNextFrame = true; + g.NavMoveDir = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags | ImGuiNavMoveFlags_Forwarded; + g.NavMoveScrollFlags = scroll_flags; +} + +// Navigation wrap-around logic is delayed to the end of the frame because this operation is only valid after entire +// popup is assembled and in case of appended popups it is not clear which EndPopup() call is final. +void ImGui::NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags wrap_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT((wrap_flags & ImGuiNavMoveFlags_WrapMask_ ) != 0 && (wrap_flags & ~ImGuiNavMoveFlags_WrapMask_) == 0); // Call with _WrapX, _WrapY, _LoopX, _LoopY + + // In theory we should test for NavMoveRequestButNoResultYet() but there's no point doing it: + // as NavEndFrame() will do the same test. It will end up calling NavUpdateCreateWrappingRequest(). + if (g.NavWindow == window && g.NavMoveScoringItems && g.NavLayer == ImGuiNavLayer_Main) + g.NavMoveFlags = (g.NavMoveFlags & ~ImGuiNavMoveFlags_WrapMask_) | wrap_flags; +} + +// FIXME: This could be replaced by updating a frame number in each window when (window == NavWindow) and (NavLayer == 0). +// This way we could find the last focused window among our children. It would be much less confusing this way? +static void ImGui::NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window) +{ + ImGuiWindow* parent = nav_window; + while (parent && parent->RootWindow != parent && (parent->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent = parent->ParentWindow; + if (parent && parent != nav_window) + parent->NavLastChildNavWindow = nav_window; +} + +// Restore the last focused child. +// Call when we are expected to land on the Main Layer (0) after FocusWindow() +static ImGuiWindow* ImGui::NavRestoreLastChildNavWindow(ImGuiWindow* window) +{ + if (window->NavLastChildNavWindow && window->NavLastChildNavWindow->WasActive) + return window->NavLastChildNavWindow; + return window; +} + +void ImGui::NavRestoreLayer(ImGuiNavLayer layer) +{ + ImGuiContext& g = *GImGui; + if (layer == ImGuiNavLayer_Main) + { + ImGuiWindow* prev_nav_window = g.NavWindow; + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); // FIXME-NAV: Should clear ongoing nav requests? + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + if (prev_nav_window) + IMGUI_DEBUG_LOG_FOCUS("[focus] NavRestoreLayer: from \"%s\" to SetNavWindow(\"%s\")\n", prev_nav_window->Name, g.NavWindow->Name); + } + ImGuiWindow* window = g.NavWindow; + if (window->NavLastIds[layer] != 0) + { + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + } + else + { + g.NavLayer = layer; + NavInitWindow(window, true); + } +} + +static inline void ImGui::NavUpdateAnyRequestFlag() +{ + ImGuiContext& g = *GImGui; + g.NavAnyRequest = g.NavMoveScoringItems || g.NavInitRequest || (IMGUI_DEBUG_NAV_SCORING && g.NavWindow != NULL); + if (g.NavAnyRequest) + IM_ASSERT(g.NavWindow != NULL); +} + +// This needs to be called before we submit any widget (aka in or before Begin) +void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == g.NavWindow); + + if (window->Flags & ImGuiWindowFlags_NoNavInputs) + { + g.NavId = 0; + SetNavFocusScope(window->NavRootFocusScopeId); + return; + } + + bool init_for_nav = false; + if (window == window->RootWindow || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) + init_for_nav = true; + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from NavInitWindow(), init_for_nav=%d, window=\"%s\", layer=%d\n", init_for_nav, window->Name, g.NavLayer); + if (init_for_nav) + { + SetNavID(0, g.NavLayer, window->NavRootFocusScopeId, ImRect()); + g.NavInitRequest = true; + g.NavInitRequestFromMove = false; + g.NavInitResult.ID = 0; + NavUpdateAnyRequestFlag(); + } + else + { + g.NavId = window->NavLastIds[0]; + SetNavFocusScope(window->NavRootFocusScopeId); + } +} + +static ImGuiInputSource ImGui::NavCalcPreferredRefPosSource() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID; + + // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag. + if ((!g.NavCursorVisible || !g.NavHighlightItemUnderNav || !window) && !activated_shortcut) + return ImGuiInputSource_Mouse; + else + return ImGuiInputSource_Keyboard; // or Nav in general +} + +static ImVec2 ImGui::NavCalcPreferredRefPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + ImGuiInputSource source = NavCalcPreferredRefPosSource(); + + const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID; + + // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag. + if (source == ImGuiInputSource_Mouse) + { + // Mouse (we need a fallback in case the mouse becomes invalid after being used) + // The +1.0f offset when stored by OpenPopupEx() allows reopening this or another popup (same or another mouse button) while not moving the mouse, it is pretty standard. + // In theory we could move that +1.0f offset in OpenPopupEx() + ImVec2 p = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : g.MouseLastValidPos; + return ImVec2(p.x + 1.0f, p.y); + } + else + { + // When navigation is active and mouse is disabled, pick a position around the bottom left of the currently navigated item + ImRect ref_rect; + if (activated_shortcut) + ref_rect = g.LastItemData.NavRect; + else + ref_rect = WindowRectRelToAbs(window, window->NavRectRel[g.NavLayer]); + + // Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?) + if (window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX)) + { + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ref_rect.Translate(window->Scroll - next_scroll); + } + ImVec2 pos = ImVec2(ref_rect.Min.x + ImMin(g.Style.FramePadding.x * 4, ref_rect.GetWidth()), ref_rect.Max.y - ImMin(g.Style.FramePadding.y, ref_rect.GetHeight())); + ImGuiViewport* viewport = GetMainViewport(); + return ImTrunc(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImTrunc() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. + } +} + +float ImGui::GetNavTweakPressedAmount(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(ImGuiInputFlags_RepeatRateNavTweak, &repeat_delay, &repeat_rate); + + ImGuiKey key_less, key_more; + if (g.NavInputSource == ImGuiInputSource_Gamepad) + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadLeft : ImGuiKey_GamepadDpadUp; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadRight : ImGuiKey_GamepadDpadDown; + } + else + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_LeftArrow : ImGuiKey_UpArrow; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_RightArrow : ImGuiKey_DownArrow; + } + float amount = (float)GetKeyPressedAmount(key_more, repeat_delay, repeat_rate) - (float)GetKeyPressedAmount(key_less, repeat_delay, repeat_rate); + if (amount != 0.0f && IsKeyDown(key_less) && IsKeyDown(key_more)) // Cancel when opposite directions are held, regardless of repeat phase + amount = 0.0f; + return amount; +} + +static void ImGui::NavUpdate() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + io.WantSetMousePos = false; + //if (g.NavScoringDebugCount > 0) IMGUI_DEBUG_LOG_NAV("[nav] NavScoringDebugCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.NavScoringDebugCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); + + // Set input source based on which keys are last pressed (as some features differs when used with Gamepad vs Keyboard) + // FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource? + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const ImGuiKey nav_gamepad_keys_to_change_source[] = { ImGuiKey_GamepadFaceRight, ImGuiKey_GamepadFaceLeft, ImGuiKey_GamepadFaceUp, ImGuiKey_GamepadFaceDown, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown }; + if (nav_gamepad_active) + for (ImGuiKey key : nav_gamepad_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Gamepad; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const ImGuiKey nav_keyboard_keys_to_change_source[] = { ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow }; + if (nav_keyboard_active) + for (ImGuiKey key : nav_keyboard_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Keyboard; + + // Process navigation init request (select first/default focus) + g.NavJustMovedToId = 0; + g.NavJustMovedToFocusScopeId = g.NavJustMovedFromFocusScopeId = 0; + if (g.NavInitResult.ID != 0) + NavInitRequestApplyResult(); + g.NavInitRequest = false; + g.NavInitRequestFromMove = false; + g.NavInitResult.ID = 0; + + // Process navigation move request + if (g.NavMoveSubmitted) + NavMoveRequestApplyResult(); + g.NavTabbingCounter = 0; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + if (g.NavCursorHideFrames > 0) + if (--g.NavCursorHideFrames == 0) + g.NavCursorVisible = true; + + // Schedule mouse position update (will be done at the bottom of this function, after 1) processing all move requests and 2) updating scrolling) + bool set_mouse_pos = false; + if (g.NavMousePosDirty && g.NavIdIsAlive) + if (g.NavCursorVisible && g.NavHighlightItemUnderNav && g.NavWindow) + set_mouse_pos = true; + g.NavMousePosDirty = false; + IM_ASSERT(g.NavLayer == ImGuiNavLayer_Main || g.NavLayer == ImGuiNavLayer_Menu); + + // Store our return window (for returning from Menu Layer to Main Layer) and clear it as soon as we step back in our own Layer 0 + if (g.NavWindow) + NavSaveLastChildNavWindowIntoParent(g.NavWindow); + if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == ImGuiNavLayer_Main) + g.NavWindow->NavLastChildNavWindow = NULL; + + // Update CTRL+TAB and Windowing features (hold Square to move/resize/etc.) + NavUpdateWindowing(); + + // Set output flags for user application + io.NavActive = (nav_keyboard_active || nav_gamepad_active) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); + io.NavVisible = (io.NavActive && g.NavId != 0 && g.NavCursorVisible) || (g.NavWindowingTarget != NULL); + + // Process NavCancel input (to close a popup, get back to parent, clear focus) + NavUpdateCancelRequest(); + + // Process manual activation request + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = 0; + g.NavActivateFlags = ImGuiActivateFlags_None; + if (g.NavId != 0 && g.NavCursorVisible && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + { + const bool activate_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Space, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadActivate, ImGuiKeyOwner_NoOwner)); + const bool activate_pressed = activate_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Space, 0, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, 0, ImGuiKeyOwner_NoOwner))); + const bool input_down = (nav_keyboard_active && (IsKeyDown(ImGuiKey_Enter, ImGuiKeyOwner_NoOwner) || IsKeyDown(ImGuiKey_KeypadEnter, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadInput, ImGuiKeyOwner_NoOwner)); + const bool input_pressed = input_down && ((nav_keyboard_active && (IsKeyPressed(ImGuiKey_Enter, 0, ImGuiKeyOwner_NoOwner) || IsKeyPressed(ImGuiKey_KeypadEnter, 0, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadInput, 0, ImGuiKeyOwner_NoOwner))); + if (g.ActiveId == 0 && activate_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferTweak; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && input_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_down || input_down)) + g.NavActivateDownId = g.NavId; + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_pressed || input_pressed)) + { + g.NavActivatePressedId = g.NavId; + NavHighlightActivated(g.NavId); + } + } + if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + g.NavCursorVisible = false; + else if (g.IO.ConfigNavCursorVisibleAlways && g.NavCursorHideFrames == 0) + g.NavCursorVisible = true; + if (g.NavActivateId != 0) + IM_ASSERT(g.NavActivateDownId == g.NavActivateId); + + // Highlight + if (g.NavHighlightActivatedTimer > 0.0f) + g.NavHighlightActivatedTimer = ImMax(0.0f, g.NavHighlightActivatedTimer - io.DeltaTime); + if (g.NavHighlightActivatedTimer == 0.0f) + g.NavHighlightActivatedId = 0; + + // Process programmatic activation request + // FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others) + if (g.NavNextActivateId != 0) + { + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavNextActivateId; + g.NavActivateFlags = g.NavNextActivateFlags; + } + g.NavNextActivateId = 0; + + // Process move requests + NavUpdateCreateMoveRequest(); + if (g.NavMoveDir == ImGuiDir_None) + NavUpdateCreateTabbingRequest(); + NavUpdateAnyRequestFlag(); + g.NavIdIsAlive = false; + + // Scrolling + if (g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) + { + // *Fallback* manual-scroll with Nav directional keys when window has no navigable item + ImGuiWindow* window = g.NavWindow; + const float scroll_speed = IM_ROUND(window->FontRefSize * 100 * io.DeltaTime); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. + const ImGuiDir move_dir = g.NavMoveDir; + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY && move_dir != ImGuiDir_None) + { + if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) + SetScrollX(window, ImTrunc(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) + SetScrollY(window, ImTrunc(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + } + + // *Normal* Manual scroll with LStick + // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. + if (nav_gamepad_active) + { + const ImVec2 scroll_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + const float tweak_factor = IsKeyDown(ImGuiKey_NavGamepadTweakSlow) ? 1.0f / 10.0f : IsKeyDown(ImGuiKey_NavGamepadTweakFast) ? 10.0f : 1.0f; + if (scroll_dir.x != 0.0f && window->ScrollbarX) + SetScrollX(window, ImTrunc(window->Scroll.x + scroll_dir.x * scroll_speed * tweak_factor)); + if (scroll_dir.y != 0.0f) + SetScrollY(window, ImTrunc(window->Scroll.y + scroll_dir.y * scroll_speed * tweak_factor)); + } + } + + // Always prioritize mouse highlight if navigation is disabled + if (!nav_keyboard_active && !nav_gamepad_active) + { + g.NavCursorVisible = false; + g.NavHighlightItemUnderNav = set_mouse_pos = false; + } + + // Update mouse position if requested + // (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied) + if (set_mouse_pos && io.ConfigNavMoveSetMousePos && (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) + TeleportMousePos(NavCalcPreferredRefPos()); + + // [DEBUG] + g.NavScoringDebugCount = 0; +#if IMGUI_DEBUG_NAV_RECTS + if (ImGuiWindow* debug_window = g.NavWindow) + { + ImDrawList* draw_list = GetForegroundDrawList(debug_window); + int layer = g.NavLayer; /* for (int layer = 0; layer < 2; layer++)*/ { ImRect r = WindowRectRelToAbs(debug_window, debug_window->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 200, 0, 255)); } + //if (1) { ImU32 col = (!debug_window->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } + } +#endif +} + +void ImGui::NavInitRequestApplyResult() +{ + // In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void) + ImGuiContext& g = *GImGui; + if (!g.NavWindow) + return; + + ImGuiNavItemData* result = &g.NavInitResult; + if (g.NavId != result->ID) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = 0; + g.NavJustMovedToIsTabbing = false; + g.NavJustMovedToHasSelectionData = (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + } + + // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) + // FIXME-NAV: On _NavFlattened windows, g.NavWindow will only be updated during subsequent frame. Not a problem currently. + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: ApplyResult: NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + g.NavIdIsAlive = true; // Mark as alive from previous frame as we got a result + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + if (g.NavInitRequestFromMove) + SetNavCursorVisibleAfterMove(); +} + +// Bias scoring rect ahead of scoring + update preferred pos (if missing) using source position +static void NavBiasScoringRect(ImRect& r, ImVec2& preferred_pos_rel, ImGuiDir move_dir, ImGuiNavMoveFlags move_flags) +{ + // Bias initial rect + ImGuiContext& g = *GImGui; + const ImVec2 rel_to_abs_offset = g.NavWindow->DC.CursorStartPos; + + // Initialize bias on departure if we don't have any. So mouse-click + arrow will record bias. + // - We default to L/U bias, so moving down from a large source item into several columns will land on left-most column. + // - But each successful move sets new bias on one axis, only cleared when using mouse. + if ((move_flags & ImGuiNavMoveFlags_Forwarded) == 0) + { + if (preferred_pos_rel.x == FLT_MAX) + preferred_pos_rel.x = ImMin(r.Min.x + 1.0f, r.Max.x) - rel_to_abs_offset.x; + if (preferred_pos_rel.y == FLT_MAX) + preferred_pos_rel.y = r.GetCenter().y - rel_to_abs_offset.y; + } + + // Apply general bias on the other axis + if ((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) && preferred_pos_rel.x != FLT_MAX) + r.Min.x = r.Max.x = preferred_pos_rel.x + rel_to_abs_offset.x; + else if ((move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) && preferred_pos_rel.y != FLT_MAX) + r.Min.y = r.Max.y = preferred_pos_rel.y + rel_to_abs_offset.y; +} + +void ImGui::NavUpdateCreateMoveRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiWindow* window = g.NavWindow; + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + + if (g.NavMoveForwardToNextFrame && window != NULL) + { + // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) + // (preserve most state, which were already set by the NavMoveRequestForward() function) + IM_ASSERT(g.NavMoveDir != ImGuiDir_None && g.NavMoveClipDir != ImGuiDir_None); + IM_ASSERT(g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded); + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestForward %d\n", g.NavMoveDir); + } + else + { + // Initiate directional inputs request + g.NavMoveDir = ImGuiDir_None; + g.NavMoveFlags = ImGuiNavMoveFlags_None; + g.NavMoveScrollFlags = ImGuiScrollFlags_None; + if (window && !g.NavWindowingTarget && !(window->Flags & ImGuiWindowFlags_NoNavInputs)) + { + const ImGuiInputFlags repeat_mode = ImGuiInputFlags_Repeat | (ImGuiInputFlags)ImGuiInputFlags_RepeatRateNavMove; + if (!IsActiveIdUsingNavDir(ImGuiDir_Left) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadLeft, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_LeftArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Left; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Right) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadRight, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_RightArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Right; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Up) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadUp, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_UpArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Up; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadDown, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_DownArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Down; } + } + g.NavMoveClipDir = g.NavMoveDir; + g.NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + } + + // Update PageUp/PageDown/Home/End scroll + // FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags_NavEnableKeyboard flag? + float scoring_rect_offset_y = 0.0f; + if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active) + scoring_rect_offset_y = NavUpdatePageUpPageDown(); + if (scoring_rect_offset_y != 0.0f) + { + g.NavScoringNoClipRect = window->InnerRect; + g.NavScoringNoClipRect.TranslateY(scoring_rect_offset_y); + } + + // [DEBUG] Always send a request when holding CTRL. Hold CTRL + Arrow change the direction. +#if IMGUI_DEBUG_NAV_SCORING + //if (io.KeyCtrl && IsKeyPressed(ImGuiKey_C)) + // g.NavMoveDirForDebug = (ImGuiDir)((g.NavMoveDirForDebug + 1) & 3); + if (io.KeyCtrl) + { + if (g.NavMoveDir == ImGuiDir_None) + g.NavMoveDir = g.NavMoveDirForDebug; + g.NavMoveClipDir = g.NavMoveDir; + g.NavMoveFlags |= ImGuiNavMoveFlags_DebugNoResult; + } +#endif + + // Submit + g.NavMoveForwardToNextFrame = false; + if (g.NavMoveDir != ImGuiDir_None) + NavMoveRequestSubmit(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); + + // Moving with no reference triggers an init request (will be used as a fallback if the direction fails to find a match) + if (g.NavMoveSubmitted && g.NavId == 0) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from move, window \"%s\", layer=%d\n", window ? window->Name : "", g.NavLayer); + g.NavInitRequest = g.NavInitRequestFromMove = true; + g.NavInitResult.ID = 0; + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = true; + } + + // When using gamepad, we project the reference nav bounding box into window visible area. + // This is to allow resuming navigation inside the visible area after doing a large amount of scrolling, + // since with gamepad all movements are relative (can't focus a visible object like we can with the mouse). + if (g.NavMoveSubmitted && g.NavInputSource == ImGuiInputSource_Gamepad && g.NavLayer == ImGuiNavLayer_Main && window != NULL)// && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded)) + { + bool clamp_x = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_WrapX)) == 0; + bool clamp_y = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapY)) == 0; + ImRect inner_rect_rel = WindowRectAbsToRel(window, ImRect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1))); + + // Take account of changing scroll to handle triggering a new move request on a scrolling frame. (#6171) + // Otherwise 'inner_rect_rel' would be off on the move result frame. + inner_rect_rel.Translate(CalcNextScrollFromScrollTargetAndClamp(window) - window->Scroll); + + if ((clamp_x || clamp_y) && !inner_rect_rel.Contains(window->NavRectRel[g.NavLayer])) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: clamp NavRectRel for gamepad move\n"); + float pad_x = ImMin(inner_rect_rel.GetWidth(), window->FontRefSize * 0.5f); + float pad_y = ImMin(inner_rect_rel.GetHeight(), window->FontRefSize * 0.5f); // Terrible approximation for the intent of starting navigation from first fully visible item + inner_rect_rel.Min.x = clamp_x ? (inner_rect_rel.Min.x + pad_x) : -FLT_MAX; + inner_rect_rel.Max.x = clamp_x ? (inner_rect_rel.Max.x - pad_x) : +FLT_MAX; + inner_rect_rel.Min.y = clamp_y ? (inner_rect_rel.Min.y + pad_y) : -FLT_MAX; + inner_rect_rel.Max.y = clamp_y ? (inner_rect_rel.Max.y - pad_y) : +FLT_MAX; + window->NavRectRel[g.NavLayer].ClipWithFull(inner_rect_rel); + g.NavId = 0; + } + } + + // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) + ImRect scoring_rect; + if (window != NULL) + { + ImRect nav_rect_rel = !window->NavRectRel[g.NavLayer].IsInverted() ? window->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0); + scoring_rect = WindowRectRelToAbs(window, nav_rect_rel); + scoring_rect.TranslateY(scoring_rect_offset_y); + if (g.NavMoveSubmitted) + NavBiasScoringRect(scoring_rect, window->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir, g.NavMoveFlags); + IM_ASSERT(!scoring_rect.IsInverted()); // Ensure we have a non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem(). + //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] + //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG] + } + g.NavScoringRect = scoring_rect; + g.NavScoringNoClipRect.Add(scoring_rect); +} + +void ImGui::NavUpdateCreateTabbingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + IM_ASSERT(g.NavMoveDir == ImGuiDir_None); + if (window == NULL || g.NavWindowingTarget != NULL || (window->Flags & ImGuiWindowFlags_NoNavInputs)) + return; + + const bool tab_pressed = IsKeyPressed(ImGuiKey_Tab, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner) && !g.IO.KeyCtrl && !g.IO.KeyAlt; + if (!tab_pressed) + return; + + // Initiate tabbing request + // (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!) + // See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping. + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (nav_keyboard_active) + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.NavCursorVisible == false && g.ActiveId == 0) ? 0 : +1; + else + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1; + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down; + NavMoveRequestSubmit(ImGuiDir_None, clip_dir, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + g.NavTabbingCounter = -1; +} + +// Apply result from previous frame navigation directional move request. Always called from NavUpdate() +void ImGui::NavMoveRequestApplyResult() +{ + ImGuiContext& g = *GImGui; +#if IMGUI_DEBUG_NAV_SCORING + if (g.NavMoveFlags & ImGuiNavMoveFlags_DebugNoResult) // [DEBUG] Scoring all items in NavWindow at all times + return; +#endif + + // Select which result to use + ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL; + + // Tabbing forward wrap + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && result == NULL) + if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID) + result = &g.NavTabbingResultFirst; + + // In a situation when there are no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result) + const ImGuiAxis axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (result == NULL) + { + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavMoveFlags |= ImGuiNavMoveFlags_NoSetNavCursorVisible; + if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavCursorVisible) == 0) + SetNavCursorVisibleAfterMove(); + NavClearPreferredPosForAxis(axis); // On a failed move, clear preferred pos for this axis. + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n"); + return; + } + + // PageUp/PageDown behavior first jumps to the bottom/top mostly visible item, _otherwise_ use the result from the previous/next page. + if (g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) + if (g.NavMoveResultLocalVisible.ID != 0 && g.NavMoveResultLocalVisible.ID != g.NavId) + result = &g.NavMoveResultLocalVisible; + + // Maybe entering a flattened child from the outside? In this case solve the tie using the regular scoring rules. + if (result != &g.NavMoveResultOther && g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) + if ((g.NavMoveResultOther.DistBox < result->DistBox) || (g.NavMoveResultOther.DistBox == result->DistBox && g.NavMoveResultOther.DistCenter < result->DistCenter)) + result = &g.NavMoveResultOther; + IM_ASSERT(g.NavWindow && result->Window); + + // Scroll to keep newly navigated item fully into view. + if (g.NavLayer == ImGuiNavLayer_Main) + { + ImRect rect_abs = WindowRectRelToAbs(result->Window, result->RectRel); + ScrollToRectEx(result->Window, rect_abs, g.NavMoveScrollFlags); + + if (g.NavMoveFlags & ImGuiNavMoveFlags_ScrollToEdgeY) + { + // FIXME: Should remove this? Or make more precise: use ScrollToRectEx() with edge? + float scroll_target = (g.NavMoveDir == ImGuiDir_Up) ? result->Window->ScrollMax.y : 0.0f; + SetScrollY(result->Window, scroll_target); + } + } + + if (g.NavWindow != result->Window) + { + IMGUI_DEBUG_LOG_FOCUS("[focus] NavMoveRequest: SetNavWindow(\"%s\")\n", result->Window->Name); + g.NavWindow = result->Window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + + // Clear active id unless requested not to + // FIXME: ImGuiNavMoveFlags_NoClearActiveId is currently unused as we don't have a clear strategy to preserve active id after interaction, + // so this is mostly provided as a gateway for further experiments (see #1418, #2890) + if (g.ActiveId != result->ID && (g.NavMoveFlags & ImGuiNavMoveFlags_NoClearActiveId) == 0) + ClearActiveID(); + + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + // PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior. + if ((g.NavId != result->ID || (g.NavMoveFlags & ImGuiNavMoveFlags_IsPageMove)) && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSelect) == 0) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = g.NavMoveKeyMods; + g.NavJustMovedToIsTabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + g.NavJustMovedToHasSelectionData = (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + //IMGUI_DEBUG_LOG_NAV("[nav] NavJustMovedFromFocusScopeId = 0x%08X, NavJustMovedToFocusScopeId = 0x%08X\n", g.NavJustMovedFromFocusScopeId, g.NavJustMovedToFocusScopeId); + } + + // Apply new NavID/Focus + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + ImVec2 preferred_scoring_pos_rel = g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer]; + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + + // Restore last preferred position for current axis + // (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) == 0) + { + preferred_scoring_pos_rel[axis] = result->RectRel.GetCenter()[axis]; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer] = preferred_scoring_pos_rel; + } + + // Tabbing: Activates Inputable, otherwise only Focus + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && (result->ItemFlags & ImGuiItemFlags_Inputable) == 0) + g.NavMoveFlags &= ~ImGuiNavMoveFlags_Activate; + + // Activate + if (g.NavMoveFlags & ImGuiNavMoveFlags_Activate) + { + g.NavNextActivateId = result->ID; + g.NavNextActivateFlags = ImGuiActivateFlags_None; + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavNextActivateFlags |= ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState | ImGuiActivateFlags_FromTabbing; + } + + // Make nav cursor visible + if ((g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavCursorVisible) == 0) + SetNavCursorVisibleAfterMove(); +} + +// Process Escape/NavCancel input (to close a popup, get back to parent, clear focus) +// FIXME: In order to support e.g. Escape to clear a selection we'll need: +// - either to store the equivalent of ActiveIdUsingKeyInputMask for a FocusScope and test for it. +// - either to move most/all of those tests to the epilogue/end functions of the scope they are dealing with (e.g. exit child window in EndChild()) or in EndFrame(), to allow an earlier intercept +static void ImGui::NavUpdateCancelRequest() +{ + ImGuiContext& g = *GImGui; + const bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (g.IO.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (!(nav_keyboard_active && IsKeyPressed(ImGuiKey_Escape, 0, ImGuiKeyOwner_NoOwner)) && !(nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadCancel, 0, ImGuiKeyOwner_NoOwner))) + return; + + IMGUI_DEBUG_LOG_NAV("[nav] NavUpdateCancelRequest()\n"); + if (g.ActiveId != 0) + { + ClearActiveID(); + } + else if (g.NavLayer != ImGuiNavLayer_Main) + { + // Leave the "menu" layer + NavRestoreLayer(ImGuiNavLayer_Main); + SetNavCursorVisibleAfterMove(); + } + else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->RootWindowForNav->ParentWindow) + { + // Exit child window + ImGuiWindow* child_window = g.NavWindow->RootWindowForNav; + ImGuiWindow* parent_window = child_window->ParentWindow; + IM_ASSERT(child_window->ChildId != 0); + FocusWindow(parent_window); + SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_window->Rect())); + SetNavCursorVisibleAfterMove(); + } + else if (g.OpenPopupStack.Size > 0 && g.OpenPopupStack.back().Window != NULL && !(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) + { + // Close open popup/menu + ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); + } + else + { + // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were + // FIXME-NAV: This should happen on window appearing. + if (g.IO.ConfigNavEscapeClearFocusItem || g.IO.ConfigNavEscapeClearFocusWindow) + if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup)))// || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) + g.NavWindow->NavLastIds[0] = 0; + + // Clear nav focus + if (g.IO.ConfigNavEscapeClearFocusItem || g.IO.ConfigNavEscapeClearFocusWindow) + g.NavId = 0; + if (g.IO.ConfigNavEscapeClearFocusWindow) + FocusWindow(NULL); + } +} + +// Handle PageUp/PageDown/Home/End keys +// Called from NavUpdateCreateMoveRequest() which will use our output to create a move request +// FIXME-NAV: This doesn't work properly with NavFlattened siblings as we use NavWindow rectangle for reference +// FIXME-NAV: how to get Home/End to aim at the beginning/end of a 2D grid? +static float ImGui::NavUpdatePageUpPageDown() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + if ((window->Flags & ImGuiWindowFlags_NoNavInputs) || g.NavWindowingTarget != NULL) + return 0.0f; + + const bool page_up_held = IsKeyDown(ImGuiKey_PageUp, ImGuiKeyOwner_NoOwner); + const bool page_down_held = IsKeyDown(ImGuiKey_PageDown, ImGuiKeyOwner_NoOwner); + const bool home_pressed = IsKeyPressed(ImGuiKey_Home, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + const bool end_pressed = IsKeyPressed(ImGuiKey_End, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + if (page_up_held == page_down_held && home_pressed == end_pressed) // Proceed if either (not both) are pressed, otherwise early out + return 0.0f; + + if (g.NavLayer != ImGuiNavLayer_Main) + NavRestoreLayer(ImGuiNavLayer_Main); + + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY) + { + // Fallback manual-scroll when window has no navigable item + if (IsKeyPressed(ImGuiKey_PageUp, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y - window->InnerRect.GetHeight()); + else if (IsKeyPressed(ImGuiKey_PageDown, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y + window->InnerRect.GetHeight()); + else if (home_pressed) + SetScrollY(window, 0.0f); + else if (end_pressed) + SetScrollY(window, window->ScrollMax.y); + } + else + { + ImRect& nav_rect_rel = window->NavRectRel[g.NavLayer]; + const float page_offset_y = ImMax(0.0f, window->InnerRect.GetHeight() - window->FontRefSize * 1.0f + nav_rect_rel.GetHeight()); + float nav_scoring_rect_offset_y = 0.0f; + if (IsKeyPressed(ImGuiKey_PageUp, true)) + { + nav_scoring_rect_offset_y = -page_offset_y; + g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; + } + else if (IsKeyPressed(ImGuiKey_PageDown, true)) + { + nav_scoring_rect_offset_y = +page_offset_y; + g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; + } + else if (home_pressed) + { + // FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y + // Scrolling will be handled via the ImGuiNavMoveFlags_ScrollToEdgeY flag, we don't scroll immediately to avoid scrolling happening before nav result. + // Preserve current horizontal position if we have any. + nav_rect_rel.Min.y = nav_rect_rel.Max.y = 0.0f; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + else if (end_pressed) + { + nav_rect_rel.Min.y = nav_rect_rel.Max.y = window->ContentSize.y; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + return nav_scoring_rect_offset_y; + } + return 0.0f; +} + +static void ImGui::NavEndFrame() +{ + ImGuiContext& g = *GImGui; + + // Show CTRL+TAB list window + if (g.NavWindowingTarget != NULL) + NavUpdateWindowingOverlay(); + + // Perform wrap-around in menus + // FIXME-NAV: Wrap may need to apply a weight bias on the other axis. e.g. 4x4 grid with 2 last items missing on last item won't handle LoopY/WrapY correctly. + // FIXME-NAV: Wrap (not Loop) support could be handled by the scoring function and then WrapX would function without an extra frame. + if (g.NavWindow && NavMoveRequestButNoResultYet() && (g.NavMoveFlags & ImGuiNavMoveFlags_WrapMask_) && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + NavUpdateCreateWrappingRequest(); +} + +static void ImGui::NavUpdateCreateWrappingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + + bool do_forward = false; + ImRect bb_rel = window->NavRectRel[g.NavLayer]; + ImGuiDir clip_dir = g.NavMoveDir; + + const ImGuiNavMoveFlags move_flags = g.NavMoveFlags; + //const ImGuiAxis move_axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (g.NavMoveDir == ImGuiDir_Left && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = window->ContentSize.x + window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(-bb_rel.GetHeight()); // Previous row + clip_dir = ImGuiDir_Up; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Right && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = -window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(+bb_rel.GetHeight()); // Next row + clip_dir = ImGuiDir_Down; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Up && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = window->ContentSize.y + window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(-bb_rel.GetWidth()); // Previous column + clip_dir = ImGuiDir_Left; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Down && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = -window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(+bb_rel.GetWidth()); // Next column + clip_dir = ImGuiDir_Right; + } + do_forward = true; + } + if (!do_forward) + return; + window->NavRectRel[g.NavLayer] = bb_rel; + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags); +} + +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +// Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmatically. +// If you want a window to never be focused, you may use the e.g. NoInputs flag. +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +{ + return window->WasActive && window == window->RootWindow && !(window->Flags & ImGuiWindowFlags_NoNavFocus); +} + +static ImGuiWindow* FindWindowNavFocusable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) +{ + ImGuiContext& g = *GImGui; + for (int i = i_start; i >= 0 && i < g.WindowsFocusOrder.Size && i != i_stop; i += dir) + if (ImGui::IsWindowNavFocusable(g.WindowsFocusOrder[i])) + return g.WindowsFocusOrder[i]; + return NULL; +} + +static void NavUpdateWindowingTarget(int focus_change_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget); + if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) + return; + + const int i_current = ImGui::FindWindowFocusIndex(g.NavWindowingTarget); + ImGuiWindow* window_target = FindWindowNavFocusable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); + if (!window_target) + window_target = FindWindowNavFocusable((focus_change_dir < 0) ? (g.WindowsFocusOrder.Size - 1) : 0, i_current, focus_change_dir); + if (window_target) // Don't reset windowing target if there's a single window in the list + { + g.NavWindowingTarget = g.NavWindowingTargetAnim = window_target; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + } + g.NavWindowingToggleLayer = false; +} + +// Apply focus and close overlay +static void ImGui::NavUpdateWindowingApplyFocus(ImGuiWindow* apply_focus_window) +{ + ImGuiContext& g = *GImGui; + if (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow) + { + ClearActiveID(); + SetNavCursorVisibleAfterMove(); + ClosePopupsOverWindow(apply_focus_window, false); + FocusWindow(apply_focus_window, ImGuiFocusRequestFlags_RestoreFocusedChild); + apply_focus_window = g.NavWindow; + if (apply_focus_window->NavLastIds[0] == 0) + NavInitWindow(apply_focus_window, false); + + // If the window has ONLY a menu layer (no main layer), select it directly + // Use NavLayersActiveMaskNext since windows didn't have a chance to be Begin()-ed on this frame, + // so CTRL+Tab where the keys are only held for 1 frame will be able to use correct layers mask since + // the target window as already been previewed once. + // FIXME-NAV: This should be done in NavInit.. or in FocusWindow... However in both of those cases, + // we won't have a guarantee that windows has been visible before and therefore NavLayersActiveMask* + // won't be valid. + if (apply_focus_window->DC.NavLayersActiveMaskNext == (1 << ImGuiNavLayer_Menu)) + g.NavLayer = ImGuiNavLayer_Menu; + } + g.NavWindowingTarget = NULL; +} + +// Windowing management mode +// Keyboard: CTRL+Tab (change focus/move/resize), Alt (toggle menu layer) +// Gamepad: Hold Menu/Square (change focus/move/resize), Tap Menu/Square (toggle menu layer) +static void ImGui::NavUpdateWindowing() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + ImGuiWindow* apply_focus_window = NULL; + bool apply_toggle_layer = false; + + ImGuiWindow* modal_window = GetTopMostPopupModal(); + bool allow_windowing = (modal_window == NULL); // FIXME: This prevent CTRL+TAB from being usable with windows that are inside the Begin-stack of that modal. + if (!allow_windowing) + g.NavWindowingTarget = NULL; + + // Fade out + if (g.NavWindowingTargetAnim && g.NavWindowingTarget == NULL) + { + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - io.DeltaTime * 10.0f, 0.0f); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + g.NavWindowingTargetAnim = NULL; + } + + // Start CTRL+Tab or Square+L/R window selection + // (g.ConfigNavWindowingKeyNext/g.ConfigNavWindowingKeyPrev defaults are ImGuiMod_Ctrl|ImGuiKey_Tab and ImGuiMod_Ctrl|ImGuiMod_Shift|ImGuiKey_Tab) + const ImGuiID owner_id = ImHashStr("##NavUpdateWindowing"); + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const bool keyboard_next_window = allow_windowing && g.ConfigNavWindowingKeyNext && Shortcut(g.ConfigNavWindowingKeyNext, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool keyboard_prev_window = allow_windowing && g.ConfigNavWindowingKeyPrev && Shortcut(g.ConfigNavWindowingKeyPrev, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool start_windowing_with_gamepad = allow_windowing && nav_gamepad_active && !g.NavWindowingTarget && IsKeyPressed(ImGuiKey_NavGamepadMenu, ImGuiInputFlags_None); + const bool start_windowing_with_keyboard = allow_windowing && !g.NavWindowingTarget && (keyboard_next_window || keyboard_prev_window); // Note: enabled even without NavEnableKeyboard! + bool just_started_windowing_from_null_focus = false; + if (start_windowing_with_gamepad || start_windowing_with_keyboard) + if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1)) + { + g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; // Current location + g.NavWindowingTimer = g.NavWindowingHighlightAlpha = 0.0f; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + g.NavWindowingToggleLayer = start_windowing_with_gamepad ? true : false; // Gamepad starts toggling layer + g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad; + if (g.NavWindow == NULL) + just_started_windowing_from_null_focus = true; + + // Manually register ownership of our mods. Using a global route in the Shortcut() calls instead would probably be correct but may have more side-effects. + if (keyboard_next_window || keyboard_prev_window) + SetKeyOwnersForKeyChord((g.ConfigNavWindowingKeyNext | g.ConfigNavWindowingKeyPrev) & ImGuiMod_Mask_, owner_id); + } + + // Gamepad update + g.NavWindowingTimer += io.DeltaTime; + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Gamepad) + { + // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); + + // Select window to focus + const int focus_change_dir = (int)IsKeyPressed(ImGuiKey_GamepadL1) - (int)IsKeyPressed(ImGuiKey_GamepadR1); + if (focus_change_dir != 0 && !just_started_windowing_from_null_focus) + { + NavUpdateWindowingTarget(focus_change_dir); + g.NavWindowingHighlightAlpha = 1.0f; + } + + // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most) + if (!IsKeyDown(ImGuiKey_NavGamepadMenu)) + { + g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. + if (g.NavWindowingToggleLayer && g.NavWindow) + apply_toggle_layer = true; + else if (!g.NavWindowingToggleLayer) + apply_focus_window = g.NavWindowingTarget; + g.NavWindowingTarget = NULL; + } + } + + // Keyboard: Focus + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise + ImGuiKeyChord shared_mods = ((g.ConfigNavWindowingKeyNext ? g.ConfigNavWindowingKeyNext : ImGuiMod_Mask_) & (g.ConfigNavWindowingKeyPrev ? g.ConfigNavWindowingKeyPrev : ImGuiMod_Mask_)) & ImGuiMod_Mask_; + IM_ASSERT(shared_mods != 0); // Next/Prev shortcut currently needs a shared modifier to "hold", otherwise Prev actions would keep cycling between two windows. + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // 1.0f + if ((keyboard_next_window || keyboard_prev_window) && !just_started_windowing_from_null_focus) + NavUpdateWindowingTarget(keyboard_next_window ? -1 : +1); + else if ((io.KeyMods & shared_mods) != shared_mods) + apply_focus_window = g.NavWindowingTarget; + } + + // Keyboard: Press and Release ALT to toggle menu layer + const ImGuiKey windowing_toggle_keys[] = { ImGuiKey_LeftAlt, ImGuiKey_RightAlt }; + bool windowing_toggle_layer_start = false; + if (g.NavWindow != NULL && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + for (ImGuiKey windowing_toggle_key : windowing_toggle_keys) + if (nav_keyboard_active && IsKeyPressed(windowing_toggle_key, 0, ImGuiKeyOwner_NoOwner)) + { + windowing_toggle_layer_start = true; + g.NavWindowingToggleLayer = true; + g.NavWindowingToggleKey = windowing_toggle_key; + g.NavInputSource = ImGuiInputSource_Keyboard; + break; + } + if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370) + // We cancel toggling nav layer when other modifiers are pressed. (See #4439) + // - AltGR is Alt+Ctrl on some layout but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). + // We cancel toggling nav layer if an owner has claimed the key. + if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper) + g.NavWindowingToggleLayer = false; + else if (windowing_toggle_layer_start == false && g.LastKeyboardKeyPressTime == g.Time) + g.NavWindowingToggleLayer = false; + else if (TestKeyOwner(g.NavWindowingToggleKey, ImGuiKeyOwner_NoOwner) == false || TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_NoOwner) == false) + g.NavWindowingToggleLayer = false; + + // Apply layer toggle on Alt release + // Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss. + if (IsKeyReleased(g.NavWindowingToggleKey) && g.NavWindowingToggleLayer) + if (g.ActiveId == 0 || g.ActiveIdAllowOverlap) + if (IsMousePosValid(&io.MousePos) == IsMousePosValid(&io.MousePosPrev)) + apply_toggle_layer = true; + if (!IsKeyDown(g.NavWindowingToggleKey)) + g.NavWindowingToggleLayer = false; + } + + // Move window + if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) + { + ImVec2 nav_move_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && !io.KeyShift) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + if (nav_move_dir.x != 0.0f || nav_move_dir.y != 0.0f) + { + const float NAV_MOVE_SPEED = 800.0f; + const float move_step = NAV_MOVE_SPEED * io.DeltaTime * ImMin(io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaPos += nav_move_dir * move_step; + g.NavHighlightItemUnderNav = true; + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaPos); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindow; + SetWindowPos(moving_window, moving_window->Pos + accum_floored, ImGuiCond_Always); + g.NavWindowingAccumDeltaPos -= accum_floored; + } + } + } + + // Apply final focus + if (apply_focus_window) + NavUpdateWindowingApplyFocus(apply_focus_window); + + // Apply menu/layer toggle + if (apply_toggle_layer && g.NavWindow) + { + ClearActiveID(); + + // Move to parent menu if necessary + ImGuiWindow* new_nav_window = g.NavWindow; + while (new_nav_window->ParentWindow + && (new_nav_window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0 + && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 + && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + new_nav_window = new_nav_window->ParentWindow; + if (new_nav_window != g.NavWindow) + { + ImGuiWindow* old_nav_window = g.NavWindow; + FocusWindow(new_nav_window); + new_nav_window->NavLastChildNavWindow = old_nav_window; + } + + // Toggle layer + const ImGuiNavLayer new_nav_layer = (g.NavWindow->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) ? (ImGuiNavLayer)((int)g.NavLayer ^ 1) : ImGuiNavLayer_Main; + if (new_nav_layer != g.NavLayer) + { + // Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?) + if (new_nav_layer == ImGuiNavLayer_Menu) + g.NavWindow->NavLastIds[new_nav_layer] = 0; + NavRestoreLayer(new_nav_layer); + SetNavCursorVisibleAfterMove(); + } + } +} + +// Window has already passed the IsWindowNavFocusable() +static const char* GetFallbackWindowNameForWindowingList(ImGuiWindow* window) +{ + if (window->Flags & ImGuiWindowFlags_Popup) + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingPopup); + if ((window->Flags & ImGuiWindowFlags_MenuBar) && strcmp(window->Name, "##MainMenuBar") == 0) + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingMainMenuBar); + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingUntitled); +} + +// Overlay displayed when using CTRL+TAB. Called by EndFrame(). +void ImGui::NavUpdateWindowingOverlay() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget != NULL); + + if (g.NavWindowingTimer < NAV_WINDOWING_LIST_APPEAR_DELAY) + return; + + if (g.NavWindowingListWindow == NULL) + g.NavWindowingListWindow = FindWindowByName("##NavWindowingOverlay"); + const ImGuiViewport* viewport = GetMainViewport(); + SetNextWindowSizeConstraints(ImVec2(viewport->Size.x * 0.20f, viewport->Size.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.WindowPadding * 2.0f); + Begin("##NavWindowingOverlay", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + if (g.ContextName[0] != 0) + SeparatorText(g.ContextName); + for (int n = g.WindowsFocusOrder.Size - 1; n >= 0; n--) + { + ImGuiWindow* window = g.WindowsFocusOrder[n]; + IM_ASSERT(window != NULL); // Fix static analyzers + if (!IsWindowNavFocusable(window)) + continue; + const char* label = window->Name; + if (label == FindRenderedTextEnd(label)) + label = GetFallbackWindowNameForWindowingList(window); + Selectable(label, g.NavWindowingTarget == window); + } + End(); + PopStyleVar(); +} + +//----------------------------------------------------------------------------- +// [SECTION] DRAG AND DROP +//----------------------------------------------------------------------------- + +bool ImGui::IsDragDropActive() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive; +} + +void ImGui::ClearDragDrop() +{ + ImGuiContext& g = *GImGui; + if (g.DragDropActive) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] ClearDragDrop()\n"); + g.DragDropActive = false; + g.DragDropPayload.Clear(); + g.DragDropAcceptFlags = ImGuiDragDropFlags_None; + g.DragDropAcceptIdCurr = g.DragDropAcceptIdPrev = 0; + g.DragDropAcceptIdCurrRectSurface = FLT_MAX; + g.DragDropAcceptFrameCount = -1; + + g.DragDropPayloadBufHeap.clear(); + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); +} + +bool ImGui::BeginTooltipHidden() +{ + ImGuiContext& g = *GImGui; + bool ret = Begin("##Tooltip_Hidden", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.CurrentWindow); + return ret; +} + +// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() +// If the item has an identifier: +// - This assume/require the item to be activated (typically via ButtonBehavior). +// - Therefore if you want to use this with a mouse button other than left mouse button, it is up to the item itself to activate with another button. +// - We then pull and use the mouse button that was used to activate the item and use it to carry on the drag. +// If the item has no identifier: +// - Currently always assume left mouse button. +bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // FIXME-DRAGDROP: While in the common-most "drag from non-zero active id" case we can tell the mouse button, + // in both SourceExtern and id==0 cases we may requires something else (explicit flags or some heuristic). + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left; + + bool source_drag_active = false; + ImGuiID source_id = 0; + ImGuiID source_parent_id = 0; + if ((flags & ImGuiDragDropFlags_SourceExtern) == 0) + { + source_id = g.LastItemData.ID; + if (source_id != 0) + { + // Common path: items with ID + if (g.ActiveId != source_id) + return false; + if (g.ActiveIdMouseButton != -1) + mouse_button = g.ActiveIdMouseButton; + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + g.ActiveIdAllowOverlap = false; + } + else + { + // Uncommon path: items without ID + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window)) + return false; + + // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: + // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag. + if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) + { + IM_ASSERT(0); + return false; + } + + // Magic fallback to handle items with no assigned ID, e.g. Text(), Image() + // We build a throwaway ID based on current ID stack + relative AABB of items in window. + // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING/RESIZINGG OF THE WIDGET, so if your widget moves your dragging operation will be canceled. + // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. + // Rely on keeping other window->LastItemXXX fields intact. + source_id = g.LastItemData.ID = window->GetIDFromRectangle(g.LastItemData.Rect); + KeepAliveID(source_id); + bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id, g.LastItemData.ItemFlags); + if (is_hovered && g.IO.MouseClicked[mouse_button]) + { + SetActiveID(source_id, window); + FocusWindow(window); + } + if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. + g.ActiveIdAllowOverlap = is_hovered; + } + if (g.ActiveId != source_id) + return false; + source_parent_id = window->IDStack.back(); + source_drag_active = IsMouseDragging(mouse_button); + + // Disable navigation and key inputs while dragging + cancel existing request if any + SetActiveIdUsingAllKeyboardKeys(); + } + else + { + // When ImGuiDragDropFlags_SourceExtern is set: + window = NULL; + source_id = ImHashStr("#SourceExtern"); + source_drag_active = true; + mouse_button = g.IO.MouseDown[0] ? 0 : -1; + KeepAliveID(source_id); + SetActiveID(source_id, NULL); + } + + IM_ASSERT(g.DragDropWithinTarget == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + if (!source_drag_active) + return false; + + // Activate drag and drop + if (!g.DragDropActive) + { + IM_ASSERT(source_id != 0); + ClearDragDrop(); + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] BeginDragDropSource() DragDropActive = true, source_id = 0x%08X%s\n", + source_id, (flags & ImGuiDragDropFlags_SourceExtern) ? " (EXTERN)" : ""); + ImGuiPayload& payload = g.DragDropPayload; + payload.SourceId = source_id; + payload.SourceParentId = source_parent_id; + g.DragDropActive = true; + g.DragDropSourceFlags = flags; + g.DragDropMouseButton = mouse_button; + if (payload.SourceId == g.ActiveId) + g.ActiveIdNoClearOnFocusLoss = true; + } + g.DragDropSourceFrameCount = g.FrameCount; + g.DragDropWithinSource = true; + + if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) + // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. + bool ret; + if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) + ret = BeginTooltipHidden(); + else + ret = BeginTooltip(); + IM_ASSERT(ret); // FIXME-NEWBEGIN: If this ever becomes false, we need to Begin("##Hidden", NULL, ImGuiWindowFlags_NoSavedSettings) + SetWindowHiddendAndSkipItemsForCurrentFrame(). + IM_UNUSED(ret); + } + + if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) + g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; + + return true; +} + +void ImGui::EndDragDropSource() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinSource && "Not after a BeginDragDropSource()?"); + + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + EndTooltip(); + + // Discard the drag if have not called SetDragDropPayload() + if (g.DragDropPayload.DataFrameCount == -1) + ClearDragDrop(); + g.DragDropWithinSource = false; +} + +// Use 'cond' to choose to submit payload on drag start or every frame +bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_size, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + if (cond == 0) + cond = ImGuiCond_Always; + + IM_ASSERT(type != NULL); + IM_ASSERT(ImStrlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 32 characters long"); + IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); + IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); + IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() + + if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) + { + // Copy payload + ImStrncpy(payload.DataType, type, IM_ARRAYSIZE(payload.DataType)); + g.DragDropPayloadBufHeap.resize(0); + if (data_size > sizeof(g.DragDropPayloadBufLocal)) + { + // Store in heap + g.DragDropPayloadBufHeap.resize((int)data_size); + payload.Data = g.DragDropPayloadBufHeap.Data; + memcpy(payload.Data, data, data_size); + } + else if (data_size > 0) + { + // Store locally + memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); + payload.Data = g.DragDropPayloadBufLocal; + memcpy(payload.Data, data, data_size); + } + else + { + payload.Data = NULL; + } + payload.DataSize = (int)data_size; + } + payload.DataFrameCount = g.FrameCount; + + // Return whether the payload has been accepted + return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); +} + +bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow) + return false; + IM_ASSERT(id != 0); + if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) + return false; + if (window->SkipItems) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + g.DragDropTargetRect = bb; + g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case? + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +// We don't use BeginDragDropTargetCustom() and duplicate its code because: +// 1) we use LastItemData's ImGuiItemStatusFlags_HoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. +// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) +bool ImGui::BeginDragDropTarget() +{ + ImGuiContext& g = *GImGui; + if (!g.DragDropActive) + return false; + + ImGuiWindow* window = g.CurrentWindow; + if (!(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)) + return false; + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindow != hovered_window->RootWindow || window->SkipItems) + return false; + + const ImRect& display_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? g.LastItemData.DisplayRect : g.LastItemData.Rect; + ImGuiID id = g.LastItemData.ID; + if (id == 0) + { + id = window->GetIDFromRectangle(display_rect); + KeepAliveID(id); + } + if (g.DragDropPayload.SourceId == id) + return false; + + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + g.DragDropTargetRect = display_rect; + g.DragDropTargetClipRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; + g.DragDropTargetId = id; + g.DragDropWithinTarget = true; + return true; +} + +bool ImGui::IsDragDropPayloadBeingAccepted() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive && g.DragDropAcceptIdPrev != 0; +} + +const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiPayload& payload = g.DragDropPayload; + IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? + IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? + if (type != NULL && !payload.IsDataType(type)) + return NULL; + + // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. + // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! + const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); + ImRect r = g.DragDropTargetRect; + float r_surface = r.GetWidth() * r.GetHeight(); + if (r_surface > g.DragDropAcceptIdCurrRectSurface) + return NULL; + + g.DragDropAcceptFlags = flags; + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + //IMGUI_DEBUG_LOG("AcceptDragDropPayload(): %08X: accept\n", g.DragDropTargetId); + + // Render default drop visuals + payload.Preview = was_accepted_previously; + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that live for 1 frame) + if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) + RenderDragDropTargetRect(r, g.DragDropTargetClipRect); + + g.DragDropAcceptFrameCount = g.FrameCount; + if ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) && g.DragDropMouseButton == -1) + payload.Delivery = was_accepted_previously && (g.DragDropSourceFrameCount < g.FrameCount); + else + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting OS window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) + return NULL; + + if (payload.Delivery) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] AcceptDragDropPayload(): 0x%08X: payload delivery\n", g.DragDropTargetId); + return &payload; +} + +// FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target. +void ImGui::RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImRect bb_display = bb; + bb_display.ClipWith(item_clip_rect); // Clip THEN expand so we have a way to visualize that target is not entirely visible. + bb_display.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(bb_display); + if (push_clip_rect) + window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(bb_display.Min, bb_display.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + if (push_clip_rect) + window->DrawList->PopClipRect(); +} + +const ImGuiPayload* ImGui::GetDragDropPayload() +{ + ImGuiContext& g = *GImGui; + return (g.DragDropActive && g.DragDropPayload.DataFrameCount != -1) ? &g.DragDropPayload : NULL; +} + +void ImGui::EndDragDropTarget() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + IM_ASSERT(g.DragDropWithinTarget); + g.DragDropWithinTarget = false; + + // Clear drag and drop state payload right after delivery + if (g.DragDropPayload.Delivery) + ClearDragDrop(); +} + +//----------------------------------------------------------------------------- +// [SECTION] LOGGING/CAPTURING +//----------------------------------------------------------------------------- +// All text output from the interface can be captured into tty/file/clipboard. +// By default, tree nodes are automatically opened during logging. +//----------------------------------------------------------------------------- + +// Pass text data straight to log (without being displayed) +static inline void LogTextV(ImGuiContext& g, const char* fmt, va_list args) +{ + if (g.LogFile) + { + g.LogBuffer.Buf.resize(0); + g.LogBuffer.appendfv(fmt, args); + ImFileWrite(g.LogBuffer.c_str(), sizeof(char), (ImU64)g.LogBuffer.size(), g.LogFile); + } + else + { + g.LogBuffer.appendfv(fmt, args); + } +} + +void ImGui::LogText(const char* fmt, ...) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + va_list args; + va_start(args, fmt); + LogTextV(g, fmt, args); + va_end(args); +} + +void ImGui::LogTextV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogTextV(g, fmt, args); +} + +// Internal version that takes a position to decide on newline placement and pad items according to their depth. +// We split text into individual lines to add current tree level padding +// FIXME: This code is a little complicated perhaps, considering simplifying the whole system. +void ImGui::LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const char* prefix = g.LogNextPrefix; + const char* suffix = g.LogNextSuffix; + g.LogNextPrefix = g.LogNextSuffix = NULL; + + if (!text_end) + text_end = FindRenderedTextEnd(text, text_end); + + const bool log_new_line = ref_pos && (ref_pos->y > g.LogLinePosY + g.Style.FramePadding.y + 1); + if (ref_pos) + g.LogLinePosY = ref_pos->y; + if (log_new_line) + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + + if (prefix) + LogRenderedText(ref_pos, prefix, prefix + ImStrlen(prefix)); // Calculate end ourself to ensure "##" are included here. + + // Re-adjust padding if we have popped out of our starting depth + if (g.LogDepthRef > window->DC.TreeDepth) + g.LogDepthRef = window->DC.TreeDepth; + const int tree_depth = (window->DC.TreeDepth - g.LogDepthRef); + + const char* text_remaining = text; + for (;;) + { + // Split the string. Each new line (after a '\n') is followed by indentation corresponding to the current depth of our log entry. + // We don't add a trailing \n yet to allow a subsequent item on the same line to be captured. + const char* line_start = text_remaining; + const char* line_end = ImStreolRange(line_start, text_end); + const bool is_last_line = (line_end == text_end); + if (line_start != line_end || !is_last_line) + { + const int line_length = (int)(line_end - line_start); + const int indentation = g.LogLineFirstItem ? tree_depth * 4 : 1; + LogText("%*s%.*s", indentation, "", line_length, line_start); + g.LogLineFirstItem = false; + if (*line_end == '\n') + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } + } + if (is_last_line) + break; + text_remaining = line_end + 1; + } + + if (suffix) + LogRenderedText(ref_pos, suffix, suffix + ImStrlen(suffix)); +} + +// Start logging/capturing text output +void ImGui::LogBegin(ImGuiLogFlags flags, int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.LogEnabled == false); + IM_ASSERT(g.LogFile == NULL && g.LogBuffer.empty()); + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiLogFlags_OutputMask_)); // Check that only 1 type flag is used + + g.LogEnabled = g.ItemUnclipByLog = true; + g.LogFlags = flags; + g.LogWindow = window; + g.LogNextPrefix = g.LogNextSuffix = NULL; + g.LogDepthRef = window->DC.TreeDepth; + g.LogDepthToExpand = ((auto_open_depth >= 0) ? auto_open_depth : g.LogDepthToExpandDefault); + g.LogLinePosY = FLT_MAX; + g.LogLineFirstItem = true; +} + +// Important: doesn't copy underlying data, use carefully (prefix/suffix must be in scope at the time of the next LogRenderedText) +void ImGui::LogSetNextTextDecoration(const char* prefix, const char* suffix) +{ + ImGuiContext& g = *GImGui; + g.LogNextPrefix = prefix; + g.LogNextSuffix = suffix; +} + +void ImGui::LogToTTY(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + IM_UNUSED(auto_open_depth); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + LogBegin(ImGuiLogFlags_OutputTTY, auto_open_depth); + g.LogFile = stdout; +#endif +} + +// Start logging/capturing text output to given file +void ImGui::LogToFile(int auto_open_depth, const char* filename) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + + // FIXME: We could probably open the file in text mode "at", however note that clipboard/buffer logging will still + // be subject to outputting OS-incompatible carriage return if within strings the user doesn't use IM_NEWLINE. + // By opening the file in binary mode "ab" we have consistent output everywhere. + if (!filename) + filename = g.IO.LogFilename; + if (!filename || !filename[0]) + return; + ImFileHandle f = ImFileOpen(filename, "ab"); + if (!f) + { + IM_ASSERT(0); + return; + } + + LogBegin(ImGuiLogFlags_OutputFile, auto_open_depth); + g.LogFile = f; +} + +// Start logging/capturing text output to clipboard +void ImGui::LogToClipboard(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogFlags_OutputClipboard, auto_open_depth); +} + +void ImGui::LogToBuffer(int auto_open_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + LogBegin(ImGuiLogFlags_OutputBuffer, auto_open_depth); +} + +void ImGui::LogFinish() +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogText(IM_NEWLINE); + switch (g.LogFlags & ImGuiLogFlags_OutputMask_) + { + case ImGuiLogFlags_OutputTTY: +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + fflush(g.LogFile); +#endif + break; + case ImGuiLogFlags_OutputFile: + ImFileClose(g.LogFile); + break; + case ImGuiLogFlags_OutputBuffer: + break; + case ImGuiLogFlags_OutputClipboard: + if (!g.LogBuffer.empty()) + SetClipboardText(g.LogBuffer.begin()); + break; + default: + IM_ASSERT(0); + break; + } + + g.LogEnabled = g.ItemUnclipByLog = false; + g.LogFlags = ImGuiLogFlags_None; + g.LogFile = NULL; + g.LogBuffer.clear(); +} + +// Helper to display logging buttons +// FIXME-OBSOLETE: We should probably obsolete this and let the user have their own helper (this is one of the oldest function alive!) +void ImGui::LogButtons() +{ + ImGuiContext& g = *GImGui; + + PushID("LogButtons"); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + const bool log_to_tty = Button("Log To TTY"); SameLine(); +#else + const bool log_to_tty = false; +#endif + const bool log_to_file = Button("Log To File"); SameLine(); + const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); + PushItemFlag(ImGuiItemFlags_NoTabStop, true); + SetNextItemWidth(80.0f); + SliderInt("Default Depth", &g.LogDepthToExpandDefault, 0, 9, NULL); + PopItemFlag(); + PopID(); + + // Start logging at the end of the function so that the buttons don't appear in the log + if (log_to_tty) + LogToTTY(); + if (log_to_file) + LogToFile(); + if (log_to_clipboard) + LogToClipboard(); +} + +//----------------------------------------------------------------------------- +// [SECTION] SETTINGS +//----------------------------------------------------------------------------- +// - UpdateSettings() [Internal] +// - MarkIniSettingsDirty() [Internal] +// - FindSettingsHandler() [Internal] +// - ClearIniSettings() [Internal] +// - LoadIniSettingsFromDisk() +// - LoadIniSettingsFromMemory() +// - SaveIniSettingsToDisk() +// - SaveIniSettingsToMemory() +//----------------------------------------------------------------------------- +// - CreateNewWindowSettings() [Internal] +// - FindWindowSettingsByID() [Internal] +// - FindWindowSettingsByWindow() [Internal] +// - ClearWindowSettings() [Internal] +// - WindowSettingsHandler_***() [Internal] +//----------------------------------------------------------------------------- + +// Called by NewFrame() +void ImGui::UpdateSettings() +{ + // Load settings on first frame (if not explicitly loaded manually before) + ImGuiContext& g = *GImGui; + if (!g.SettingsLoaded) + { + IM_ASSERT(g.SettingsWindows.empty()); + if (g.IO.IniFilename) + LoadIniSettingsFromDisk(g.IO.IniFilename); + g.SettingsLoaded = true; + } + + // Save settings (with a delay after the last modification, so we don't spam disk too much) + if (g.SettingsDirtyTimer > 0.0f) + { + g.SettingsDirtyTimer -= g.IO.DeltaTime; + if (g.SettingsDirtyTimer <= 0.0f) + { + if (g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + else + g.IO.WantSaveIniSettings = true; // Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves. + g.SettingsDirtyTimer = 0.0f; + } + } +} + +void ImGui::MarkIniSettingsDirty() +{ + ImGuiContext& g = *GImGui; + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +void ImGui::MarkIniSettingsDirty(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (!(window->Flags & ImGuiWindowFlags_NoSavedSettings)) + if (g.SettingsDirtyTimer <= 0.0f) + g.SettingsDirtyTimer = g.IO.IniSavingRate; +} + +void ImGui::AddSettingsHandler(const ImGuiSettingsHandler* handler) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(FindSettingsHandler(handler->TypeName) == NULL); + g.SettingsHandlers.push_back(*handler); +} + +void ImGui::RemoveSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + if (ImGuiSettingsHandler* handler = FindSettingsHandler(type_name)) + g.SettingsHandlers.erase(handler); +} + +ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + const ImGuiID type_hash = ImHashStr(type_name); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.TypeHash == type_hash) + return &handler; + return NULL; +} + +// Clear all settings (windows, tables, docking etc.) +void ImGui::ClearIniSettings() +{ + ImGuiContext& g = *GImGui; + g.SettingsIniData.clear(); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ClearAllFn != NULL) + handler.ClearAllFn(&g, &handler); +} + +void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) +{ + size_t file_data_size = 0; + char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size); + if (!file_data) + return; + if (file_data_size > 0) + LoadIniSettingsFromMemory(file_data, (size_t)file_data_size); + IM_FREE(file_data); +} + +// Zero-tolerance, no error reporting, cheap .ini parsing +// Set ini_size==0 to let us use strlen(ini_data). Do not call this function with a 0 if your buffer is actually empty! +void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Initialized); + //IM_ASSERT(!g.WithinFrameScope && "Cannot be called between NewFrame() and EndFrame()"); + //IM_ASSERT(g.SettingsLoaded == false && g.FrameCount == 0); + + // For user convenience, we allow passing a non zero-terminated string (hence the ini_size parameter). + // For our convenience and to make the code simpler, we'll also write zero-terminators within the buffer. So let's create a writable copy.. + if (ini_size == 0) + ini_size = ImStrlen(ini_data); + g.SettingsIniData.Buf.resize((int)ini_size + 1); + char* const buf = g.SettingsIniData.Buf.Data; + char* const buf_end = buf + ini_size; + memcpy(buf, ini_data, ini_size); + buf_end[0] = 0; + + // Call pre-read handlers + // Some types will clear their data (e.g. dock information) some types will allow merge/override (window) + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ReadInitFn != NULL) + handler.ReadInitFn(&g, &handler); + + void* entry_data = NULL; + ImGuiSettingsHandler* entry_handler = NULL; + + char* line_end = NULL; + for (char* line = buf; line < buf_end; line = line_end + 1) + { + // Skip new lines markers, then find end of the line + while (*line == '\n' || *line == '\r') + line++; + line_end = line; + while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') + line_end++; + line_end[0] = 0; + if (line[0] == ';') + continue; + if (line[0] == '[' && line_end > line && line_end[-1] == ']') + { + // Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code. + line_end[-1] = 0; + const char* name_end = line_end - 1; + const char* type_start = line + 1; + char* type_end = (char*)(void*)ImStrchrRange(type_start, name_end, ']'); + const char* name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; + if (!type_end || !name_start) + continue; + *type_end = 0; // Overwrite first ']' + name_start++; // Skip second '[' + entry_handler = FindSettingsHandler(type_start); + entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; + } + else if (entry_handler != NULL && entry_data != NULL) + { + // Let type handler parse the line + entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); + } + } + g.SettingsLoaded = true; + + // [DEBUG] Restore untouched copy so it can be browsed in Metrics (not strictly necessary) + memcpy(buf, ini_data, ini_size); + + // Call post-read handlers + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ApplyAllFn != NULL) + handler.ApplyAllFn(&g, &handler); +} + +void ImGui::SaveIniSettingsToDisk(const char* ini_filename) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + if (!ini_filename) + return; + + size_t ini_data_size = 0; + const char* ini_data = SaveIniSettingsToMemory(&ini_data_size); + ImFileHandle f = ImFileOpen(ini_filename, "wt"); + if (!f) + return; + ImFileWrite(ini_data, sizeof(char), ini_data_size, f); + ImFileClose(f); +} + +// Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer +const char* ImGui::SaveIniSettingsToMemory(size_t* out_size) +{ + ImGuiContext& g = *GImGui; + g.SettingsDirtyTimer = 0.0f; + g.SettingsIniData.Buf.resize(0); + g.SettingsIniData.Buf.push_back(0); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + handler.WriteAllFn(&g, &handler, &g.SettingsIniData); + if (out_size) + *out_size = (size_t)g.SettingsIniData.size(); + return g.SettingsIniData.c_str(); +} + +ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) +{ + ImGuiContext& g = *GImGui; + + if (g.IO.ConfigDebugIniSettings == false) + { + // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier. + if (const char* p = strstr(name, "###")) + name = p; + } + const size_t name_len = ImStrlen(name); + + // Allocate chunk + const size_t chunk_size = sizeof(ImGuiWindowSettings) + name_len + 1; + ImGuiWindowSettings* settings = g.SettingsWindows.alloc_chunk(chunk_size); + IM_PLACEMENT_NEW(settings) ImGuiWindowSettings(); + settings->ID = ImHashStr(name, name_len); + memcpy(settings->GetName(), name, name_len + 1); // Store with zero terminator + + return settings; +} + +// We don't provide a FindWindowSettingsByName() because Docking system doesn't always hold on names. +// This is called once per window .ini entry + once per newly instantiated window. +ImGuiWindowSettings* ImGui::FindWindowSettingsByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->ID == id && !settings->WantDelete) + return settings; + return NULL; +} + +// This is faster if you are holding on a Window already as we don't need to perform a search. +ImGuiWindowSettings* ImGui::FindWindowSettingsByWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (window->SettingsOffset != -1) + return g.SettingsWindows.ptr_from_offset(window->SettingsOffset); + return FindWindowSettingsByID(window->ID); +} + +// This will revert window to its initial state, including enabling the ImGuiCond_FirstUseEver/ImGuiCond_Once conditions once more. +void ImGui::ClearWindowSettings(const char* name) +{ + //IMGUI_DEBUG_LOG("ClearWindowSettings('%s')\n", name); + ImGuiWindow* window = FindWindowByName(name); + if (window != NULL) + { + window->Flags |= ImGuiWindowFlags_NoSavedSettings; + InitOrLoadWindowSettings(window, NULL); + } + if (ImGuiWindowSettings* settings = window ? FindWindowSettingsByWindow(window) : FindWindowSettingsByID(ImHashStr(name))) + settings->WantDelete = true; +} + +static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + window->SettingsOffset = -1; + g.SettingsWindows.clear(); +} + +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = ImHashStr(name); + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByID(id); + if (settings) + *settings = ImGuiWindowSettings(); // Clear existing if recycling previous entry + else + settings = ImGui::CreateNewWindowSettings(name); + settings->ID = id; + settings->WantApply = true; + return (void*)settings; +} + +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; + int x, y; + int i; + if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } + else if (sscanf(line, "IsChild=%d", &i) == 1) { settings->IsChild = (i != 0); } +} + +// Apply to existing windows (if any) +static void WindowSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->WantApply) + { + if (ImGuiWindow* window = ImGui::FindWindowByID(settings->ID)) + ApplyWindowSettings(window, settings); + settings->WantApply = false; + } +} + +static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + // Gather data from windows that were active during this session + // (if a window wasn't opened in this session we preserve its settings) + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + { + if (window->Flags & ImGuiWindowFlags_NoSavedSettings) + continue; + + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByWindow(window); + if (!settings) + { + settings = ImGui::CreateNewWindowSettings(window->Name); + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + } + IM_ASSERT(settings->ID == window->ID); + settings->Pos = ImVec2ih(window->Pos); + settings->Size = ImVec2ih(window->SizeFull); + settings->IsChild = (window->Flags & ImGuiWindowFlags_ChildWindow) != 0; + settings->Collapsed = window->Collapsed; + settings->WantDelete = false; + } + + // Write to text buffer + buf->reserve(buf->size() + g.SettingsWindows.size() * 6); // ballpark reserve + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + if (settings->WantDelete) + continue; + const char* settings_name = settings->GetName(); + buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); + if (settings->IsChild) + { + buf->appendf("IsChild=1\n"); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + } + else + { + buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + if (settings->Collapsed) + buf->appendf("Collapsed=1\n"); + } + buf->append("\n"); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] LOCALIZATION +//----------------------------------------------------------------------------- + +void ImGui::LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count) +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < count; n++) + g.LocalizationTable[entries[n].Key] = entries[n].Text; +} + +//----------------------------------------------------------------------------- +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +//----------------------------------------------------------------------------- +// - GetMainViewport() +// - SetWindowViewport() [Internal] +// - ScaleWindowsInViewport() [Internal] +// - UpdateViewportsNewFrame() [Internal] +// (this section is more complete in the 'docking' branch) +//----------------------------------------------------------------------------- + +ImGuiViewport* ImGui::GetMainViewport() +{ + ImGuiContext& g = *GImGui; + return g.Viewports[0]; +} + +void ImGui::SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + window->Viewport = viewport; +} + +static void ScaleWindow(ImGuiWindow* window, float scale) +{ + ImVec2 origin = window->Viewport->Pos; + window->Pos = ImFloor((window->Pos - origin) * scale + origin); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + window->ContentSize = ImTrunc(window->ContentSize * scale); +} + +// Scale all windows (position, size). Use when e.g. changing DPI. (This is a lossy operation!) +void ImGui::ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindow* window : g.Windows) + if (window->Viewport == viewport) + ScaleWindow(window, scale); +} + +// Update viewports and monitor infos +static void ImGui::UpdateViewportsNewFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.Viewports.Size == 1); + + // Update main viewport with current platform position. + // FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent. + ImGuiViewportP* main_viewport = g.Viewports[0]; + main_viewport->Flags = ImGuiViewportFlags_IsPlatformWindow | ImGuiViewportFlags_OwnedByApp; + main_viewport->Pos = ImVec2(0.0f, 0.0f); + main_viewport->Size = g.IO.DisplaySize; + + for (ImGuiViewportP* viewport : g.Viewports) + { + // Lock down space taken by menu bars and status bars + // Setup initial value for functions like BeginMainMenuBar(), DockSpaceOverViewport() etc. + viewport->WorkInsetMin = viewport->BuildWorkInsetMin; + viewport->WorkInsetMax = viewport->BuildWorkInsetMax; + viewport->BuildWorkInsetMin = viewport->BuildWorkInsetMax = ImVec2(0.0f, 0.0f); + viewport->UpdateWorkRect(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DOCKING +//----------------------------------------------------------------------------- + +// (this section is filled in the 'docking' branch) + + +//----------------------------------------------------------------------------- +// [SECTION] PLATFORM DEPENDENT HELPERS +//----------------------------------------------------------------------------- +// - Default clipboard handlers +// - Default shell function handlers +// - Default IME handlers +//----------------------------------------------------------------------------- + +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) + +#ifdef _MSC_VER +#pragma comment(lib, "user32") +#pragma comment(lib, "kernel32") +#endif + +// Win32 clipboard implementation +// We use g.ClipboardHandlerData for temporary storage to ensure it is freed on Shutdown() +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); + if (!::OpenClipboard(NULL)) + return NULL; + HANDLE wbuf_handle = ::GetClipboardData(CF_UNICODETEXT); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return NULL; + } + if (const WCHAR* wbuf_global = (const WCHAR*)::GlobalLock(wbuf_handle)) + { + int buf_len = ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, NULL, 0, NULL, NULL); + g.ClipboardHandlerData.resize(buf_len); + ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, g.ClipboardHandlerData.Data, buf_len, NULL, NULL); + } + ::GlobalUnlock(wbuf_handle); + ::CloseClipboard(); + return g.ClipboardHandlerData.Data; +} + +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) +{ + if (!::OpenClipboard(NULL)) + return; + const int wbuf_length = ::MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); + HGLOBAL wbuf_handle = ::GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(WCHAR)); + if (wbuf_handle == NULL) + { + ::CloseClipboard(); + return; + } + WCHAR* wbuf_global = (WCHAR*)::GlobalLock(wbuf_handle); + ::MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf_global, wbuf_length); + ::GlobalUnlock(wbuf_handle); + ::EmptyClipboard(); + if (::SetClipboardData(CF_UNICODETEXT, wbuf_handle) == NULL) + ::GlobalFree(wbuf_handle); + ::CloseClipboard(); +} + +#elif defined(__APPLE__) && TARGET_OS_OSX && defined(IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS) + +#include // Use old API to avoid need for separate .mm file +static PasteboardRef main_clipboard = 0; + +// OSX clipboard implementation +// If you enable this you will need to add '-framework ApplicationServices' to your linker command-line! +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) +{ + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardClear(main_clipboard); + CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)text, ImStrlen(text)); + if (cf_data) + { + PasteboardPutItemFlavor(main_clipboard, (PasteboardItemID)1, CFSTR("public.utf8-plain-text"), cf_data, 0); + CFRelease(cf_data); + } +} + +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + if (!main_clipboard) + PasteboardCreate(kPasteboardClipboard, &main_clipboard); + PasteboardSynchronize(main_clipboard); + + ItemCount item_count = 0; + PasteboardGetItemCount(main_clipboard, &item_count); + for (ItemCount i = 0; i < item_count; i++) + { + PasteboardItemID item_id = 0; + PasteboardGetItemIdentifier(main_clipboard, i + 1, &item_id); + CFArrayRef flavor_type_array = 0; + PasteboardCopyItemFlavors(main_clipboard, item_id, &flavor_type_array); + for (CFIndex j = 0, nj = CFArrayGetCount(flavor_type_array); j < nj; j++) + { + CFDataRef cf_data; + if (PasteboardCopyItemFlavorData(main_clipboard, item_id, CFSTR("public.utf8-plain-text"), &cf_data) == noErr) + { + g.ClipboardHandlerData.clear(); + int length = (int)CFDataGetLength(cf_data); + g.ClipboardHandlerData.resize(length + 1); + CFDataGetBytes(cf_data, CFRangeMake(0, length), (UInt8*)g.ClipboardHandlerData.Data); + g.ClipboardHandlerData[length] = 0; + CFRelease(cf_data); + return g.ClipboardHandlerData.Data; + } + } + } + return NULL; +} + +#else + +// Local Dear ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers. +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + return g.ClipboardHandlerData.empty() ? NULL : g.ClipboardHandlerData.begin(); +} + +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text) +{ + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); + const char* text_end = text + ImStrlen(text); + g.ClipboardHandlerData.resize((int)(text_end - text) + 1); + memcpy(&g.ClipboardHandlerData[0], text, (size_t)(text_end - text)); + g.ClipboardHandlerData[(int)(text_end - text)] = 0; +} + +#endif // Default clipboard handlers + +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#if defined(__APPLE__) && TARGET_OS_IPHONE +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#if defined(__3DS__) +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#if defined(_WIN32) && defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#endif // #ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#ifdef _WIN32 +#include // ShellExecuteA() +#ifdef _MSC_VER +#pragma comment(lib, "shell32") +#endif +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ + const int path_wsize = ::MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); + ImVector path_wbuf; + path_wbuf.resize(path_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, path, -1, path_wbuf.Data, path_wsize); + return (INT_PTR)::ShellExecuteW(NULL, L"open", path_wbuf.Data, NULL, NULL, SW_SHOWDEFAULT) > 32; +} +#else +#include +#include +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ +#if defined(__APPLE__) + const char* args[] { "open", "--", path, NULL }; +#else + const char* args[] { "xdg-open", path, NULL }; +#endif + pid_t pid = fork(); + if (pid < 0) + return false; + if (!pid) + { + execvp(args[0], const_cast(args)); + exit(-1); + } + else + { + int status; + waitpid(pid, &status, 0); + return WEXITSTATUS(status) == 0; + } +} +#endif +#else +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char*) { return false; } +#endif // Default shell handlers + +//----------------------------------------------------------------------------- + +// Win32 API IME support (for Asian languages, etc.) +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) + +#include +#ifdef _MSC_VER +#pragma comment(lib, "imm32") +#endif + +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport* viewport, ImGuiPlatformImeData* data) +{ + // Notify OS Input Method Editor of text input position + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + if (hwnd == 0) + return; + + //::ImmAssociateContextEx(hwnd, NULL, data->WantVisible ? IACE_DEFAULT : 0); + if (HIMC himc = ::ImmGetContext(hwnd)) + { + COMPOSITIONFORM composition_form = {}; + composition_form.ptCurrentPos.x = (LONG)data->InputPos.x; + composition_form.ptCurrentPos.y = (LONG)data->InputPos.y; + composition_form.dwStyle = CFS_FORCE_POSITION; + ::ImmSetCompositionWindow(himc, &composition_form); + CANDIDATEFORM candidate_form = {}; + candidate_form.dwStyle = CFS_CANDIDATEPOS; + candidate_form.ptCurrentPos.x = (LONG)data->InputPos.x; + candidate_form.ptCurrentPos.y = (LONG)data->InputPos.y; + ::ImmSetCandidateWindow(himc, &candidate_form); + ::ImmReleaseContext(hwnd, himc); + } +} + +#else + +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport*, ImGuiPlatformImeData*) {} + +#endif // Default IME handlers + +//----------------------------------------------------------------------------- +// [SECTION] METRICS/DEBUGGER WINDOW +//----------------------------------------------------------------------------- +// - DebugRenderViewportThumbnail() [Internal] +// - RenderViewportsThumbnails() [Internal] +// - DebugTextEncoding() +// - MetricsHelpMarker() [Internal] +// - ShowFontAtlas() [Internal] +// - ShowMetricsWindow() +// - DebugNodeColumns() [Internal] +// - DebugNodeDrawList() [Internal] +// - DebugNodeDrawCmdShowMeshAndBoundingBox() [Internal] +// - DebugNodeFont() [Internal] +// - DebugNodeFontGlyph() [Internal] +// - DebugNodeStorage() [Internal] +// - DebugNodeTabBar() [Internal] +// - DebugNodeViewport() [Internal] +// - DebugNodeWindow() [Internal] +// - DebugNodeWindowSettings() [Internal] +// - DebugNodeWindowsList() [Internal] +// - DebugNodeWindowsListByBeginStackParent() [Internal] +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + +void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImVec2 scale = bb.GetSize() / viewport->Size; + ImVec2 off = bb.Min - viewport->Pos * scale; + float alpha_mul = 1.0f; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul * 0.40f)); + for (ImGuiWindow* thumb_window : g.Windows) + { + if (!thumb_window->WasActive || (thumb_window->Flags & ImGuiWindowFlags_ChildWindow)) + continue; + + ImRect thumb_r = thumb_window->Rect(); + ImRect title_r = thumb_window->TitleBarRect(); + thumb_r = ImRect(ImTrunc(off + thumb_r.Min * scale), ImTrunc(off + thumb_r.Max * scale)); + title_r = ImRect(ImTrunc(off + title_r.Min * scale), ImTrunc(off + ImVec2(title_r.Max.x, title_r.Min.y + title_r.GetHeight() * 3.0f) * scale)); // Exaggerate title bar height + thumb_r.ClipWithFull(bb); + title_r.ClipWithFull(bb); + const bool window_is_focused = (g.NavWindow && thumb_window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + window->DrawList->AddRectFilled(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_WindowBg, alpha_mul)); + window->DrawList->AddRectFilled(title_r.Min, title_r.Max, GetColorU32(window_is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg, alpha_mul)); + window->DrawList->AddRect(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + window->DrawList->AddText(g.Font, g.FontSize * 1.0f, title_r.Min, GetColorU32(ImGuiCol_Text, alpha_mul), thumb_window->Name, FindRenderedTextEnd(thumb_window->Name)); + } + draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + if (viewport->ID == g.DebugMetricsConfig.HighlightViewportID) + window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); +} + +static void RenderViewportsThumbnails() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + float SCALE = 1.0f / 8.0f; + ImRect bb_full(g.Viewports[0]->Pos, g.Viewports[0]->Pos + g.Viewports[0]->Size); + ImVec2 p = window->DC.CursorPos; + ImVec2 off = p - bb_full.Min * SCALE; + + // Draw viewports + for (ImGuiViewportP* viewport : g.Viewports) + { + ImRect viewport_draw_bb(off + (viewport->Pos) * SCALE, off + (viewport->Pos + viewport->Size) * SCALE); + ImGui::DebugRenderViewportThumbnail(window->DrawList, viewport, viewport_draw_bb); + } + ImGui::Dummy(bb_full.GetSize() * SCALE); +} + +// Draw an arbitrary US keyboard layout to visualize translated keys +void ImGui::DebugRenderKeyboardPreview(ImDrawList* draw_list) +{ + const float scale = ImGui::GetFontSize() / 13.0f; + const ImVec2 key_size = ImVec2(35.0f, 35.0f) * scale; + const float key_rounding = 3.0f * scale; + const ImVec2 key_face_size = ImVec2(25.0f, 25.0f) * scale; + const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f) * scale; + const float key_face_rounding = 2.0f * scale; + const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f) * scale; + const ImVec2 key_step = ImVec2(key_size.x - 1.0f, key_size.y - 1.0f); + const float key_row_offset = 9.0f * scale; + + ImVec2 board_min = GetCursorScreenPos(); + ImVec2 board_max = ImVec2(board_min.x + 3 * key_step.x + 2 * key_row_offset + 10.0f, board_min.y + 3 * key_step.y + 10.0f); + ImVec2 start_pos = ImVec2(board_min.x + 5.0f - key_step.x, board_min.y); + + struct KeyLayoutData { int Row, Col; const char* Label; ImGuiKey Key; }; + const KeyLayoutData keys_to_display[] = + { + { 0, 0, "", ImGuiKey_Tab }, { 0, 1, "Q", ImGuiKey_Q }, { 0, 2, "W", ImGuiKey_W }, { 0, 3, "E", ImGuiKey_E }, { 0, 4, "R", ImGuiKey_R }, + { 1, 0, "", ImGuiKey_CapsLock }, { 1, 1, "A", ImGuiKey_A }, { 1, 2, "S", ImGuiKey_S }, { 1, 3, "D", ImGuiKey_D }, { 1, 4, "F", ImGuiKey_F }, + { 2, 0, "", ImGuiKey_LeftShift },{ 2, 1, "Z", ImGuiKey_Z }, { 2, 2, "X", ImGuiKey_X }, { 2, 3, "C", ImGuiKey_C }, { 2, 4, "V", ImGuiKey_V } + }; + + // Elements rendered manually via ImDrawList API are not clipped automatically. + // While not strictly necessary, here IsItemVisible() is used to avoid rendering these shapes when they are out of view. + Dummy(board_max - board_min); + if (!IsItemVisible()) + return; + draw_list->PushClipRect(board_min, board_max, true); + for (int n = 0; n < IM_ARRAYSIZE(keys_to_display); n++) + { + const KeyLayoutData* key_data = &keys_to_display[n]; + ImVec2 key_min = ImVec2(start_pos.x + key_data->Col * key_step.x + key_data->Row * key_row_offset, start_pos.y + key_data->Row * key_step.y); + ImVec2 key_max = key_min + key_size; + draw_list->AddRectFilled(key_min, key_max, IM_COL32(204, 204, 204, 255), key_rounding); + draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding); + ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y); + ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y); + draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, ImDrawFlags_None, 2.0f); + draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding); + ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y); + draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label); + if (IsKeyDown(key_data->Key)) + draw_list->AddRectFilled(key_min, key_max, IM_COL32(255, 0, 0, 128), key_rounding); + } + draw_list->PopClipRect(); +} + +// Helper tool to diagnose between text encoding issues and font loading issues. Pass your UTF-8 string and verify that there are correct. +void ImGui::DebugTextEncoding(const char* str) +{ + Text("Text: \"%s\"", str); + if (!BeginTable("##DebugTextEncoding", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable)) + return; + TableSetupColumn("Offset"); + TableSetupColumn("UTF-8"); + TableSetupColumn("Glyph"); + TableSetupColumn("Codepoint"); + TableHeadersRow(); + for (const char* p = str; *p != 0; ) + { + unsigned int c; + const int c_utf8_len = ImTextCharFromUtf8(&c, p, NULL); + TableNextColumn(); + Text("%d", (int)(p - str)); + TableNextColumn(); + for (int byte_index = 0; byte_index < c_utf8_len; byte_index++) + { + if (byte_index > 0) + SameLine(); + Text("0x%02X", (int)(unsigned char)p[byte_index]); + } + TableNextColumn(); + if (GetFont()->FindGlyphNoFallback((ImWchar)c)) + TextUnformatted(p, p + c_utf8_len); + else + TextUnformatted((c == IM_UNICODE_CODEPOINT_INVALID) ? "[invalid]" : "[missing]"); + TableNextColumn(); + Text("U+%04X", (int)c); + p += c_utf8_len; + } + EndTable(); +} + +static void DebugFlashStyleColorStop() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorIdx != ImGuiCol_COUNT) + g.Style.Colors[g.DebugFlashStyleColorIdx] = g.DebugFlashStyleColorBackup; + g.DebugFlashStyleColorIdx = ImGuiCol_COUNT; +} + +// Flash a given style color for some + inhibit modifications of this color via PushStyleColor() calls. +void ImGui::DebugFlashStyleColor(ImGuiCol idx) +{ + ImGuiContext& g = *GImGui; + DebugFlashStyleColorStop(); + g.DebugFlashStyleColorTime = 0.5f; + g.DebugFlashStyleColorIdx = idx; + g.DebugFlashStyleColorBackup = g.Style.Colors[idx]; +} + +void ImGui::UpdateDebugToolFlashStyleColor() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorTime <= 0.0f) + return; + ColorConvertHSVtoRGB(ImCos(g.DebugFlashStyleColorTime * 6.0f) * 0.5f + 0.5f, 0.5f, 0.5f, g.Style.Colors[g.DebugFlashStyleColorIdx].x, g.Style.Colors[g.DebugFlashStyleColorIdx].y, g.Style.Colors[g.DebugFlashStyleColorIdx].z); + g.Style.Colors[g.DebugFlashStyleColorIdx].w = 1.0f; + if ((g.DebugFlashStyleColorTime -= g.IO.DeltaTime) <= 0.0f) + DebugFlashStyleColorStop(); +} + +static const char* FormatTextureIDForDebugDisplay(char* buf, int buf_size, ImTextureID tex_id) +{ + union { void* ptr; int integer; } tex_id_opaque; + memcpy(&tex_id_opaque, &tex_id, ImMin(sizeof(void*), sizeof(tex_id))); + if (sizeof(tex_id) >= sizeof(void*)) + ImFormatString(buf, buf_size, "0x%p", tex_id_opaque.ptr); + else + ImFormatString(buf, buf_size, "0x%04X", tex_id_opaque.integer); + return buf; +} + +// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds. +static void MetricsHelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +// [DEBUG] List fonts in a font atlas and display its texture +void ImGui::ShowFontAtlas(ImFontAtlas* atlas) +{ + for (ImFont* font : atlas->Fonts) + { + PushID(font); + DebugNodeFont(font); + PopID(); + } + if (TreeNode("Font Atlas", "Font Atlas (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight)) + { + ImGuiContext& g = *GImGui; + PushStyleVar(ImGuiStyleVar_ImageBorderSize, ImMax(1.0f, g.Style.ImageBorderSize)); + ImageWithBg(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + PopStyleVar(); + TreePop(); + } +} + +void ImGui::ShowMetricsWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + if (cfg->ShowDebugLog) + ShowDebugLogWindow(&cfg->ShowDebugLog); + if (cfg->ShowIDStackTool) + ShowIDStackToolWindow(&cfg->ShowIDStackTool); + + if (!Begin("Dear ImGui Metrics/Debugger", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // [DEBUG] Clear debug breaks hooks after exactly one cycle. + DebugBreakClearData(); + + // Basic info + Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); + if (g.ContextName[0] != 0) + { + SameLine(); + Text("(Context Name: \"%s\")", g.ContextName); + } + Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); + Text("%d visible windows, %d current allocations", io.MetricsRenderWindows, g.DebugAllocInfo.TotalAllocCount - g.DebugAllocInfo.TotalFreeCount); + //SameLine(); if (SmallButton("GC")) { g.GcCompactAll = true; } + + Separator(); + + // Debugging enums + enum { WRT_OuterRect, WRT_OuterRectClipped, WRT_InnerRect, WRT_InnerClipRect, WRT_WorkRect, WRT_Content, WRT_ContentIdeal, WRT_ContentRegionRect, WRT_Count }; // Windows Rect Type + const char* wrt_rects_names[WRT_Count] = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Content", "ContentIdeal", "ContentRegionRect" }; + enum { TRT_OuterRect, TRT_InnerRect, TRT_WorkRect, TRT_HostClipRect, TRT_InnerClipRect, TRT_BackgroundClipRect, TRT_ColumnsRect, TRT_ColumnsWorkRect, TRT_ColumnsClipRect, TRT_ColumnsContentHeadersUsed, TRT_ColumnsContentHeadersIdeal, TRT_ColumnsContentFrozen, TRT_ColumnsContentUnfrozen, TRT_Count }; // Tables Rect Type + const char* trt_rects_names[TRT_Count] = { "OuterRect", "InnerRect", "WorkRect", "HostClipRect", "InnerClipRect", "BackgroundClipRect", "ColumnsRect", "ColumnsWorkRect", "ColumnsClipRect", "ColumnsContentHeadersUsed", "ColumnsContentHeadersIdeal", "ColumnsContentFrozen", "ColumnsContentUnfrozen" }; + if (cfg->ShowWindowsRectsType < 0) + cfg->ShowWindowsRectsType = WRT_WorkRect; + if (cfg->ShowTablesRectsType < 0) + cfg->ShowTablesRectsType = TRT_WorkRect; + + struct Funcs + { + static ImRect GetTableRect(ImGuiTable* table, int rect_type, int n) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); // Always using last submitted instance + if (rect_type == TRT_OuterRect) { return table->OuterRect; } + else if (rect_type == TRT_InnerRect) { return table->InnerRect; } + else if (rect_type == TRT_WorkRect) { return table->WorkRect; } + else if (rect_type == TRT_HostClipRect) { return table->HostClipRect; } + else if (rect_type == TRT_InnerClipRect) { return table->InnerClipRect; } + else if (rect_type == TRT_BackgroundClipRect) { return table->BgClipRect; } + else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table_instance->LastOuterHeight); } + else if (rect_type == TRT_ColumnsWorkRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->WorkRect.Min.y, c->WorkMaxX, table->WorkRect.Max.y); } + else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } + else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } + else if (rect_type == TRT_ColumnsContentFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXFrozen, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight); } + else if (rect_type == TRT_ColumnsContentUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight, c->ContentMaxXUnfrozen, table->InnerClipRect.Max.y); } + IM_ASSERT(0); + return ImRect(); + } + + static ImRect GetWindowRect(ImGuiWindow* window, int rect_type) + { + if (rect_type == WRT_OuterRect) { return window->Rect(); } + else if (rect_type == WRT_OuterRectClipped) { return window->OuterRectClipped; } + else if (rect_type == WRT_InnerRect) { return window->InnerRect; } + else if (rect_type == WRT_InnerClipRect) { return window->InnerClipRect; } + else if (rect_type == WRT_WorkRect) { return window->WorkRect; } + else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } + else if (rect_type == WRT_ContentIdeal) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSizeIdeal); } + else if (rect_type == WRT_ContentRegionRect) { return window->ContentRegionRect; } + IM_ASSERT(0); + return ImRect(); + } + }; + + // Tools + if (TreeNode("Tools")) + { + // Debug Break features + // The Item Picker tool is super useful to visually select an item and break into the call-stack of where it was submitted. + SeparatorTextEx(0, "Debug breaks", NULL, CalcTextSize("(?)").x + g.Style.SeparatorTextPadding.x); + SameLine(); + MetricsHelpMarker("Will call the IM_DEBUG_BREAK() macro to break in debugger.\nWarning: If you don't have a debugger attached, this will probably crash."); + if (Checkbox("Show Item Picker", &g.DebugItemPickerActive) && g.DebugItemPickerActive) + DebugStartItemPicker(); + Checkbox("Show \"Debug Break\" buttons in other sections (io.ConfigDebugIsDebuggerPresent)", &g.IO.ConfigDebugIsDebuggerPresent); + + SeparatorText("Visualize"); + + Checkbox("Show Debug Log", &cfg->ShowDebugLog); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowDebugLogWindow() from your code."); + + Checkbox("Show ID Stack Tool", &cfg->ShowIDStackTool); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowIDStackToolWindow() from your code."); + + Checkbox("Show windows begin order", &cfg->ShowWindowsBeginOrder); + Checkbox("Show windows rectangles", &cfg->ShowWindowsRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowWindowsRects |= Combo("##show_windows_rect_type", &cfg->ShowWindowsRectsType, wrt_rects_names, WRT_Count, WRT_Count); + if (cfg->ShowWindowsRects && g.NavWindow != NULL) + { + BulletText("'%s':", g.NavWindow->Name); + Indent(); + for (int rect_n = 0; rect_n < WRT_Count; rect_n++) + { + ImRect r = Funcs::GetWindowRect(g.NavWindow, rect_n); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), wrt_rects_names[rect_n]); + } + Unindent(); + } + + Checkbox("Show tables rectangles", &cfg->ShowTablesRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowTablesRects |= Combo("##show_table_rects_type", &cfg->ShowTablesRectsType, trt_rects_names, TRT_Count, TRT_Count); + if (cfg->ShowTablesRects && g.NavWindow != NULL) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1 || (table->OuterWindow != g.NavWindow && table->InnerWindow != g.NavWindow)) + continue; + + BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + Indent(); + char buf[128]; + for (int rect_n = 0; rect_n < TRT_Count; rect_n++) + { + if (rect_n >= TRT_ColumnsRect) + { + if (rect_n != TRT_ColumnsRect && rect_n != TRT_ColumnsClipRect) + continue; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, rect_n, column_n); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, rect_n, -1); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + Unindent(); + } + } + Checkbox("Show groups rectangles", &g.DebugShowGroupRects); // Storing in context as this is used by group code and prefers to be in hot-data + + SeparatorText("Validate"); + + Checkbox("Debug Begin/BeginChild return value", &io.ConfigDebugBeginReturnValueLoop); + SameLine(); + MetricsHelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); + + Checkbox("UTF-8 Encoding viewer", &cfg->ShowTextEncodingViewer); + SameLine(); + MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct."); + if (cfg->ShowTextEncodingViewer) + { + static char buf[64] = ""; + SetNextItemWidth(-FLT_MIN); + InputText("##DebugTextEncodingBuf", buf, IM_ARRAYSIZE(buf)); + if (buf[0] != 0) + DebugTextEncoding(buf); + } + + TreePop(); + } + + // Windows + if (TreeNode("Windows", "Windows (%d)", g.Windows.Size)) + { + //SetNextItemOpen(true, ImGuiCond_Once); + DebugNodeWindowsList(&g.Windows, "By display order"); + DebugNodeWindowsList(&g.WindowsFocusOrder, "By focus order (root windows)"); + if (TreeNode("By submission order (begin stack)")) + { + // Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship! + ImVector& temp_buffer = g.WindowsTempSortBuffer; + temp_buffer.resize(0); + for (ImGuiWindow* window : g.Windows) + if (window->LastFrameActive + 1 >= g.FrameCount) + temp_buffer.push_back(window); + struct Func { static int IMGUI_CDECL WindowComparerByBeginOrder(const void* lhs, const void* rhs) { return ((int)(*(const ImGuiWindow* const *)lhs)->BeginOrderWithinContext - (*(const ImGuiWindow* const*)rhs)->BeginOrderWithinContext); } }; + ImQsort(temp_buffer.Data, (size_t)temp_buffer.Size, sizeof(ImGuiWindow*), Func::WindowComparerByBeginOrder); + DebugNodeWindowsListByBeginStackParent(temp_buffer.Data, temp_buffer.Size, NULL); + TreePop(); + } + + TreePop(); + } + + // DrawLists + int drawlist_count = 0; + for (ImGuiViewportP* viewport : g.Viewports) + drawlist_count += viewport->DrawDataP.CmdLists.Size; + if (TreeNode("DrawLists", "DrawLists (%d)", drawlist_count)) + { + Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); + Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); + for (ImGuiViewportP* viewport : g.Viewports) + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + TreePop(); + } + + // Viewports + if (TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size)) + { + SetNextItemOpen(true, ImGuiCond_Once); + if (TreeNode("Windows Minimap")) + { + RenderViewportsThumbnails(); + TreePop(); + } + cfg->HighlightViewportID = 0; + + for (ImGuiViewportP* viewport : g.Viewports) + DebugNodeViewport(viewport); + TreePop(); + } + + // Details for Popups + if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) + { + for (const ImGuiPopupData& popup_data : g.OpenPopupStack) + { + // As it's difficult to interact with tree nodes while popups are open, we display everything inline. + ImGuiWindow* window = popup_data.Window; + BulletText("PopupID: %08x, Window: '%s' (%s%s), RestoreNavWindow '%s', ParentWindow '%s'", + popup_data.PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", + popup_data.RestoreNavWindow ? popup_data.RestoreNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); + } + TreePop(); + } + + // Details for TabBars + if (TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.GetAliveCount())) + { + for (int n = 0; n < g.TabBars.GetMapSize(); n++) + if (ImGuiTabBar* tab_bar = g.TabBars.TryGetMapData(n)) + { + PushID(tab_bar); + DebugNodeTabBar(tab_bar, "TabBar"); + PopID(); + } + TreePop(); + } + + // Details for Tables + if (TreeNode("Tables", "Tables (%d)", g.Tables.GetAliveCount())) + { + for (int n = 0; n < g.Tables.GetMapSize(); n++) + if (ImGuiTable* table = g.Tables.TryGetMapData(n)) + DebugNodeTable(table); + TreePop(); + } + + // Details for Fonts + ImFontAtlas* atlas = g.IO.Fonts; + if (TreeNode("Fonts", "Fonts (%d)", atlas->Fonts.Size)) + { + ShowFontAtlas(atlas); + TreePop(); + } + + // Details for InputText + if (TreeNode("InputText")) + { + DebugNodeInputTextState(&g.InputTextState); + TreePop(); + } + + // Details for TypingSelect + if (TreeNode("TypingSelect", "TypingSelect (%d)", g.TypingSelectState.SearchBuffer[0] != 0 ? 1 : 0)) + { + DebugNodeTypingSelectState(&g.TypingSelectState); + TreePop(); + } + + // Details for MultiSelect + if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) + { + ImGuiBoxSelectState* bs = &g.BoxSelectState; + BulletText("BoxSelect ID=0x%08X, Starting = %d, Active %d", bs->ID, bs->IsStarting, bs->IsActive); + for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) + if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) + DebugNodeMultiSelectState(state); + TreePop(); + } + + // Details for Docking +#ifdef IMGUI_HAS_DOCK + if (TreeNode("Docking")) + { + TreePop(); + } +#endif // #ifdef IMGUI_HAS_DOCK + + // Settings + if (TreeNode("Settings")) + { + if (SmallButton("Clear")) + ClearIniSettings(); + SameLine(); + if (SmallButton("Save to memory")) + SaveIniSettingsToMemory(); + SameLine(); + if (SmallButton("Save to disk")) + SaveIniSettingsToDisk(g.IO.IniFilename); + SameLine(); + if (g.IO.IniFilename) + Text("\"%s\"", g.IO.IniFilename); + else + TextUnformatted(""); + Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); + if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) + { + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + BulletText("\"%s\"", handler.TypeName); + TreePop(); + } + if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + DebugNodeWindowSettings(settings); + TreePop(); + } + + if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) + { + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + DebugNodeTableSettings(settings); + TreePop(); + } + +#ifdef IMGUI_HAS_DOCK +#endif // #ifdef IMGUI_HAS_DOCK + + if (TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", g.SettingsIniData.size())) + { + InputTextMultiline("##Ini", (char*)(void*)g.SettingsIniData.c_str(), g.SettingsIniData.Buf.Size, ImVec2(-FLT_MIN, GetTextLineHeight() * 20), ImGuiInputTextFlags_ReadOnly); + TreePop(); + } + TreePop(); + } + + // Settings + if (TreeNode("Memory allocations")) + { + ImGuiDebugAllocInfo* info = &g.DebugAllocInfo; + Text("%d current allocations", info->TotalAllocCount - info->TotalFreeCount); + if (SmallButton("GC now")) { g.GcCompactAll = true; } + Text("Recent frames with allocations:"); + int buf_size = IM_ARRAYSIZE(info->LastEntriesBuf); + for (int n = buf_size - 1; n >= 0; n--) + { + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[(info->LastEntriesIdx - n + buf_size) % buf_size]; + BulletText("Frame %06d: %+3d ( %2d alloc, %2d free )", entry->FrameCount, entry->AllocCount - entry->FreeCount, entry->AllocCount, entry->FreeCount); + if (n == 0) + { + SameLine(); + Text("<- %d frames ago", g.FrameCount - entry->FrameCount); + } + } + TreePop(); + } + + if (TreeNode("Inputs")) + { + Text("KEYBOARD/GAMEPAD/MOUSE KEYS"); + { + // User code should never have to go through such hoops! You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. + Indent(); + Text("Keys down:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyDown(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); SameLine(); Text("(%.02f)", GetKeyData(key)->DownDuration); } + Text("Keys pressed:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyPressed(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys released:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyReleased(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys mods: %s%s%s%s", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""); + Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; SameLine(); Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. + DebugRenderKeyboardPreview(GetWindowDrawList()); + Unindent(); + } + + Text("MOUSE STATE"); + { + Indent(); + if (IsMousePosValid()) + Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y); + else + Text("Mouse pos: "); + Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y); + int count = IM_ARRAYSIZE(io.MouseDown); + Text("Mouse down:"); for (int i = 0; i < count; i++) if (IsMouseDown(i)) { SameLine(); Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } + Text("Mouse clicked:"); for (int i = 0; i < count; i++) if (IsMouseClicked(i)) { SameLine(); Text("b%d (%d)", i, io.MouseClickedCount[i]); } + Text("Mouse released:"); for (int i = 0; i < count; i++) if (IsMouseReleased(i)) { SameLine(); Text("b%d", i); } + Text("Mouse wheel: %.1f", io.MouseWheel); + Text("MouseStationaryTimer: %.2f", g.MouseStationaryTimer); + Text("Mouse source: %s", GetMouseSourceName(io.MouseSource)); + Text("Pen Pressure: %.1f", io.PenPressure); // Note: currently unused + Unindent(); + } + + Text("MOUSE WHEELING"); + { + Indent(); + Text("WheelingWindow: '%s'", g.WheelingWindow ? g.WheelingWindow->Name : "NULL"); + Text("WheelingWindowReleaseTimer: %.2f", g.WheelingWindowReleaseTimer); + Text("WheelingAxisAvg[] = { %.3f, %.3f }, Main Axis: %s", g.WheelingAxisAvg.x, g.WheelingAxisAvg.y, (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? "X" : (g.WheelingAxisAvg.x < g.WheelingAxisAvg.y) ? "Y" : ""); + Unindent(); + } + + Text("KEY OWNERS"); + { + Indent(); + if (BeginChild("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + continue; + Text("%s: 0x%08X%s", GetKeyName(key), owner_data->OwnerCurr, + owner_data->LockUntilRelease ? " LockUntilRelease" : owner_data->LockThisFrame ? " LockThisFrame" : ""); + DebugLocateItemOnHover(owner_data->OwnerCurr); + } + EndChild(); + Unindent(); + } + Text("SHORTCUT ROUTING"); + SameLine(); + MetricsHelpMarker("Declared shortcut routes automatically set key owner when mods matches."); + { + Indent(); + if (BeginChild("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; ) + { + ImGuiKeyRoutingData* routing_data = &rt->Entries[idx]; + ImGuiKeyChord key_chord = key | routing_data->Mods; + Text("%s: 0x%08X (scored %d)", GetKeyChordName(key_chord), routing_data->RoutingCurr, routing_data->RoutingCurrScore); + DebugLocateItemOnHover(routing_data->RoutingCurr); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + SameLine(); + if (DebugBreakButton("**DebugBreak**", "in SetShortcutRouting() for this KeyChord")) + g.DebugBreakInShortcutRouting = key_chord; + } + idx = routing_data->NextEntryIndex; + } + } + EndChild(); + Text("(ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: 0x%X)", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Unindent(); + } + TreePop(); + } + + if (TreeNode("Internal state")) + { + Text("WINDOWING"); + Indent(); + Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); + Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindow->Name : "NULL"); + Text("HoveredWindowUnderMovingWindow: '%s'", g.HoveredWindowUnderMovingWindow ? g.HoveredWindowUnderMovingWindow->Name : "NULL"); + Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); + Unindent(); + + Text("ITEMS"); + Indent(); + Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d, Source: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, g.ActiveIdAllowOverlap, GetInputSourceName(g.ActiveIdSource)); + DebugLocateItemOnHover(g.ActiveId); + Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); + Text("ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: %X", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Not displaying g.HoveredId as it is update mid-frame + Text("HoverItemDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverItemDelayId, g.HoverItemDelayTimer, g.HoverItemDelayClearTimer); + Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); + DebugLocateItemOnHover(g.DragDropPayload.SourceId); + Unindent(); + + Text("NAV,FOCUS"); + Indent(); + Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); + Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); + DebugLocateItemOnHover(g.NavId); + Text("NavInputSource: %s", GetInputSourceName(g.NavInputSource)); + Text("NavLastValidSelectionUserData = %" IM_PRId64 " (0x%" IM_PRIX64 ")", g.NavLastValidSelectionUserData, g.NavLastValidSelectionUserData); + Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); + Text("NavActivateId/DownId/PressedId: %08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId, g.NavActivatePressedId); + Text("NavActivateFlags: %04X", g.NavActivateFlags); + Text("NavCursorVisible: %d, NavHighlightItemUnderNav: %d", g.NavCursorVisible, g.NavHighlightItemUnderNav); + Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId); + Text("NavFocusRoute[] = "); + for (int path_n = g.NavFocusRoute.Size - 1; path_n >= 0; path_n--) + { + const ImGuiFocusScopeData& focus_scope = g.NavFocusRoute[path_n]; + SameLine(0.0f, 0.0f); + Text("0x%08X/", focus_scope.ID); + SetItemTooltip("In window \"%s\"", FindWindowByID(focus_scope.WindowID)->Name); + } + Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); + Unindent(); + + TreePop(); + } + + // Overlay: Display windows Rectangles and Begin Order + if (cfg->ShowWindowsRects || cfg->ShowWindowsBeginOrder) + { + for (ImGuiWindow* window : g.Windows) + { + if (!window->WasActive) + continue; + ImDrawList* draw_list = GetForegroundDrawList(window); + if (cfg->ShowWindowsRects) + { + ImRect r = Funcs::GetWindowRect(window, cfg->ShowWindowsRectsType); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + if (cfg->ShowWindowsBeginOrder && !(window->Flags & ImGuiWindowFlags_ChildWindow)) + { + char buf[32]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "%d", window->BeginOrderWithinContext); + float font_size = GetFontSize(); + draw_list->AddRectFilled(window->Pos, window->Pos + ImVec2(font_size, font_size), IM_COL32(200, 100, 100, 255)); + draw_list->AddText(window->Pos, IM_COL32(255, 255, 255, 255), buf); + } + } + } + + // Overlay: Display Tables Rectangles + if (cfg->ShowTablesRects) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1) + continue; + ImDrawList* draw_list = GetForegroundDrawList(table->OuterWindow); + if (cfg->ShowTablesRectsType >= TRT_ColumnsRect) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); + ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); + float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; + draw_list->AddRect(r.Min, r.Max, col, 0.0f, 0, thickness); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, -1); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + } + } + +#ifdef IMGUI_HAS_DOCK + // Overlay: Display Docking info + if (show_docking_nodes && g.IO.KeyCtrl) + { + } +#endif // #ifdef IMGUI_HAS_DOCK + + End(); +} + +void ImGui::DebugBreakClearData() +{ + // Those fields are scattered in their respective subsystem to stay in hot-data locations + ImGuiContext& g = *GImGui; + g.DebugBreakInWindow = 0; + g.DebugBreakInTable = 0; + g.DebugBreakInShortcutRouting = ImGuiKey_None; +} + +void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location) +{ + if (!BeginItemTooltip()) + return; + Text("To call IM_DEBUG_BREAK() %s:", description_of_location); + Separator(); + TextUnformatted(keyboard_only ? "- Press 'Pause/Break' on keyboard." : "- Press 'Pause/Break' on keyboard.\n- or Click (may alter focus/active id).\n- or navigate using keyboard and press space."); + Separator(); + TextUnformatted("Choose one way that doesn't interfere with what you are trying to debug!\nYou need a debugger attached or this will crash!"); + EndTooltip(); +} + +// Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. +// In order to reduce interferences with the contents we are trying to debug into. +bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 pos = window->DC.CursorPos + ImVec2(0.0f, window->DC.CurrLineTextBaseOffset); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x * 2.0f, label_size.y); + + const ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + // WE DO NOT USE ButtonEx() or ButtonBehavior() in order to reduce our side-effects. + bool hovered = ItemHoverable(bb, id, g.CurrentItemFlags); + bool pressed = hovered && (IsKeyChordPressed(g.DebugBreakKeyChord) || IsMouseClicked(0) || g.NavActivateId == id); + DebugBreakButtonTooltip(false, description_of_location); + + ImVec4 col4f = GetStyleColorVec4(hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImVec4 hsv; + ColorConvertRGBtoHSV(col4f.x, col4f.y, col4f.z, hsv.x, hsv.y, hsv.z); + ColorConvertHSVtoRGB(hsv.x + 0.20f, hsv.y, hsv.z, col4f.x, col4f.y, col4f.z); + + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, GetColorU32(col4f), true, g.Style.FrameRounding); + RenderTextClipped(bb.Min, bb.Max, label, NULL, &label_size, g.Style.ButtonTextAlign, &bb); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +// [DEBUG] Display contents of Columns +void ImGui::DebugNodeColumns(ImGuiOldColumns* columns) +{ + if (!TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) + return; + BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); + for (ImGuiOldColumnData& column : columns->Columns) + BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", (int)columns->Columns.index_from_ptr(&column), column.OffsetNorm, GetColumnOffsetFromNorm(columns, column.OffsetNorm)); + TreePop(); +} + +// [DEBUG] Display contents of ImDrawList +void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(viewport); // Used in docking branch + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + int cmd_count = draw_list->CmdBuffer.Size; + if (cmd_count > 0 && draw_list->CmdBuffer.back().ElemCount == 0 && draw_list->CmdBuffer.back().UserCallback == NULL) + cmd_count--; + bool node_open = TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, cmd_count); + if (draw_list == GetWindowDrawList()) + { + SameLine(); + TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) + if (node_open) + TreePop(); + return; + } + + ImDrawList* fg_draw_list = GetForegroundDrawList(window); // Render additional visuals into the top-most draw list + if (window && IsItemHovered() && fg_draw_list) + fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!node_open) + return; + + if (window && !window->WasActive) + TextDisabled("Warning: owning Window is inactive. This DrawList is not being rendered!"); + + for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.Data; pcmd < draw_list->CmdBuffer.Data + cmd_count; pcmd++) + { + if (pcmd->UserCallback) + { + BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); + continue; + } + + char texid_desc[20]; + FormatTextureIDForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TextureId); + char buf[300]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", + pcmd->ElemCount / 3, texid_desc, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + bool pcmd_node_open = TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); + if (IsItemHovered() && (cfg->ShowDrawCmdMesh || cfg->ShowDrawCmdBoundingBoxes) && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, cfg->ShowDrawCmdMesh, cfg->ShowDrawCmdBoundingBoxes); + if (!pcmd_node_open) + continue; + + // Calculate approximate coverage area (touched pixel count) + // This will be in pixels squared as long there's no post-scaling happening to the renderer output. + const ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; + const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + pcmd->VtxOffset; + float total_area = 0.0f; + for (unsigned int idx_n = pcmd->IdxOffset; idx_n < pcmd->IdxOffset + pcmd->ElemCount; ) + { + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos; + total_area += ImTriangleArea(triangle[0], triangle[1], triangle[2]); + } + + // Display vertex information summary. Hover to get all triangles drawn in wire-frame + ImFormatString(buf, IM_ARRAYSIZE(buf), "Mesh: ElemCount: %d, VtxOffset: +%d, IdxOffset: +%d, Area: ~%0.f px", pcmd->ElemCount, pcmd->VtxOffset, pcmd->IdxOffset, total_area); + Selectable(buf); + if (IsItemHovered() && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, true, false); + + // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. + ImGuiListClipper clipper; + clipper.Begin(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + while (clipper.Step()) + for (int prim = clipper.DisplayStart, idx_i = pcmd->IdxOffset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) + { + char* buf_p = buf, * buf_end = buf + IM_ARRAYSIZE(buf); + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_i++) + { + const ImDrawVert& v = vtx_buffer[idx_buffer ? idx_buffer[idx_i] : idx_i]; + triangle[n] = v.pos; + buf_p += ImFormatString(buf_p, buf_end - buf_p, "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", + (n == 0) ? "Vert:" : " ", idx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); + } + + Selectable(buf, false); + if (fg_draw_list && IsItemHovered()) + { + ImDrawListFlags backup_flags = fg_draw_list->Flags; + fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); + fg_draw_list->Flags = backup_flags; + } + } + TreePop(); + } + TreePop(); +} + +// [DEBUG] Display mesh/aabb of a ImDrawCmd +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb) +{ + IM_ASSERT(show_mesh || show_aabb); + + // Draw wire-frame version of all triangles + ImRect clip_rect = draw_cmd->ClipRect; + ImRect vtxs_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + ImDrawListFlags backup_flags = out_draw_list->Flags; + out_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + for (unsigned int idx_n = draw_cmd->IdxOffset, idx_end = draw_cmd->IdxOffset + draw_cmd->ElemCount; idx_n < idx_end; ) + { + ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; // We don't hold on those pointers past iterations as ->AddPolyline() may invalidate them if out_draw_list==draw_list + ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + draw_cmd->VtxOffset; + + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos)); + if (show_mesh) + out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); // In yellow: mesh triangles + } + // Draw bounding boxes + if (show_aabb) + { + out_draw_list->AddRect(ImTrunc(clip_rect.Min), ImTrunc(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU + out_draw_list->AddRect(ImTrunc(vtxs_rect.Min), ImTrunc(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles + } + out_draw_list->Flags = backup_flags; +} + +// [DEBUG] Display details for a single font, called by ShowStyleEditor(). +void ImGui::DebugNodeFont(ImFont* font) +{ + bool opened = TreeNode(font, "Font: \"%s\": %.2f px, %d glyphs, %d sources(s)", + font->Sources ? font->Sources[0].Name : "", font->FontSize, font->Glyphs.Size, font->SourcesCount); + + // Display preview text + if (!opened) + Indent(); + Indent(); + PushFont(font); + Text("The quick brown fox jumps over the lazy dog"); + PopFont(); + if (!opened) + { + Unindent(); + Unindent(); + return; + } + if (SmallButton("Set as default")) + GetIO().FontDefault = font; + + // Display details + SetNextItemWidth(GetFontSize() * 8); + DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); + SameLine(); MetricsHelpMarker( + "Note that the default embedded font is NOT meant to be scaled.\n\n" + "Font are currently rendered into bitmaps at a given size at the time of building the atlas. " + "You may oversample them to get some flexibility with scaling. " + "You can also render at multiple sizes and select which one to use at runtime.\n\n" + "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)"); + Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); + char c_str[5]; + Text("Fallback character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->FallbackChar), font->FallbackChar); + Text("Ellipsis character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->EllipsisChar), font->EllipsisChar); + const int surface_sqrt = (int)ImSqrt((float)font->MetricsTotalSurface); + Text("Texture Area: about %d px ~%dx%d px", font->MetricsTotalSurface, surface_sqrt, surface_sqrt); + for (int config_i = 0; config_i < font->SourcesCount; config_i++) + if (font->Sources) + { + const ImFontConfig* src = &font->Sources[config_i]; + int oversample_h, oversample_v; + ImFontAtlasBuildGetOversampleFactors(src, &oversample_h, &oversample_v); + BulletText("Input %d: \'%s\', Oversample: (%d=>%d,%d=>%d), PixelSnapH: %d, Offset: (%.1f,%.1f)", + config_i, src->Name, src->OversampleH, oversample_h, src->OversampleV, oversample_v, src->PixelSnapH, src->GlyphOffset.x, src->GlyphOffset.y); + } + + // Display all glyphs of the fonts in separate pages of 256 characters + { + if (TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size)) + { + ImDrawList* draw_list = GetWindowDrawList(); + const ImU32 glyph_col = GetColorU32(ImGuiCol_Text); + const float cell_size = font->FontSize * 1; + const float cell_spacing = GetStyle().ItemSpacing.y; + for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256) + { + // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k) + // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT + // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here) + if (!(base & 8191) && font->IsGlyphRangeUnused(base, base + 8191)) + { + base += 8192 - 256; + continue; + } + + int count = 0; + for (unsigned int n = 0; n < 256; n++) + if (font->FindGlyphNoFallback((ImWchar)(base + n))) + count++; + if (count <= 0) + continue; + if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph")) + continue; + + // Draw a 16x16 grid of glyphs + ImVec2 base_pos = GetCursorScreenPos(); + for (unsigned int n = 0; n < 256; n++) + { + // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions + // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string. + ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing)); + ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size); + const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n)); + draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50)); + if (!glyph) + continue; + font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n)); + if (IsMouseHoveringRect(cell_p1, cell_p2) && BeginTooltip()) + { + DebugNodeFontGlyph(font, glyph); + EndTooltip(); + } + } + Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16)); + TreePop(); + } + TreePop(); + } + } + TreePop(); + Unindent(); +} + +void ImGui::DebugNodeFontGlyph(ImFont*, const ImFontGlyph* glyph) +{ + Text("Codepoint: U+%04X", glyph->Codepoint); + Separator(); + Text("Visible: %d", glyph->Visible); + Text("AdvanceX: %.1f", glyph->AdvanceX); + Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1); + Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1); +} + +// [DEBUG] Display contents of ImGuiStorage +void ImGui::DebugNodeStorage(ImGuiStorage* storage, const char* label) +{ + if (!TreeNode(label, "%s: %d entries, %d bytes", label, storage->Data.Size, storage->Data.size_in_bytes())) + return; + for (const ImGuiStoragePair& p : storage->Data) + { + BulletText("Key 0x%08X Value { i: %d }", p.key, p.val_i); // Important: we currently don't store a type, real value may not be integer. + DebugLocateItemOnHover(p.key); + } + TreePop(); +} + +// [DEBUG] Display contents of ImGuiTabBar +void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) +{ + // Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings. + char buf[256]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + const bool is_active = (tab_bar->PrevFrameVisible >= GetFrameCount() - 2); + p += ImFormatString(p, buf_end - p, "%s 0x%08X (%d tabs)%s {", label, tab_bar->ID, tab_bar->Tabs.Size, is_active ? "" : " *Inactive*"); + for (int tab_n = 0; tab_n < ImMin(tab_bar->Tabs.Size, 3); tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + p += ImFormatString(p, buf_end - p, "%s'%s'", tab_n > 0 ? ", " : "", TabBarGetTabName(tab_bar, tab)); + } + p += ImFormatString(p, buf_end - p, (tab_bar->Tabs.Size > 3) ? " ... }" : " } "); + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(label, "%s", buf); + if (!is_active) { PopStyleColor(); } + if (is_active && IsItemHovered()) + { + ImDrawList* draw_list = GetForegroundDrawList(); + draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + } + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + PushID(tab); + if (SmallButton("<")) { TabBarQueueReorder(tab_bar, tab, -1); } SameLine(0, 2); + if (SmallButton(">")) { TabBarQueueReorder(tab_bar, tab, +1); } SameLine(); + Text("%02d%c Tab 0x%08X '%s' Offset: %.2f, Width: %.2f/%.2f", + tab_n, (tab->ID == tab_bar->SelectedTabId) ? '*' : ' ', tab->ID, TabBarGetTabName(tab_bar, tab), tab->Offset, tab->Width, tab->ContentWidth); + PopID(); + } + TreePop(); + } +} + +void ImGui::DebugNodeViewport(ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + SetNextItemOpen(true, ImGuiCond_Once); + bool open = TreeNode("viewport0", "Viewport #%d", 0); + if (IsItemHovered()) + g.DebugMetricsConfig.HighlightViewportID = viewport->ID; + if (open) + { + ImGuiWindowFlags flags = viewport->Flags; + BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Inset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f", + viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y, + viewport->WorkInsetMin.x, viewport->WorkInsetMin.y, viewport->WorkInsetMax.x, viewport->WorkInsetMax.y); + BulletText("Flags: 0x%04X =%s%s%s", viewport->Flags, + (flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", + (flags & ImGuiViewportFlags_IsPlatformMonitor) ? " IsPlatformMonitor" : "", + (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : ""); + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + TreePop(); + } +} + +void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) +{ + if (window == NULL) + { + BulletText("%s: NULL", label); + return; + } + + ImGuiContext& g = *GImGui; + const bool is_active = window->WasActive; + ImGuiTreeNodeFlags tree_node_flags = (window == g.NavWindow) ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + const bool open = TreeNodeEx(label, tree_node_flags, "%s '%s'%s", label, window->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered() && is_active) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!open) + return; + + if (window->MemoryCompacted) + TextDisabled("Note: some memory buffers have been compacted/freed."); + + if (g.IO.ConfigDebugIsDebuggerPresent && DebugBreakButton("**DebugBreak**", "in Begin()")) + g.DebugBreakInWindow = window->ID; + + ImGuiWindowFlags flags = window->Flags; + DebugNodeDrawList(window, window->Viewport, window->DrawList, "DrawList"); + BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y, window->ContentSizeIdeal.x, window->ContentSizeIdeal.y); + BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, + (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", + (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", + (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); + if (flags & ImGuiWindowFlags_ChildWindow) + BulletText("ChildFlags: 0x%08X (%s%s%s%s..)", window->ChildFlags, + (window->ChildFlags & ImGuiChildFlags_Borders) ? "Borders " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeX) ? "ResizeX " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeY) ? "ResizeY " : "", + (window->ChildFlags & ImGuiChildFlags_NavFlattened) ? "NavFlattened " : ""); + BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y, window->ScrollbarX ? "X" : "", window->ScrollbarY ? "Y" : ""); + BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); + BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + { + ImRect r = window->NavRectRel[layer]; + if (r.Min.x >= r.Max.x && r.Min.y >= r.Max.y) + BulletText("NavLastIds[%d]: 0x%08X", layer, window->NavLastIds[layer]); + else + BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window->NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y); + DebugLocateItemOnHover(window->NavLastIds[layer]); + } + const ImVec2* pr = window->NavPreferredScoringPosRel; + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. + BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); + if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } + if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } + if (window->ParentWindowForFocusRoute != NULL) { DebugNodeWindow(window->ParentWindowForFocusRoute, "ParentWindowForFocusRoute"); } + if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } + if (window->ColumnsStorage.Size > 0 && TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) + { + for (ImGuiOldColumns& columns : window->ColumnsStorage) + DebugNodeColumns(&columns); + TreePop(); + } + DebugNodeStorage(&window->StateStorage, "Storage"); + TreePop(); +} + +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings* settings) +{ + if (settings->WantDelete) + BeginDisabled(); + Text("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", + settings->ID, settings->GetName(), settings->Pos.x, settings->Pos.y, settings->Size.x, settings->Size.y, settings->Collapsed); + if (settings->WantDelete) + EndDisabled(); +} + +void ImGui::DebugNodeWindowsList(ImVector* windows, const char* label) +{ + if (!TreeNode(label, "%s (%d)", label, windows->Size)) + return; + for (int i = windows->Size - 1; i >= 0; i--) // Iterate front to back + { + PushID((*windows)[i]); + DebugNodeWindow((*windows)[i], "Window"); + PopID(); + } + TreePop(); +} + +// FIXME-OPT: This is technically suboptimal, but it is simpler this way. +void ImGui::DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack) +{ + for (int i = 0; i < windows_size; i++) + { + ImGuiWindow* window = windows[i]; + if (window->ParentWindowInBeginStack != parent_in_begin_stack) + continue; + char buf[20]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "[%04d] Window", window->BeginOrderWithinContext); + //BulletText("[%04d] Window '%s'", window->BeginOrderWithinContext, window->Name); + DebugNodeWindow(window, buf); + Indent(); + DebugNodeWindowsListByBeginStackParent(windows + i + 1, windows_size - i - 1, window); + Unindent(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DEBUG LOG WINDOW +//----------------------------------------------------------------------------- + +void ImGui::DebugLog(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + DebugLogV(fmt, args); + va_end(args); +} + +void ImGui::DebugLogV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + const int old_size = g.DebugLogBuf.size(); + if (g.ContextName[0] != 0) + g.DebugLogBuf.appendf("[%s] [%05d] ", g.ContextName, g.FrameCount); + else + g.DebugLogBuf.appendf("[%05d] ", g.FrameCount); + g.DebugLogBuf.appendfv(fmt, args); + g.DebugLogIndex.append(g.DebugLogBuf.c_str(), old_size, g.DebugLogBuf.size()); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTTY) + IMGUI_DEBUG_PRINTF("%s", g.DebugLogBuf.begin() + old_size); +#ifdef IMGUI_ENABLE_TEST_ENGINE + // IMGUI_TEST_ENGINE_LOG() adds a trailing \n automatically + const int new_size = g.DebugLogBuf.size(); + const bool trailing_carriage_return = (g.DebugLogBuf[new_size - 1] == '\n'); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTestEngine) + IMGUI_TEST_ENGINE_LOG("%.*s", new_size - old_size - (trailing_carriage_return ? 1 : 0), g.DebugLogBuf.begin() + old_size); +#endif +} + +// FIXME-LAYOUT: To be done automatically via layout mode once we rework ItemSize/ItemAdd into ItemLayout. +static void SameLineOrWrap(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos(window->DC.CursorPosPrevLine.x + g.Style.ItemSpacing.x, window->DC.CursorPosPrevLine.y); + if (window->WorkRect.Contains(ImRect(pos, pos + size))) + ImGui::SameLine(); +} + +static void ShowDebugLogFlag(const char* name, ImGuiDebugLogFlags flags) +{ + ImGuiContext& g = *GImGui; + ImVec2 size(ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui::CalcTextSize(name).x, ImGui::GetFrameHeight()); + SameLineOrWrap(size); // FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout. + + bool highlight_errors = (flags == ImGuiDebugLogFlags_EventError && g.DebugLogSkippedErrors > 0); + if (highlight_errors) + ImGui::PushStyleColor(ImGuiCol_Text, ImLerp(g.Style.Colors[ImGuiCol_Text], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.30f)); + if (ImGui::CheckboxFlags(name, &g.DebugLogFlags, flags) && g.IO.KeyShift && (g.DebugLogFlags & flags) != 0) + { + g.DebugLogAutoDisableFrames = 2; + g.DebugLogAutoDisableFlags |= flags; + } + if (highlight_errors) + { + ImGui::PopStyleColor(); + ImGui::SetItemTooltip("%d past errors skipped.", g.DebugLogSkippedErrors); + } + else + { + ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); + } +} + +void ImGui::ShowDebugLogWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 12.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui Debug Log", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + ImGuiDebugLogFlags all_enable_flags = ImGuiDebugLogFlags_EventMask_ & ~ImGuiDebugLogFlags_EventInputRouting; + CheckboxFlags("All", &g.DebugLogFlags, all_enable_flags); + SetItemTooltip("(except InputRouting which is spammy)"); + + ShowDebugLogFlag("Errors", ImGuiDebugLogFlags_EventError); + ShowDebugLogFlag("ActiveId", ImGuiDebugLogFlags_EventActiveId); + ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper); + ShowDebugLogFlag("Focus", ImGuiDebugLogFlags_EventFocus); + ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO); + //ShowDebugLogFlag("Font", ImGuiDebugLogFlags_EventFont); + ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav); + ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup); + ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); + ShowDebugLogFlag("InputRouting", ImGuiDebugLogFlags_EventInputRouting); + + if (SmallButton("Clear")) + { + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + g.DebugLogSkippedErrors = 0; + } + SameLine(); + if (SmallButton("Copy")) + SetClipboardText(g.DebugLogBuf.c_str()); + SameLine(); + if (SmallButton("Configure Outputs..")) + OpenPopup("Outputs"); + if (BeginPopup("Outputs")) + { + CheckboxFlags("OutputToTTY", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTTY); +#ifndef IMGUI_ENABLE_TEST_ENGINE + BeginDisabled(); +#endif + CheckboxFlags("OutputToTestEngine", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTestEngine); +#ifndef IMGUI_ENABLE_TEST_ENGINE + EndDisabled(); +#endif + EndPopup(); + } + + BeginChild("##log", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + + const ImGuiDebugLogFlags backup_log_flags = g.DebugLogFlags; + g.DebugLogFlags &= ~ImGuiDebugLogFlags_EventClipper; + + ImGuiListClipper clipper; + clipper.Begin(g.DebugLogIndex.size()); + while (clipper.Step()) + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + DebugTextUnformattedWithLocateItem(g.DebugLogIndex.get_line_begin(g.DebugLogBuf.c_str(), line_no), g.DebugLogIndex.get_line_end(g.DebugLogBuf.c_str(), line_no)); + g.DebugLogFlags = backup_log_flags; + if (GetScrollY() >= GetScrollMaxY()) + SetScrollHereY(1.0f); + EndChild(); + + End(); +} + +// Display line, search for 0xXXXXXXXX identifiers and call DebugLocateItemOnHover() when hovered. +void ImGui::DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end) +{ + TextUnformatted(line_begin, line_end); + if (!IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + return; + ImGuiContext& g = *GImGui; + ImRect text_rect = g.LastItemData.Rect; + for (const char* p = line_begin; p <= line_end - 10; p++) + { + ImGuiID id = 0; + if (p[0] != '0' || (p[1] != 'x' && p[1] != 'X') || sscanf(p + 2, "%X", &id) != 1 || ImCharIsXdigitA(p[10])) + continue; + ImVec2 p0 = CalcTextSize(line_begin, p); + ImVec2 p1 = CalcTextSize(p, p + 10); + g.LastItemData.Rect = ImRect(text_rect.Min + ImVec2(p0.x, 0.0f), text_rect.Min + ImVec2(p0.x + p1.x, p1.y)); + if (IsMouseHoveringRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, true)) + DebugLocateItemOnHover(id); + p += 10; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) +//----------------------------------------------------------------------------- + +// Draw a small cross at current CursorPos in current window's DrawList +void ImGui::DebugDrawCursorPos(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos = window->DC.CursorPos; + window->DrawList->AddLine(ImVec2(pos.x, pos.y - 3.0f), ImVec2(pos.x, pos.y + 4.0f), col, 1.0f); + window->DrawList->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f); +} + +// Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList +void ImGui::DebugDrawLineExtents(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float curr_x = window->DC.CursorPos.x; + float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y); + float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(curr_x + 5.0f, line_y1), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 0.5f, line_y1), ImVec2(curr_x - 0.5f, line_y2), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y2), ImVec2(curr_x + 5.0f, line_y2), col, 1.0f); +} + +// Draw last item rect in ForegroundDrawList (so it is always visible) +void ImGui::DebugDrawItemRect(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); +} + +// [DEBUG] Locate item position/rectangle given an ID. +static const ImU32 DEBUG_LOCATE_ITEM_COLOR = IM_COL32(0, 255, 0, 255); // Green + +void ImGui::DebugLocateItem(ImGuiID target_id) +{ + ImGuiContext& g = *GImGui; + g.DebugLocateId = target_id; + g.DebugLocateFrames = 2; + g.DebugBreakInLocateId = false; +} + +// FIXME: Doesn't work over through a modal window, because they clear HoveredWindow. +void ImGui::DebugLocateItemOnHover(ImGuiID target_id) +{ + if (target_id == 0 || !IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return; + ImGuiContext& g = *GImGui; + DebugLocateItem(target_id); + GetForegroundDrawList(g.CurrentWindow)->AddRect(g.LastItemData.Rect.Min - ImVec2(3.0f, 3.0f), g.LastItemData.Rect.Max + ImVec2(3.0f, 3.0f), DEBUG_LOCATE_ITEM_COLOR); + + // Can't easily use a context menu here because it will mess with focus, active id etc. + if (g.IO.ConfigDebugIsDebuggerPresent && g.MouseStationaryTimer > 1.0f) + { + DebugBreakButtonTooltip(false, "in ItemAdd()"); + if (IsKeyChordPressed(g.DebugBreakKeyChord)) + g.DebugBreakInLocateId = true; + } +} + +void ImGui::DebugLocateItemResolveWithLastItem() +{ + ImGuiContext& g = *GImGui; + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInLocateId) + IM_DEBUG_BREAK(); + + ImGuiLastItemData item_data = g.LastItemData; + g.DebugLocateId = 0; + ImDrawList* draw_list = GetForegroundDrawList(g.CurrentWindow); + ImRect r = item_data.Rect; + r.Expand(3.0f); + ImVec2 p1 = g.IO.MousePos; + ImVec2 p2 = ImVec2((p1.x < r.Min.x) ? r.Min.x : (p1.x > r.Max.x) ? r.Max.x : p1.x, (p1.y < r.Min.y) ? r.Min.y : (p1.y > r.Max.y) ? r.Max.y : p1.y); + draw_list->AddRect(r.Min, r.Max, DEBUG_LOCATE_ITEM_COLOR); + draw_list->AddLine(p1, p2, DEBUG_LOCATE_ITEM_COLOR); +} + +void ImGui::DebugStartItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerActive = true; +} + +// [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. +void ImGui::UpdateDebugToolItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerBreakId = 0; + if (!g.DebugItemPickerActive) + return; + + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + SetMouseCursor(ImGuiMouseCursor_Hand); + if (IsKeyPressed(ImGuiKey_Escape)) + g.DebugItemPickerActive = false; + const bool change_mapping = g.IO.KeyMods == (ImGuiMod_Ctrl | ImGuiMod_Shift); + if (!change_mapping && IsMouseClicked(g.DebugItemPickerMouseButton) && hovered_id) + { + g.DebugItemPickerBreakId = hovered_id; + g.DebugItemPickerActive = false; + } + for (int mouse_button = 0; mouse_button < 3; mouse_button++) + if (change_mapping && IsMouseClicked(mouse_button)) + g.DebugItemPickerMouseButton = (ImU8)mouse_button; + SetNextWindowBgAlpha(0.70f); + if (!BeginTooltip()) + return; + Text("HoveredId: 0x%08X", hovered_id); + Text("Press ESC to abort picking."); + const char* mouse_button_names[] = { "Left", "Right", "Middle" }; + if (change_mapping) + Text("Remap w/ Ctrl+Shift: click anywhere to select new mouse button."); + else + TextColored(GetStyleColorVec4(hovered_id ? ImGuiCol_Text : ImGuiCol_TextDisabled), "Click %s Button to break in debugger! (remap w/ Ctrl+Shift)", mouse_button_names[g.DebugItemPickerMouseButton]); + EndTooltip(); +} + +// [DEBUG] ID Stack Tool: update queries. Called by NewFrame() +void ImGui::UpdateDebugToolStackQueries() +{ + ImGuiContext& g = *GImGui; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Clear hook when id stack tool is not visible + g.DebugHookIdInfo = 0; + if (g.FrameCount != tool->LastActiveFrame + 1) + return; + + // Update queries. The steps are: -1: query Stack, >= 0: query each stack item + // We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time + const ImGuiID query_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId; + if (tool->QueryId != query_id) + { + tool->QueryId = query_id; + tool->StackLevel = -1; + tool->Results.resize(0); + } + if (query_id == 0) + return; + + // Advance to next stack level when we got our result, or after 2 frames (in case we never get a result) + int stack_level = tool->StackLevel; + if (stack_level >= 0 && stack_level < tool->Results.Size) + if (tool->Results[stack_level].QuerySuccess || tool->Results[stack_level].QueryFrameCount > 2) + tool->StackLevel++; + + // Update hook + stack_level = tool->StackLevel; + if (stack_level == -1) + g.DebugHookIdInfo = query_id; + if (stack_level >= 0 && stack_level < tool->Results.Size) + { + g.DebugHookIdInfo = tool->Results[stack_level].ID; + tool->Results[stack_level].QueryFrameCount++; + } +} + +// [DEBUG] ID Stack tool: hooks called by GetID() family functions +void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Step 0: stack query + // This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget. + if (tool->StackLevel == -1) + { + tool->StackLevel++; + tool->Results.resize(window->IDStack.Size + 1, ImGuiStackLevelInfo()); + for (int n = 0; n < window->IDStack.Size + 1; n++) + tool->Results[n].ID = (n < window->IDStack.Size) ? window->IDStack[n] : id; + return; + } + + // Step 1+: query for individual level + IM_ASSERT(tool->StackLevel >= 0); + if (tool->StackLevel != window->IDStack.Size) + return; + ImGuiStackLevelInfo* info = &tool->Results[tool->StackLevel]; + IM_ASSERT(info->ID == id && info->QueryFrameCount > 0); + + switch (data_type) + { + case ImGuiDataType_S32: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%d", (int)(intptr_t)data_id); + break; + case ImGuiDataType_String: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%.*s", data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)ImStrlen((const char*)data_id), (const char*)data_id); + break; + case ImGuiDataType_Pointer: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "(void*)0x%p", data_id); + break; + case ImGuiDataType_ID: + if (info->Desc[0] != 0) // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one. + return; + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "0x%08X [override]", id); + break; + default: + IM_ASSERT(0); + } + info->QuerySuccess = true; + info->DataType = data_type; +} + +static int StackToolFormatLevelInfo(ImGuiIDStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size) +{ + ImGuiStackLevelInfo* info = &tool->Results[n]; + ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL; + if (window) // Source: window name (because the root ID don't call GetID() and so doesn't get hooked) + return ImFormatString(buf, buf_size, format_for_ui ? "\"%s\" [window]" : "%s", window->Name); + if (info->QuerySuccess) // Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id) + return ImFormatString(buf, buf_size, (format_for_ui && info->DataType == ImGuiDataType_String) ? "\"%s\"" : "%s", info->Desc); + if (tool->StackLevel < tool->Results.Size) // Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers. + return (*buf = 0); +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (const char* label = ImGuiTestEngine_FindItemDebugLabel(GImGui, info->ID)) // Source: ImGuiTestEngine's ItemInfo() + return ImFormatString(buf, buf_size, format_for_ui ? "??? \"%s\"" : "%s", label); +#endif + return ImFormatString(buf, buf_size, "???"); +} + +// ID Stack Tool: Display UI +void ImGui::ShowIDStackToolWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 8.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui ID Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // Display hovered/active status + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Build and display path + tool->ResultPathBuf.resize(0); + for (int stack_n = 0; stack_n < tool->Results.Size; stack_n++) + { + char level_desc[256]; + StackToolFormatLevelInfo(tool, stack_n, false, level_desc, IM_ARRAYSIZE(level_desc)); + tool->ResultPathBuf.append(stack_n == 0 ? "//" : "/"); + for (int n = 0; level_desc[n]; n++) + { + if (level_desc[n] == '/') + tool->ResultPathBuf.append("\\"); + tool->ResultPathBuf.append(level_desc + n, level_desc + n + 1); + } + } + Text("0x%08X", tool->QueryId); + SameLine(); + MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details."); + + // CTRL+C to copy path + const float time_since_copy = (float)g.Time - tool->CopyToClipboardLastTime; + SameLine(); + PushStyleVarY(ImGuiStyleVar_FramePadding, 0.0f); Checkbox("Ctrl+C: copy path", &tool->CopyToClipboardOnCtrlC); PopStyleVar(); + SameLine(); + TextColored((time_since_copy >= 0.0f && time_since_copy < 0.75f && ImFmod(time_since_copy, 0.25f) < 0.25f * 0.5f) ? ImVec4(1.f, 1.f, 0.3f, 1.f) : ImVec4(), "*COPIED*"); + if (tool->CopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused)) + { + tool->CopyToClipboardLastTime = (float)g.Time; + SetClipboardText(tool->ResultPathBuf.c_str()); + } + + Text("- Path \"%s\"", tool->ResultPathBuf.c_str()); +#ifdef IMGUI_ENABLE_TEST_ENGINE + Text("- Label \"%s\"", tool->QueryId ? ImGuiTestEngine_FindItemDebugLabel(&g, tool->QueryId) : ""); +#endif + + Separator(); + + // Display decorated stack + tool->LastActiveFrame = g.FrameCount; + if (tool->Results.Size > 0 && BeginTable("##table", 3, ImGuiTableFlags_Borders)) + { + const float id_width = CalcTextSize("0xDDDDDDDD").x; + TableSetupColumn("Seed", ImGuiTableColumnFlags_WidthFixed, id_width); + TableSetupColumn("PushID", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Result", ImGuiTableColumnFlags_WidthFixed, id_width); + TableHeadersRow(); + for (int n = 0; n < tool->Results.Size; n++) + { + ImGuiStackLevelInfo* info = &tool->Results[n]; + TableNextColumn(); + Text("0x%08X", (n > 0) ? tool->Results[n - 1].ID : 0); + TableNextColumn(); + StackToolFormatLevelInfo(tool, n, true, g.TempBuffer.Data, g.TempBuffer.Size); + TextUnformatted(g.TempBuffer.Data); + TableNextColumn(); + Text("0x%08X", info->ID); + if (n == tool->Results.Size - 1) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_Header)); + } + EndTable(); + } + End(); +} + +#else + +void ImGui::ShowMetricsWindow(bool*) {} +void ImGui::ShowFontAtlas(ImFontAtlas*) {} +void ImGui::DebugNodeColumns(ImGuiOldColumns*) {} +void ImGui::DebugNodeDrawList(ImGuiWindow*, ImGuiViewportP*, const ImDrawList*, const char*) {} +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {} +void ImGui::DebugNodeFont(ImFont*) {} +void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {} +void ImGui::DebugNodeTabBar(ImGuiTabBar*, const char*) {} +void ImGui::DebugNodeWindow(ImGuiWindow*, const char*) {} +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings*) {} +void ImGui::DebugNodeWindowsList(ImVector*, const char*) {} +void ImGui::DebugNodeViewport(ImGuiViewportP*) {} + +void ImGui::ShowDebugLogWindow(bool*) {} +void ImGui::ShowIDStackToolWindow(bool*) {} +void ImGui::DebugStartItemPicker() {} +void ImGui::DebugHookIdInfo(ImGuiID, ImGuiDataType, const void*, const void*) {} + +#endif // #ifndef IMGUI_DISABLE_DEBUG_TOOLS + +//----------------------------------------------------------------------------- + +// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. +// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. +#ifdef IMGUI_INCLUDE_IMGUI_USER_INL +#include "imgui_user.inl" +#endif + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imgui.h b/thirdparty/imgui/imgui.h new file mode 100644 index 00000000..75750ae1 --- /dev/null +++ b/thirdparty/imgui/imgui.h @@ -0,0 +1,3770 @@ +// dear imgui, v1.91.9b +// (headers) + +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. + +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues?q=label%3Agallery (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Library Version +// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') +#define IMGUI_VERSION "1.91.9b" +#define IMGUI_VERSION_NUM 19191 +#define IMGUI_HAS_TABLE + +/* + +Index of this file: +// [SECTION] Header mess +// [SECTION] Forward declarations and basic types +// [SECTION] Texture identifier (ImTextureID) +// [SECTION] Dear ImGui end-user API functions +// [SECTION] Flags & Enumerations +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Helpers: Debug log, Memory allocations macros, ImVector<> +// [SECTION] ImGuiStyle +// [SECTION] ImGuiIO +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage, ImGuiSelectionExternalStorage) +// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) +// [SECTION] ImGuiPlatformIO + other Platform Dependent Interfaces (ImGuiPlatformImeData) +// [SECTION] Obsolete functions and types + +*/ + +#pragma once + +// Configuration file with compile-time options +// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system) +#ifdef IMGUI_USER_CONFIG +#include IMGUI_USER_CONFIG +#endif +#include "imconfig.h" + +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +// Includes +#include // FLT_MIN, FLT_MAX +#include // va_list, va_start, va_end +#include // ptrdiff_t, NULL +#include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp + +// Define attributes of all API symbols declarations (e.g. for DLL under Windows) +// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) +// Using dear imgui via a shared library is not recommended: we don't guarantee backward nor forward ABI compatibility + this is a call-heavy library and function call overhead adds up. +#ifndef IMGUI_API +#define IMGUI_API +#endif +#ifndef IMGUI_IMPL_API +#define IMGUI_IMPL_API IMGUI_API +#endif + +// Helper Macros +#ifndef IM_ASSERT +#include +#define IM_ASSERT(_EXPR) assert(_EXPR) // You can override the default assert handler by editing imconfig.h +#endif +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! +#define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. + +// Check that version and structures layouts are matching between compiled imgui code and caller. Read comments above DebugCheckVersionAndDataLayout() for details. +#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) + +// Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +// (MSVC provides an equivalent mechanism via SAL Annotations but it would require the macros in a different +// location. e.g. #include + void myprintf(_Printf_format_string_ const char* format, ...)) +#if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) +#define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) +#elif !defined(IMGUI_USE_STB_SPRINTF) && (defined(__clang__) || defined(__GNUC__)) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) +#else +#define IM_FMTARGS(FMT) +#define IM_FMTLIST(FMT) +#endif + +// Disable some of MSVC most aggressive Debug runtime checks in function header/footer (used in some simple/low-level functions) +#if defined(_MSC_VER) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(IMGUI_DEBUG_PARANOID) +#define IM_MSVC_RUNTIME_CHECKS_OFF __pragma(runtime_checks("",off)) __pragma(check_stack(off)) __pragma(strict_gs_check(push,off)) +#define IM_MSVC_RUNTIME_CHECKS_RESTORE __pragma(runtime_checks("",restore)) __pragma(check_stack()) __pragma(strict_gs_check(pop)) +#else +#define IM_MSVC_RUNTIME_CHECKS_OFF +#define IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#endif +#if defined(__clang__) +#pragma clang diagnostic push +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations and basic types +//----------------------------------------------------------------------------- + +// Scalar data types +typedef unsigned int ImGuiID;// A unique ID used by widgets (typically the result of hashing a stack of string) +typedef signed char ImS8; // 8-bit signed integer +typedef unsigned char ImU8; // 8-bit unsigned integer +typedef signed short ImS16; // 16-bit signed integer +typedef unsigned short ImU16; // 16-bit unsigned integer +typedef signed int ImS32; // 32-bit signed integer == int +typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) +typedef signed long long ImS64; // 64-bit signed integer +typedef unsigned long long ImU64; // 64-bit unsigned integer + +// Forward declarations: ImDrawList, ImFontAtlas layer +struct ImDrawChannel; // Temporary storage to output draw commands out of order, used by ImDrawListSplitter and ImDrawList::ChannelsSplit() +struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call, unless it is a callback) +struct ImDrawData; // All draw command lists required to render the frame + pos/size coordinates to use for the projection matrix. +struct ImDrawList; // A single draw command list (generally one per window, conceptually you may see this as a dynamic "mesh" builder) +struct ImDrawListSharedData; // Data shared among multiple draw lists (typically owned by parent ImGui context, but you may create one yourself) +struct ImDrawListSplitter; // Helper to split a draw list into different layers which can be drawn into out of order, then flattened back. +struct ImDrawVert; // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) +struct ImFont; // Runtime data for a single font within a parent ImFontAtlas +struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontBuilderIO; // Opaque interface to a font builder (stb_truetype or FreeType). +struct ImFontConfig; // Configuration data when adding a font or merging fonts +struct ImFontGlyph; // A single font glyph (code point + coordinates within in ImFontAtlas + offset) +struct ImFontGlyphRangesBuilder; // Helper to build glyph ranges from text/string data +struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using) + +// Forward declarations: ImGui layer +struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) +struct ImGuiIO; // Main configuration and I/O between your application and ImGui (also see: ImGuiPlatformIO) +struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) +struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSelect()/EndMultiSelect() block +struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformIO; // Interface between platform/renderer backends and ImGui (e.g. Clipboard, IME hooks). Extends ImGuiIO. In docking branch, this gets extended to support multi-viewports. +struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. +struct ImGuiSelectionBasicStorage; // Optional helper to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionExternalStorage;//Optional helper to apply multi-selection requests to existing randomly accessible storage. +struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) +struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) +struct ImGuiStorage; // Helper for key->value storage (container sorted by key) +struct ImGuiStoragePair; // Helper for key->value storage (pair) +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table +struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) +struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") +struct ImGuiViewport; // A Platform Window (always only one in 'master' branch), in the future may represent Platform Monitor + +// Enumerations +// - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +enum ImGuiDir : int; // -> enum ImGuiDir // Enum: A cardinal direction (Left, Right, Up, Down) +enum ImGuiKey : int; // -> enum ImGuiKey // Enum: A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value) +enum ImGuiMouseSource : int; // -> enum ImGuiMouseSource // Enum; A mouse input source identifier (Mouse, TouchScreen, Pen) +enum ImGuiSortDirection : ImU8; // -> enum ImGuiSortDirection // Enum: A sorting direction (ascending or descending) +typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling +typedef int ImGuiCond; // -> enum ImGuiCond_ // Enum: A condition for many Set*() functions +typedef int ImGuiDataType; // -> enum ImGuiDataType_ // Enum: A primary data type +typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) +typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor shape +typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling +typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() + +// Flags (declared as int to allow using as flags without overhead, and to not pollute the top of this file) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions +typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build +typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags +typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiChildFlags; // -> enum ImGuiChildFlags_ // Flags: for BeginChild() +typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. +typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags +typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() +typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() +typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. +typedef int ImGuiInputFlags; // -> enum ImGuiInputFlags_ // Flags: for Shortcut(), SetNextItemShortcut() +typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() +typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag(), shared by all items +typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. +typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() +typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() +typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() +typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() +typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() +typedef int ImGuiViewportFlags; // -> enum ImGuiViewportFlags_ // Flags: for ImGuiViewport +typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() + +// Character types +// (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) +typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. +#ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] +typedef ImWchar32 ImWchar; +#else +typedef ImWchar16 ImWchar; +#endif + +// Multi-Selection item index or identifier when using BeginMultiSelect() +// - Used by SetNextItemSelectionUserData() + and inside ImGuiMultiSelectIO structure. +// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER/ID as well. Read comments near ImGuiMultiSelectIO for details. +typedef ImS64 ImGuiSelectionUserData; + +// Callback and functions types +typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() +typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() +typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() + +// ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] +// - This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec2 +{ + float x, y; + constexpr ImVec2() : x(0.0f), y(0.0f) { } + constexpr ImVec2(float _x, float _y) : x(_x), y(_y) { } + float& operator[] (size_t idx) { IM_ASSERT(idx == 0 || idx == 1); return ((float*)(void*)(char*)this)[idx]; } // We very rarely use this [] operator, so the assert overhead is fine. + float operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return ((const float*)(const void*)(const char*)this)[idx]; } +#ifdef IM_VEC2_CLASS_EXTRA + IM_VEC2_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec2. +#endif +}; + +// ImVec4: 4D vector used to store clipping rectangles, colors etc. [Compile-time configurable type] +struct ImVec4 +{ + float x, y, z, w; + constexpr ImVec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) { } + constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) { } +#ifdef IM_VEC4_CLASS_EXTRA + IM_VEC4_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec4. +#endif +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] Texture identifier (ImTextureID) +//----------------------------------------------------------------------------- + +// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type] +// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file. +// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details. +// - You can make this a structure with various constructors if you need. You will have to implement ==/!= operators. +// - (note: before v1.91.4 (2024/10/08) the default type for ImTextureID was void*. Use intermediary intptr_t cast and read FAQ if you have casting warnings) +#ifndef ImTextureID +typedef ImU64 ImTextureID; // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that) +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Dear ImGui end-user API functions +// (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!) +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Context creation and access + // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for details. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); // access the ImGuiIO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) + IMGUI_API ImGuiPlatformIO& GetPlatformIO(); // access the ImGuiPlatformIO structure (mostly hooks/functions to connect to platform/renderer and OS Clipboard, IME etc.) + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleColor(), PushStyleVar() to modify style mid-frame! + IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! + IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. + + // Demo, Debug, Information + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. + IMGUI_API void ShowDebugLogWindow(bool* p_open = NULL); // create Debug Log window. display a simplified log of important dear imgui events. + IMGUI_API void ShowIDStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. + IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. + IMGUI_API void ShowFontSelector(const char* label); // add font selector block (not a window), essentially a combo listing the loaded fonts. + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as an end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.80 WIP" (essentially the value for IMGUI_VERSION from the compiled version of imgui.cpp) + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // new, recommended style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // best used with borders and a custom, thicker font + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style + + // Windows + // - Begin() = push window to the stack and start appending to it. End() = pop window from the stack. + // - Passing 'bool* p_open != NULL' shows a window-closing widget in the upper-right corner of the window, + // which clicking will set the boolean to false when clicked. + // - You may append multiple times to the same window during the same frame by calling Begin()/End() pairs multiple times. + // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). + // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + // - Note that the bottom of window stack always contains a window called "Debug". + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); + IMGUI_API void End(); + + // Child Windows + // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. + // - Before 1.90 (November 2023), the "ImGuiChildFlags child_flags = 0" parameter was "bool border = false". + // This API is backward compatible with old code, as we guarantee that ImGuiChildFlags_Borders == true. + // Consider updating your old code: + // BeginChild("Name", size, false) -> Begin("Name", size, 0); or Begin("Name", size, ImGuiChildFlags_None); + // BeginChild("Name", size, true) -> Begin("Name", size, ImGuiChildFlags_Borders); + // - Manual sizing (each axis can use a different setting e.g. ImVec2(0.0f, 400.0f)): + // == 0.0f: use remaining parent window size for this axis. + // > 0.0f: use specified size for this axis. + // < 0.0f: right/bottom-align to specified distance from available content boundaries. + // - Specifying ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY makes the sizing automatic based on child contents. + // Combining both ImGuiChildFlags_AutoResizeX _and_ ImGuiChildFlags_AutoResizeY defeats purpose of a scrolling region and is NOT recommended. + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API void EndChild(); + + // Windows Utilities + // - 'current window' = the window we are appending into while inside a Begin()/End() block. 'next window' = next window we will Begin() into. + IMGUI_API bool IsWindowAppearing(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. + IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API ImVec2 GetWindowSize(); // get current window size (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API float GetWindowWidth(); // get current window width (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().x. + IMGUI_API float GetWindowHeight(); // get current window height (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().y. + + // Window manipulation + // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use 0.0f or FLT_MAX if you don't want limits. Use -1 for both min and max of same axis to preserve current size (which itself is a constraint). Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() + IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // set next window scrolling value (use < 0.0f to not affect a given axis). + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowFontScale(float scale); // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / top-most. use NULL to remove focus. + + // Windows Scrolling + // - Any change of Scroll will be applied at the beginning of next frame in the first call to Begin(). + // - You may instead use SetNextWindowScroll() prior to calling Begin() to avoid this delay, as an alternative to using SetScrollX()/SetScrollY(). + IMGUI_API float GetScrollX(); // get scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.x - WindowSize.x - DecorationsSize.x + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.y - WindowSize.y - DecorationsSize.y + IMGUI_API void SetScrollHereX(float center_x_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_x_ratio=0.0: left, 0.5: center, 1.0: right. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollHereY(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + IMGUI_API void SetScrollFromPosY(float local_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); // modify a style color. always use this if you modify the style after NewFrame(). + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); // modify a style float variable. always use this if you modify the style after NewFrame()! + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); // modify a style ImVec2 variable. " + IMGUI_API void PushStyleVarX(ImGuiStyleVar idx, float val_x); // modify X component of a style ImVec2 variable. " + IMGUI_API void PushStyleVarY(ImGuiStyleVar idx, float val_y); // modify Y component of a style ImVec2 variable. " + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); // modify specified shared item flag, e.g. PushItemFlag(ImGuiItemFlags_NoTabStop, true) + IMGUI_API void PopItemFlag(); + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // push width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side). + IMGUI_API void PopItemWidth(); + IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side) + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position. NOT necessarily the width of last item unlike most 'Item' functions. + IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // push word-wrapping position for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + + // Style read access + // - Use the ShowStyleEditor() function to interactively see/edit the colors. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a white pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col, float alpha_mul = 1.0f); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. + + // Layout cursor positioning + // - By "cursor" we mean the current output position. + // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. + // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. + // - YOU CAN DO 99% OF WHAT YOU NEED WITH ONLY GetCursorScreenPos() and GetContentRegionAvail(). + // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: + // - Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. -> this is the preferred way forward. + // - Window-local coordinates: SameLine(offset), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), PushTextWrapPos() + // - Window-local coordinates: GetContentRegionMax(), GetWindowContentRegionMin(), GetWindowContentRegionMax() --> all obsoleted. YOU DON'T NEED THEM. + // - GetCursorScreenPos() = GetCursorPos() + GetWindowPos(). GetWindowPos() is almost only ever useful to convert from window-local to absolute coordinates. Try not to use it. + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND (prefer using this rather than GetCursorPos(), also more useful to work with ImDrawList API). + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetContentRegionAvail(); // available space from current position. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetCursorPos(); // [window-local] cursor position in window-local coordinates. This is not your best friend. + IMGUI_API float GetCursorPosX(); // [window-local] " + IMGUI_API float GetCursorPosY(); // [window-local] " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // [window-local] " + IMGUI_API void SetCursorPosX(float local_x); // [window-local] " + IMGUI_API void SetCursorPosY(float local_y); // [window-local] " + IMGUI_API ImVec2 GetCursorStartPos(); // [window-local] initial cursor position, in window-local coordinates. Call GetCursorScreenPos() after Begin() to get the absolute coordinates version. + + // Other layout functions + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. + IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in a horizontal-layout context. + IMGUI_API void Spacing(); // add vertical spacing. + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size. unlike InvisibleButton(), Dummy() won't take the mouse click or be navigable into. + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.com/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + IMGUI_API ImGuiID GetID(int int_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void SeparatorText(const char* label); // currently: formatted text with a horizontal line + + // Widgets: Main + // - Most widgets return true when the value has been changed or when pressed/selected + // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button + IMGUI_API bool SmallButton(const char* label); // button with (FramePadding.y == 0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, int* flags, int flags_value); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); // use with e.g. if (RadioButton("one", my_value==1)) { my_value = 1; } + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); // shortcut to handle the above pattern when value is an integer + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL); + IMGUI_API void Bullet(); // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + IMGUI_API bool TextLink(const char* label); // hyperlink text button, return true when clicked + IMGUI_API void TextLinkOpenURL(const char* label, const char* url = NULL); // hyperlink text button, automatically open file/url when clicked + + // Widgets: Images + // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. + // - Image() pads adds style.ImageBorderSize on each side, ImageButton() adds style.FramePadding on each side. + // - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1)); + IMGUI_API void ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + // Widgets: Combo Box (Dropdown) + // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. + // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. This is analogous to how ListBox are created. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drag Sliders + // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every function, note that a 'float v[X]' function argument is the same as 'float* v', + // the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For keyboard/gamepad navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits if ImGuiSliderFlags_AlwaysClamp is not used. + // - Use v_max = FLT_MAX / INT_MAX etc to avoid clamping to a maximum, same with v_min = -FLT_MAX / INT_MIN to avoid clamping to a minimum. + // - We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. + // - Legacy: Pre-1.78 there are DragXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Regular Sliders + // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Legacy: Pre-1.78 there are SliderXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + + // Widgets: Input with Keyboard + // - If you want to use InputText() with std::string or any custom dynamic string type, see misc/cpp/imgui_stdlib.h and comments in imgui_demo.cpp. + // - Most of the ImGuiInputTextFlags flags are only useful for InputText() and not for InputFloatX, InputIntX, InputDouble etc. + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little color square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // - Note that in C++ a 'float v[X]' function argument is the _same_ as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. + // - You can pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // display a color square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + // - TreeNode functions return true when the node is open, in which case you need to also call TreePop() when you are finished displaying the tree node contents. + IMGUI_API bool TreeNode(const char* label); + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // helper variation to easily decorelate the id from the displayed string. Read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushID(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const void* ptr_id); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopID() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. + IMGUI_API void SetNextItemOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + IMGUI_API void SetNextItemStorageID(ImGuiID storage_id); // set id to use for open/close storage (default to same as item id). + + // Widgets: Selectables + // - A selectable highlights when hovered, and can display another color when selected. + // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. This is so a series of selected Selectable appear contiguous. + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + + // Multi-selection system for Selectable(), Checkbox(), TreeNode() functions [BETA] + // - This enables standard multi-selection/range-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc.) in a way that also allow a clipper to be used. + // - ImGuiSelectionUserData is often used to store your item index within the current view (but may store something else). + // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. + // - TreeNode() is technically supported but... using this correctly is more complicated. You need some sort of linear/random access to your tree, + // which is suited to advanced trees setups already implementing filters and clipper. We will work simplifying the current demo. + // - 'selection_size' and 'items_count' parameters are optional and used by a few features. If they are costly for you to compute, you may avoid them. + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1); + IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); + IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); + IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. + + // Widgets: List Boxes + // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // - If you don't need a label you can probably simply use BeginChild() with the ImGuiChildFlags_FrameStyle flag for the same result. + // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items. + // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. + // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth + // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items + IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region + IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items = -1); + + // Widgets: Data Plotting + // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + + // Widgets: Value() Helpers. + // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Widgets: Menus + // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. + // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. + // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. + // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndMenu() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Tooltips + // - Tooltips are windows following the mouse. They do not take focus away. + // - A tooltip window can contain items of any types. + // - SetTooltip() is more or less a shortcut for the 'if (BeginTooltip()) { Text(...); EndTooltip(); }' idiom (with a subtlety that it discard any previously submitted tooltip) + IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. + IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip()/BeginItemTooltip() returns true! + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip. Often used after a ImGui::IsItemHovered() check. Override any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Tooltips: helpers for showing a tooltip when hovering an item + // - BeginItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip) && BeginTooltip())' idiom. + // - SetItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { SetTooltip(...); }' idiom. + // - Where 'ImGuiHoveredFlags_ForTooltip' itself is a shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on active input type. For mouse it defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + IMGUI_API bool BeginItemTooltip(); // begin/append a tooltip window if preceding item was hovered. + IMGUI_API void SetItemTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip if preceding item was hovered. override any previous call to SetTooltip(). + IMGUI_API void SetItemTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Popups, Modals + // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. + // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. + // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). + // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. + // This is sometimes leading to confusing mistakes. May rework this in the future. + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards if returned true. ImGuiWindowFlags are forwarded to the window. + // - BeginPopupModal(): block every interaction behind the window, cannot be closed by user, add a dimming background, has a title bar. + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + + // Popups: open/close functions + // - OpenPopup(): set popup state to open. ImGuiPopupFlags are available for opening options. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - CloseCurrentPopup(): use inside the BeginPopup()/EndPopup() scope to close manually. + // - CloseCurrentPopup() is called by default by Selectable()/MenuItem() when activated (FIXME: need some options). + // - Use ImGuiPopupFlags_NoOpenOverExistingPopup to avoid opening a popup if there's already one at the same level. This is equivalent to e.g. testing for !IsAnyPopupOpen() prior to OpenPopup(). + // - Use IsWindowAppearing() after BeginPopup() to tell if a window just opened. + // - IMPORTANT: Notice that for OpenPopupOnItemClick() we exceptionally default flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter + IMGUI_API void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags = 0); // call to mark popup as open (don't call every frame!). + IMGUI_API void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags = 0); // id overload to facilitate calling from nested stacks + IMGUI_API void OpenPopupOnItemClick(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // helper to open popup when clicked on last item. Default to ImGuiPopupFlags_MouseButtonRight == 1. (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors) + IMGUI_API void CloseCurrentPopup(); // manually close the popup we have begin-ed into. + + // Popups: open+begin combined functions helpers + // - Helpers to do OpenPopup+BeginPopup where the Open action is triggered by e.g. hovering an item and right-clicking. + // - They are convenient to easily create context menus, hence the name. + // - IMPORTANT: Notice that BeginPopupContextXXX takes ImGuiPopupFlags just like OpenPopup() and unlike BeginPopup(). For full consistency, we may add ImGuiWindowFlags to the BeginPopupContextXXX functions in the future. + // - IMPORTANT: Notice that we exceptionally default their flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter, so if you add other flags remember to re-add the ImGuiPopupFlags_MouseButtonRight. + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked on last item. Use str_id==NULL to associate the popup to previous item. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1);// open+begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked in void (where there are no windows). + + // Popups: query functions + // - IsPopupOpen(): return true if the popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId: return true if any popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel: return true if any popup is open. + IMGUI_API bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags = 0); // return true if the popup is open. + + // Tables + // - Full-featured replacement for old Columns API. + // - See Demo->Tables for demo code. See top of imgui_tables.cpp for general commentary. + // - See ImGuiTableFlags_ and ImGuiTableColumnFlags_ enums for a description of available flags. + // The typical call flow is: + // - 1. Call BeginTable(), early out if returning false. + // - 2. Optionally call TableSetupColumn() to submit column name/flags/defaults. + // - 3. Optionally call TableSetupScrollFreeze() to request scroll freezing of columns/rows. + // - 4. Optionally call TableHeadersRow() to submit a header row. Names are pulled from TableSetupColumn() data. + // - 5. Populate contents: + // - In most situations you can use TableNextRow() + TableSetColumnIndex(N) to start appending into a column. + // - If you are using tables as a sort of grid, where every column is holding the same type of contents, + // you may prefer using TableNextColumn() instead of TableNextRow() + TableSetColumnIndex(). + // TableNextColumn() will automatically wrap-around into the next row if needed. + // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! + // - Summary of possible call flow: + // - TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // - TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // - TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // - TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! + // - 5. Call EndTable() + IMGUI_API bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextColumn(); // append into the next column (or first column of next row if currently in last column). Return true when column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true when column is visible. + + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify label, resizing policy, default width/weight, id, various other flags etc. + // - Use TableHeadersRow() to create a header row and automatically submit a TableHeader() for each column. + // Headers are required to perform: reordering, sorting, and opening the context menu. + // The context menu can also be made available in columns body using ImGuiTableFlags_ContextMenuInBody. + // - You may manually submit headers using TableNextRow() + TableHeader() calls, but this is only useful in + // some advanced use cases (e.g. adding custom widgets in header row). + // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); + IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + IMGUI_API void TableHeadersRow(); // submit a row with headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableAngledHeadersRow(); // submit a row with angled headers for every column with the ImGuiTableColumnFlags_AngledHeader flag. MUST BE FIRST ROW. + + // Tables: Sorting & Miscellaneous functions + // - Sorting: call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. + // When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have + // changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, + // else you may wastefully sort your data every frame! + // - Functions args 'int column_n' treat the default value of -1 as the same as passing the current column index. + IMGUI_API ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable(). + IMGUI_API int TableGetColumnCount(); // return number of columns (value passed to BeginTable) + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API int TableGetRowIndex(); // return current row index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column. + IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1); // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column. + IMGUI_API void TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) + IMGUI_API int TableGetHoveredColumn(); // return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. Can also use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. + IMGUI_API void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1); // change the color of a cell, row, or column. See ImGuiTableBgTarget_ flags for details. + + // Legacy Columns API (prefer using Tables!) + // - You can also use SameLine(pos_x) to mimic simplified columns. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool borders = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // Tab Bars, Tabs + // - Note: Tabs are automatically created by the docking system (when in 'docking' branch). Use this to create tab bars/tabs yourself. + IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar + IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. + IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. + IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + + // Logging/Capture + // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) + IMGUI_API void LogToFile(int auto_open_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int auto_open_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + IMGUI_API void LogTextV(const char* fmt, va_list args) IM_FMTLIST(1); + + // Drag and Drop + // - On source items, call BeginDragDropSource(), if it returns true also call SetDragDropPayload() + EndDragDropSource(). + // - On target candidates, call BeginDragDropTarget(), if it returns true also call AcceptDragDropPayload() + EndDragDropTarget(). + // - If you stop calling BeginDragDropSource() the payload is preserved however it won't have a preview tooltip (we currently display a fallback "..." tooltip, see #1725) + // - An item can be both drag source and drop target. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call after submitting an item which may be dragged. when this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. Return true when payload has been accepted. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. returns NULL when drag and drop is finished or inactive. use ImGuiPayload::IsDataType() to test for the payload type. + + // Disabling [BETA API] + // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) + // - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) + // - Tooltips windows by exception are opted out of disabling. + // - BeginDisabled(false)/EndDisabled() essentially does nothing but is provided to facilitate use of boolean expressions (as a micro-optimization: if you have tens of thousands of BeginDisabled(false)/EndDisabled() pairs, you might want to reformulate your code to avoid making those calls) + IMGUI_API void BeginDisabled(bool disabled = true); + IMGUI_API void EndDisabled(); + + // Clipping + // - Mouse hovering is affected by ImGui::PushClipRect() calls, unlike direct calls to ImDrawList::PushClipRect() which are render only. + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a newly appearing window. + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Keyboard/Gamepad Navigation + IMGUI_API void SetNavCursorVisible(bool visible); // alter visibility of keyboard/gamepad cursor. by default: show when using an arrow key, hide when clicking with mouse. + + // Overlapping mode + IMGUI_API void SetNextItemAllowOverlap(); // allow next item to be overlapped by a subsequent item. Useful with invisible buttons, selectable, treenode covering an area where subsequent items may need to be added. Note that both Selectable() and TreeNode() have dedicated flags doing this. + + // Item/Widgets Utilities and Query Functions + // - Most of the functions are referring to the previous Item that has been submitted. + // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited. This will continuously return true while holding mouse button on an item. Items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(ImGuiMouseButton mouse_button = 0); // is the last item hovered and mouse clicked on? (**) == IsMouseClicked(mouse_button) && IsItemHovered()Important. (**) this is NOT equivalent to the behavior of e.g. Button(). Read comments in function definition. + IMGUI_API bool IsItemVisible(); // is the last item visible? (items may be out of sight because of clipping/scrolling) + IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. + IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). + IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing. + IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode(). + IMGUI_API bool IsAnyItemHovered(); // is any item hovered? + IMGUI_API bool IsAnyItemActive(); // is any item active? + IMGUI_API bool IsAnyItemFocused(); // is any item focused? + IMGUI_API ImGuiID GetItemID(); // get ID of last item (~~ often same ImGui::GetID(label) beforehand) + IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item + + // Viewports + // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. + // - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. + // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. + IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. + + // Background/Foreground Draw Lists + IMGUI_API ImDrawList* GetBackgroundDrawList(); // this draw list will be the first rendered one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(); // this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + + // Miscellaneous Utilities + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API double GetTime(); // get global imgui time. incremented by io.DeltaTime every frame. + IMGUI_API int GetFrameCount(); // get global imgui frame count. incremented by 1 every frame. + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); // you may use this when creating your own ImDrawList instances. + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). + IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + + // Text Utilities + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + + // Color Utilities + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs Utilities: Keyboard/Mouse/Gamepad + // - the ImGuiKey enum contains all possible keyboard, mouse and gamepad inputs (e.g. ImGuiKey_A, ImGuiKey_MouseLeft, ImGuiKey_GamepadDpadUp...). + // - (legacy: before v1.87, we used ImGuiKey to carry native/user indices as defined by each backends. This was obsoleted in 1.87 (2022-02) and completely removed in 1.91.5 (2024-11). See https://github.com/ocornut/imgui/issues/4921) + // - (legacy: any use of ImGuiKey will assert when key < 512 to detect passing legacy native/user indices) + IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. + IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord); // was key chord (mods + key) pressed, e.g. you can pass 'ImGuiMod_Ctrl | ImGuiKey_S' as a key-chord. This doesn't do any routing or focus check, please consider using Shortcut() function instead. + IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names are provided for debugging purpose and are not meant to be saved persistently nor compared. + IMGUI_API void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); // Override io.WantCaptureKeyboard flag next frame (said flag is left for your application to handle, typically when true it instructs your app to ignore inputs). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard"; after the next NewFrame() call. + + // Inputs Utilities: Shortcut Testing & Routing [BETA] + // - ImGuiKeyChord = a ImGuiKey + optional ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super. + // ImGuiKey_C // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments) + // ImGuiMod_Ctrl | ImGuiKey_C // Accepted by functions taking ImGuiKeyChord arguments) + // only ImGuiMod_XXX values are legal to combine with an ImGuiKey. You CANNOT combine two ImGuiKey values. + // - The general idea is that several callers may register interest in a shortcut, and only one owner gets it. + // Parent -> call Shortcut(Ctrl+S) // When Parent is focused, Parent gets the shortcut. + // Child1 -> call Shortcut(Ctrl+S) // When Child1 is focused, Child1 gets the shortcut (Child1 overrides Parent shortcuts) + // Child2 -> no call // When Child2 is focused, Parent gets the shortcut. + // The whole system is order independent, so if Child1 makes its calls before Parent, results will be identical. + // This is an important property as it facilitate working with foreign code or larger codebase. + // - To understand the difference: + // - IsKeyChordPressed() compares mods and call IsKeyPressed() -> function has no side-effect. + // - Shortcut() submits a route, routes are resolved, if it currently can be routed it calls IsKeyChordPressed() -> function has (desirable) side-effects as it can prevents another call from getting the route. + // - Visualize registered routes in 'Metrics/Debugger->Inputs'. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + IMGUI_API void SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + + // Inputs Utilities: Key/Input Ownership [BETA] + // - One common use case would be to allow your items to disable standard inputs behaviors such + // as Tab or Alt key handling, Mouse Wheel scrolling, etc. + // e.g. Button(...); SetItemKeyOwner(ImGuiKey_MouseWheelY); to make hovering/activating a button disable wheel for scrolling. + // - Reminder ImGuiKey enum include access to mouse buttons and gamepad, so key ownership can apply to them. + // - Many related features are still in imgui_internal.h. For instance, most IsKeyXXX()/IsMouseXXX() functions have an owner-id-aware version. + IMGUI_API void SetItemKeyOwner(ImGuiKey key); // Set key owner to last item ID if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + + // Inputs Utilities: Mouse + // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. + // - You can also use regular integer: it is forever guaranteed that 0=Left, 1=Right, 2=Middle. + // - Dragging operations are only reported after mouse has moved a certain distance away from the initial clicking position (see 'lock_threshold' and 'io.MouseDraggingThreshold') + IMGUI_API bool IsMouseDown(ImGuiMouseButton button); // is mouse button held? + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) + IMGUI_API bool IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay); // delayed mouse release (use very sparingly!). Generally used with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount==1' test. This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. + IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available + IMGUI_API bool IsAnyMouseDown(); // [WILL OBSOLETE] is any mouse button held? This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve mouse position at the time of opening popup we have BeginPopup() into (helper to avoid user backing that value themselves) + IMGUI_API bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold = -1.0f); // is mouse dragging? (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API ImVec2 GetMouseDragDelta(ImGuiMouseButton button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API void ResetMouseDragDelta(ImGuiMouseButton button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired mouse cursor shape. Important: reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor cursor_type); // set desired mouse cursor shape + IMGUI_API void SetNextFrameWantCaptureMouse(bool want_capture_mouse); // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instucts your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call. + + // Clipboard Utilities + // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard. + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Settings/.Ini Utilities + // - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini"). + // - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually. + // - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables). + IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename). + IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source. + IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext). + IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. + + // Debug Utilities + // - Your main debugging friend is the ShowMetricsWindow() function, which is also accessible from Demo->Tools->Metrics Debugger + IMGUI_API void DebugTextEncoding(const char* text); + IMGUI_API void DebugFlashStyleColor(ImGuiCol idx); + IMGUI_API void DebugStartItemPicker(); + IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + IMGUI_API void DebugLog(const char* fmt, ...) IM_FMTARGS(1); // Call via IMGUI_DEBUG_LOG() for maximum stripping in caller code! + IMGUI_API void DebugLogV(const char* fmt, va_list args) IM_FMTLIST(1); +#endif + + // Memory Allocators + // - Those functions are not reliant on the current context. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. + IMGUI_API void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data = NULL); + IMGUI_API void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + +} // namespace ImGui + +//----------------------------------------------------------------------------- +// [SECTION] Flags & Enumerations +//----------------------------------------------------------------------------- + +// Flags for ImGui::Begin() +// (Those are per-window flags. There are shared flags in ImGuiIO: io.ConfigWindowsResizeFromEdges and io.ConfigWindowsMoveFromTitleBarOnly) +enum ImGuiWindowFlags_ +{ + ImGuiWindowFlags_None = 0, + ImGuiWindowFlags_NoTitleBar = 1 << 0, // Disable title-bar + ImGuiWindowFlags_NoResize = 1 << 1, // Disable user resizing with the lower-right grip + ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window + ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programmatically) + ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it. Also referred to as Window Menu Button (e.g. within a docking node). + ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame + ImGuiWindowFlags_NoBackground = 1 << 7, // Disable drawing background color (WindowBg, etc.) and outside border. Similar as using SetNextWindowBgAlpha(0.0f). + ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file + ImGuiWindowFlags_NoMouseInputs = 1 << 9, // Disable catching mouse, hovering test with pass through. + ImGuiWindowFlags_MenuBar = 1 << 10, // Has a menu-bar + ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, // Allow horizontal scrollbar to appear (off by default). You may use SetNextWindowContentSize(ImVec2(width,0.0f)); prior to calling Begin() to specify width. Read code in imgui_demo in the "Horizontal Scrolling" section. + ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, // Disable taking focus when transitioning from hidden to visible state + ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) + ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) + ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) + ImGuiWindowFlags_NoNavInputs = 1 << 16, // No keyboard/gamepad navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 17, // No focusing toward this window with keyboard/gamepad navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 18, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, + ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, + + // [Internal] + ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() + ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() + ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() + ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() + ImGuiWindowFlags_ChildMenu = 1 << 28, // Don't use! For internal use by BeginMenu() + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiWindowFlags_NavFlattened = 1 << 29, // Obsoleted in 1.90.9: Use ImGuiChildFlags_NavFlattened in BeginChild() call. + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 30, // Obsoleted in 1.90.0: Use ImGuiChildFlags_AlwaysUseWindowPadding in BeginChild() call. +#endif +}; + +// Flags for ImGui::BeginChild() +// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'. +// About using AutoResizeX/AutoResizeY flags: +// - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints"). +// - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing. +// - This allows BeginChild() to return false when not within boundaries (e.g. when scrolling), which is more optimal. BUT it won't update its auto-size while clipped. +// While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional "resizing after becoming visible again" glitch. +// - You may also use ImGuiChildFlags_AlwaysAutoResize to force an update even when child window is not in view. +// HOWEVER PLEASE UNDERSTAND THAT DOING SO WILL PREVENT BeginChild() FROM EVER RETURNING FALSE, disabling benefits of coarse clipping. +enum ImGuiChildFlags_ +{ + ImGuiChildFlags_None = 0, + ImGuiChildFlags_Borders = 1 << 0, // Show an outer border and enable WindowPadding. (IMPORTANT: this is always == 1 == true for legacy reason) + ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, // Pad with style.WindowPadding even if no border are drawn (no padding by default for non-bordered child windows because it makes more sense) + ImGuiChildFlags_ResizeX = 1 << 2, // Allow resize from right border (layout direction). Enable .ini saving (unless ImGuiWindowFlags_NoSavedSettings passed to window flags) + ImGuiChildFlags_ResizeY = 1 << 3, // Allow resize from bottom border (layout direction). " + ImGuiChildFlags_AutoResizeX = 1 << 4, // Enable auto-resizing width. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AutoResizeY = 1 << 5, // Enable auto-resizing height. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AlwaysAutoResize = 1 << 6, // Combined with AutoResizeX/AutoResizeY. Always measure size even when child is hidden, always return true, always disable clipping optimization! NOT RECOMMENDED. + ImGuiChildFlags_FrameStyle = 1 << 7, // Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding. + ImGuiChildFlags_NavFlattened = 1 << 8, // [BETA] Share focus scope, allow keyboard/gamepad navigation to cross over parent border to this child or between sibling child windows. + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiChildFlags_Border = ImGuiChildFlags_Borders, // Renamed in 1.91.1 (August 2024) for consistency. +#endif +}; + +// Flags for ImGui::PushItemFlag() +// (Those are shared by all items) +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_None = 0, // (Default) + ImGuiItemFlags_NoTabStop = 1 << 0, // false // Disable keyboard tabbing. This is a "lighter" version of ImGuiItemFlags_NoNav. + ImGuiItemFlags_NoNav = 1 << 1, // false // Disable any form of focusing (keyboard/gamepad directional navigation and SetKeyboardFocusHere() calls). + ImGuiItemFlags_NoNavDefaultFocus = 1 << 2, // false // Disable item being a candidate for default focus (e.g. used by title bar items). + ImGuiItemFlags_ButtonRepeat = 1 << 3, // false // Any button-like behavior will have repeat mode enabled (based on io.KeyRepeatDelay and io.KeyRepeatRate values). Note that you can also call IsItemActive() after any button to tell if it is being held. + ImGuiItemFlags_AutoClosePopups = 1 << 4, // true // MenuItem()/Selectable() automatically close their parent popup window. + ImGuiItemFlags_AllowDuplicateId = 1 << 5, // false // Allow submitting an item with the same identifier as an item already submitted this frame without triggering a warning tooltip if io.ConfigDebugHighlightIdConflicts is set. +}; + +// Flags for ImGui::InputText() +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigInputTextCursorBlink and io.ConfigInputTextEnterKeepActive) +enum ImGuiInputTextFlags_ +{ + // Basic filters (also see ImGuiInputTextFlags_CallbackCharFilter) + ImGuiInputTextFlags_None = 0, + ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ + ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef + ImGuiInputTextFlags_CharsScientific = 1 << 2, // Allow 0123456789.+-*/eE (Scientific notation input) + ImGuiInputTextFlags_CharsUppercase = 1 << 3, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 4, // Filter out spaces, tabs + + // Inputs + ImGuiInputTextFlags_AllowTabInput = 1 << 5, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 6, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider using IsItemDeactivatedAfterEdit() instead! + ImGuiInputTextFlags_EscapeClearsAll = 1 << 7, // Escape key clears content if not empty, and deactivate otherwise (contrast to default behavior of Escape to revert) + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 8, // In multi-line mode, validate with Enter, add new line with Ctrl+Enter (default is opposite: validate with Ctrl+Enter, add line with Enter). + + // Other options + ImGuiInputTextFlags_ReadOnly = 1 << 9, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 10, // Password mode, display all characters as '*', disable copy + ImGuiInputTextFlags_AlwaysOverwrite = 1 << 11, // Overwrite mode + ImGuiInputTextFlags_AutoSelectAll = 1 << 12, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_ParseEmptyRefVal = 1 << 13, // InputFloat(), InputInt(), InputScalar() etc. only: parse empty string as zero value. + ImGuiInputTextFlags_DisplayEmptyRefVal = 1 << 14, // InputFloat(), InputInt(), InputScalar() etc. only: when value is zero, do not display it. Generally used with ImGuiInputTextFlags_ParseEmptyRefVal. + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 15, // Disable following the cursor horizontally + ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). + + // Elide display / Alignment + ImGuiInputTextFlags_ElideLeft = 1 << 17, // When text doesn't fit, elide left side to ensure right side stays visible. Useful for path/filenames. Single-line only! + + // Callback features + ImGuiInputTextFlags_CallbackCompletion = 1 << 18, // Callback on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 19, // Callback on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 20, // Callback on each iteration. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 21, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. + ImGuiInputTextFlags_CallbackResize = 1 << 22, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) + ImGuiInputTextFlags_CallbackEdit = 1 << 23, // Callback on any edit. Note that InputText() already returns true on edit + you can always use IsItemEdited(). The callback is useful to manipulate the underlying buffer while focus is active. + + // Obsolete names + //ImGuiInputTextFlags_AlwaysInsertMode = ImGuiInputTextFlags_AlwaysOverwrite // [renamed in 1.82] name was not matching behavior +}; + +// Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() +enum ImGuiTreeNodeFlags_ +{ + ImGuiTreeNodeFlags_None = 0, + ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected + ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack + ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) + ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Open on double-click instead of simple click (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Open when clicking on the arrow part (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. + ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow. IMPORTANT: node can still be marked open/close if you don't set the _Leaf flag! + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding() before the node. + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line without using AllowOverlap mode. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (cover the indent area). + ImGuiTreeNodeFlags_SpanLabelWidth = 1 << 13, // Narrow hit box + narrow hovering highlight, will only cover the label text. + ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, // Frame will span all columns of its container table (label will still fit in current column) + ImGuiTreeNodeFlags_LabelSpanAllColumns = 1 << 15, // Label will span all columns of its container table + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 16, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 17, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 + ImGuiTreeNodeFlags_SpanTextWidth = ImGuiTreeNodeFlags_SpanLabelWidth,// Renamed in 1.90.7 +#endif +}; + +// Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument instead of 'ImGuiPopupFlags flags', +// we need to treat small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. +// - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. +// IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter +// and want to use another flag, you need to pass in the ImGuiPopupFlags_MouseButtonRight flag explicitly. +// - Multiple buttons currently cannot be combined/or-ed in those functions (we could allow it later). +enum ImGuiPopupFlags_ +{ + ImGuiPopupFlags_None = 0, + ImGuiPopupFlags_MouseButtonLeft = 0, // For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left) + ImGuiPopupFlags_MouseButtonRight = 1, // For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right) + ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) + ImGuiPopupFlags_MouseButtonMask_ = 0x1F, + ImGuiPopupFlags_MouseButtonDefault_ = 1, + ImGuiPopupFlags_NoReopen = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't reopen same popup if already open (won't reposition, won't reinitialize navigation) + //ImGuiPopupFlags_NoReopenAlwaysNavInit = 1 << 6, // For OpenPopup*(), BeginPopupContext*(): focus and initialize navigation even when not reopening. + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 8, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 10, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 11, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, +}; + +// Flags for ImGui::Selectable() +enum ImGuiSelectableFlags_ +{ + ImGuiSelectableFlags_None = 0, + ImGuiSelectableFlags_NoAutoClosePopups = 1 << 0, // Clicking this doesn't close parent popup window (overrides ImGuiItemFlags_AutoClosePopups) + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Frame will span all columns of its container table (text will still fit in current column) + ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too + ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text + ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + ImGuiSelectableFlags_Highlight = 1 << 5, // Make the item be displayed as if it is hovered + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiSelectableFlags_DontClosePopups = ImGuiSelectableFlags_NoAutoClosePopups, // Renamed in 1.91.0 + ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap, // Renamed in 1.89.7 +#endif +}; + +// Flags for ImGui::BeginCombo() +enum ImGuiComboFlags_ +{ + ImGuiComboFlags_None = 0, + ImGuiComboFlags_PopupAlignLeft = 1 << 0, // Align the popup toward the left by default + ImGuiComboFlags_HeightSmall = 1 << 1, // Max ~4 items visible. Tip: If you want your combo popup to be a specific size you can use SetNextWindowSizeConstraints() prior to calling BeginCombo() + ImGuiComboFlags_HeightRegular = 1 << 2, // Max ~8 items visible (default) + ImGuiComboFlags_HeightLarge = 1 << 3, // Max ~20 items visible + ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible + ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button + ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button + ImGuiComboFlags_WidthFitPreview = 1 << 7, // Width dynamically calculated from preview contents + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, +}; + +// Flags for ImGui::BeginTabBar() +enum ImGuiTabBarFlags_ +{ + ImGuiTabBarFlags_None = 0, + ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list + ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear + ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) + ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab + ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab + ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 7, // Resize tabs when they don't fit + ImGuiTabBarFlags_FittingPolicyScroll = 1 << 8, // Add scroll buttons when tabs don't fit + ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll, + ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown, +}; + +// Flags for ImGui::BeginTabItem() +enum ImGuiTabItemFlags_ +{ + ImGuiTabItemFlags_None = 0, + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + set ImGuiTabItemFlags_NoAssumedClosure. + ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID()/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) + ImGuiTabItemFlags_Trailing = 1 << 7, // Enforce the tab position to the right of the tab bar (before the scrolling buttons) + ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, // Tab is selected when trying to close + closure is not immediately assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. +}; + +// Flags for ImGui::IsWindowFocused() +enum ImGuiFocusedFlags_ +{ + ImGuiFocusedFlags_None = 0, + ImGuiFocusedFlags_ChildWindows = 1 << 0, // Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! + ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, +}; + +// Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() +// Note: if you are trying to check whether your mouse should be dispatched to Dear ImGui or to your app, you should use 'io.WantCaptureMouse' instead! Please read the FAQ! +// Note: windows with the ImGuiWindowFlags_NoInputs flag are ignored by IsWindowHovered() calls. +enum ImGuiHoveredFlags_ +{ + ImGuiHoveredFlags_None = 0, // Return true if directly over the item/window, not obstructed by another window, not obstructed by an active popup or modal blocking inputs under them. + ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered + ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) + ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered + ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + //ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, // IsItemHovered() only: Return true even if the item uses AllowOverlap mode and is overlapped by another hoverable item. + ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window. + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_NoNavOverride = 1 << 11, // IsItemHovered() only: Disable using keyboard/gamepad navigation state when active, always query mouse + ImGuiHoveredFlags_AllowWhenOverlapped = ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenOverlappedByWindow, + ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, + + // Tooltips mode + // - typically used in IsItemHovered() + SetTooltip() sequence. + // - this is a shortcut to pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' where you can reconfigure desired behavior. + // e.g. 'TooltipHoveredFlagsForMouse' defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + // - for frequently actioned or hovered items providing a tooltip, you want may to use ImGuiHoveredFlags_ForTooltip (stationary + delay) so the tooltip doesn't show too often. + // - for items which main purpose is to be hovered, or items with low affordance, or in less consistent apps, prefer no delay or shorter delay. + ImGuiHoveredFlags_ForTooltip = 1 << 12, // Shortcut for standard flags when using IsItemHovered() + SetTooltip() sequence. + + // (Advanced) Mouse Hovering delays. + // - generally you can use ImGuiHoveredFlags_ForTooltip to use application-standardized flags. + // - use those if you need specific overrides. + ImGuiHoveredFlags_Stationary = 1 << 13, // Require mouse to be stationary for style.HoverStationaryDelay (~0.15 sec) _at least one time_. After this, can move on same item/window. Using the stationary test tends to reduces the need for a long delay. + ImGuiHoveredFlags_DelayNone = 1 << 14, // IsItemHovered() only: Return true immediately (default). As this is the default you generally ignore this. + ImGuiHoveredFlags_DelayShort = 1 << 15, // IsItemHovered() only: Return true after style.HoverDelayShort elapsed (~0.15 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_DelayNormal = 1 << 16, // IsItemHovered() only: Return true after style.HoverDelayNormal elapsed (~0.40 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_NoSharedDelay = 1 << 17, // IsItemHovered() only: Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) +}; + +// Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() +enum ImGuiDragDropFlags_ +{ + ImGuiDragDropFlags_None = 0, + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // Disable preview tooltip. By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disables this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disables this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + ImGuiDragDropFlags_PayloadAutoExpire = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + ImGuiDragDropFlags_PayloadNoCrossContext = 1 << 6, // Hint to specify that the payload may not be copied outside current dear imgui context. + ImGuiDragDropFlags_PayloadNoCrossProcess = 1 << 7, // Hint to specify that the payload may not be copied outside current process. + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect, // For peeking ahead and inspecting the payload before delivery. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiDragDropFlags_SourceAutoExpirePayload = ImGuiDragDropFlags_PayloadAutoExpire, // Renamed in 1.90.9 +#endif +}; + +// Standard Drag and Drop payload types. You can define you own payload types using short strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3]: Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4]: Standard type for colors. User code may use this type. + +// A primary data type +enum ImGuiDataType_ +{ + ImGuiDataType_S8, // signed char / char (with sensible compilers) + ImGuiDataType_U8, // unsigned char + ImGuiDataType_S16, // short + ImGuiDataType_U16, // unsigned short + ImGuiDataType_S32, // int + ImGuiDataType_U32, // unsigned int + ImGuiDataType_S64, // long long / __int64 + ImGuiDataType_U64, // unsigned long long / unsigned __int64 + ImGuiDataType_Float, // float + ImGuiDataType_Double, // double + ImGuiDataType_Bool, // bool (provided for user convenience, not supported by scalar widgets) + ImGuiDataType_String, // char* (provided for user convenience, not supported by scalar widgets) + ImGuiDataType_COUNT +}; + +// A cardinal direction +enum ImGuiDir : int +{ + ImGuiDir_None = -1, + ImGuiDir_Left = 0, + ImGuiDir_Right = 1, + ImGuiDir_Up = 2, + ImGuiDir_Down = 3, + ImGuiDir_COUNT +}; + +// A sorting direction +enum ImGuiSortDirection : ImU8 +{ + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. +}; + +// A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value): can represent Keyboard, Mouse and Gamepad values. +// All our named keys are >= 512. Keys value 0 to 511 are left unused and were legacy native/opaque key values (< 1.87). +// Support for legacy keys was completely removed in 1.91.5. +// Read details about the 1.87+ transition : https://github.com/ocornut/imgui/issues/4921 +// Note that "Keys" related to physical keys and are not the same concept as input "Characters", the later are submitted via io.AddInputCharacter(). +// The keyboard key enum values are named after the keys on a standard US keyboard, and on other keyboard types the keys reported may not match the keycaps. +enum ImGuiKey : int +{ + // Keyboard + ImGuiKey_None = 0, + ImGuiKey_NamedKey_BEGIN = 512, // First valid key value (other than 0) + + ImGuiKey_Tab = 512, // == ImGuiKey_NamedKey_BEGIN + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper, + ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper, + ImGuiKey_Menu, + ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, + ImGuiKey_A, ImGuiKey_B, ImGuiKey_C, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_I, ImGuiKey_J, + ImGuiKey_K, ImGuiKey_L, ImGuiKey_M, ImGuiKey_N, ImGuiKey_O, ImGuiKey_P, ImGuiKey_Q, ImGuiKey_R, ImGuiKey_S, ImGuiKey_T, + ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, + ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, + ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_F13, ImGuiKey_F14, ImGuiKey_F15, ImGuiKey_F16, ImGuiKey_F17, ImGuiKey_F18, + ImGuiKey_F19, ImGuiKey_F20, ImGuiKey_F21, ImGuiKey_F22, ImGuiKey_F23, ImGuiKey_F24, + ImGuiKey_Apostrophe, // ' + ImGuiKey_Comma, // , + ImGuiKey_Minus, // - + ImGuiKey_Period, // . + ImGuiKey_Slash, // / + ImGuiKey_Semicolon, // ; + ImGuiKey_Equal, // = + ImGuiKey_LeftBracket, // [ + ImGuiKey_Backslash, // \ (this text inhibit multiline comment caused by backslash) + ImGuiKey_RightBracket, // ] + ImGuiKey_GraveAccent, // ` + ImGuiKey_CapsLock, + ImGuiKey_ScrollLock, + ImGuiKey_NumLock, + ImGuiKey_PrintScreen, + ImGuiKey_Pause, + ImGuiKey_Keypad0, ImGuiKey_Keypad1, ImGuiKey_Keypad2, ImGuiKey_Keypad3, ImGuiKey_Keypad4, + ImGuiKey_Keypad5, ImGuiKey_Keypad6, ImGuiKey_Keypad7, ImGuiKey_Keypad8, ImGuiKey_Keypad9, + ImGuiKey_KeypadDecimal, + ImGuiKey_KeypadDivide, + ImGuiKey_KeypadMultiply, + ImGuiKey_KeypadSubtract, + ImGuiKey_KeypadAdd, + ImGuiKey_KeypadEnter, + ImGuiKey_KeypadEqual, + ImGuiKey_AppBack, // Available on some keyboard/mouses. Often referred as "Browser Back" + ImGuiKey_AppForward, + ImGuiKey_Oem102, // Non-US backslash. + + // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION ACTION + // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets) + ImGuiKey_GamepadStart, // Menu (Xbox) + (Switch) Start/Options (PS) + ImGuiKey_GamepadBack, // View (Xbox) - (Switch) Share (PS) + ImGuiKey_GamepadFaceLeft, // X (Xbox) Y (Switch) Square (PS) // Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows) + ImGuiKey_GamepadFaceRight, // B (Xbox) A (Switch) Circle (PS) // Cancel / Close / Exit + ImGuiKey_GamepadFaceUp, // Y (Xbox) X (Switch) Triangle (PS) // Text Input / On-screen Keyboard + ImGuiKey_GamepadFaceDown, // A (Xbox) B (Switch) Cross (PS) // Activate / Open / Toggle / Tweak + ImGuiKey_GamepadDpadLeft, // D-pad Left // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadRight, // D-pad Right // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadUp, // D-pad Up // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadDown, // D-pad Down // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadL1, // L Bumper (Xbox) L (Switch) L1 (PS) // Tweak Slower / Focus Previous (in Windowing mode) + ImGuiKey_GamepadR1, // R Bumper (Xbox) R (Switch) R1 (PS) // Tweak Faster / Focus Next (in Windowing mode) + ImGuiKey_GamepadL2, // L Trig. (Xbox) ZL (Switch) L2 (PS) [Analog] + ImGuiKey_GamepadR2, // R Trig. (Xbox) ZR (Switch) R2 (PS) [Analog] + ImGuiKey_GamepadL3, // L Stick (Xbox) L3 (Switch) L3 (PS) + ImGuiKey_GamepadR3, // R Stick (Xbox) R3 (Switch) R3 (PS) + ImGuiKey_GamepadLStickLeft, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickRight, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickUp, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickDown, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadRStickLeft, // [Analog] + ImGuiKey_GamepadRStickRight, // [Analog] + ImGuiKey_GamepadRStickUp, // [Analog] + ImGuiKey_GamepadRStickDown, // [Analog] + + // Aliases: Mouse Buttons (auto-submitted from AddMouseButtonEvent() calls) + // - This is mirroring the data also written to io.MouseDown[], io.MouseWheel, in a format allowing them to be accessed via standard key API. + ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle, ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY, + + // [Internal] Reserved for mod storage + ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt, ImGuiKey_ReservedForModSuper, + ImGuiKey_NamedKey_END, + + // Keyboard Modifiers (explicitly submitted by backend via AddKeyEvent() calls) + // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing + // them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc. + // - Code polling every key (e.g. an interface to detect a key press for input mapping) might want to ignore those + // and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiMod_Ctrl). + // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys. + // In practice: it's complicated; mods are often provided from different sources. Keyboard layout, IME, sticky keys and + // backends tend to interfere and break that equivalence. The safer decision is to relay that ambiguity down to the end-user... + // - On macOS, we swap Cmd(Super) and Ctrl keys at the time of the io.AddKeyEvent() call. + ImGuiMod_None = 0, + ImGuiMod_Ctrl = 1 << 12, // Ctrl (non-macOS), Cmd (macOS) + ImGuiMod_Shift = 1 << 13, // Shift + ImGuiMod_Alt = 1 << 14, // Option/Menu + ImGuiMod_Super = 1 << 15, // Windows/Super (non-macOS), Ctrl (macOS) + ImGuiMod_Mask_ = 0xF000, // 4-bits + + // [Internal] If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END. + ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, + //ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, // Size of KeysData[]: only hold named keys + //ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, // Accesses to io.KeysData[] must use (key - ImGuiKey_NamedKey_BEGIN) index. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiKey_COUNT = ImGuiKey_NamedKey_END, // Obsoleted in 1.91.5 because it was extremely misleading (since named keys don't start at 0 anymore) + ImGuiMod_Shortcut = ImGuiMod_Ctrl, // Removed in 1.90.7, you can now simply use ImGuiMod_Ctrl + ImGuiKey_ModCtrl = ImGuiMod_Ctrl, ImGuiKey_ModShift = ImGuiMod_Shift, ImGuiKey_ModAlt = ImGuiMod_Alt, ImGuiKey_ModSuper = ImGuiMod_Super, // Renamed in 1.89 + //ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 +#endif +}; + +// Flags for Shortcut(), SetNextItemShortcut(), +// (and for upcoming extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() that are still in imgui_internal.h) +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlags_ +{ + ImGuiInputFlags_None = 0, + ImGuiInputFlags_Repeat = 1 << 0, // Enable repeat. Return true on successive repeats. Default for legacy IsKeyPressed(). NOT Default for legacy IsMouseClicked(). MUST BE == 1. + + // Flags for Shortcut(), SetNextItemShortcut() + // - Routing policies: RouteGlobal+OverActive >> RouteActive or RouteFocused (if owner is active item) >> RouteGlobal+OverFocused >> RouteFocused (if in focused window stack) >> RouteGlobal. + // - Default policy is RouteFocused. Can select only 1 policy among all available. + ImGuiInputFlags_RouteActive = 1 << 10, // Route to active item only. + ImGuiInputFlags_RouteFocused = 1 << 11, // Route to windows in the focus stack (DEFAULT). Deep-most focused window takes inputs. Active item takes inputs over deep-most focused window. + ImGuiInputFlags_RouteGlobal = 1 << 12, // Global route (unless a focused window or active item registered the route). + ImGuiInputFlags_RouteAlways = 1 << 13, // Do not register route, poll keys directly. + // - Routing options + ImGuiInputFlags_RouteOverFocused = 1 << 14, // Option: global route: higher priority than focused route (unless active item in focused route). + ImGuiInputFlags_RouteOverActive = 1 << 15, // Option: global route: higher priority than active item. Unlikely you need to use that: will interfere with every active items, e.g. CTRL+A registered by InputText will be overridden by this. May not be fully honored as user/internal code is likely to always assume they can access keys when active. + ImGuiInputFlags_RouteUnlessBgFocused = 1 << 16, // Option: global route: will not be applied if underlying background/void is focused (== no Dear ImGui windows are focused). Useful for overlay applications. + ImGuiInputFlags_RouteFromRootWindow = 1 << 17, // Option: route evaluated from the point of view of root window rather than current window. + + // Flags for SetNextItemShortcut() + ImGuiInputFlags_Tooltip = 1 << 18, // Automatically display a tooltip when hovering item [BETA] Unsure of right api (opt-in/opt-out) +}; + +// Configuration flags stored in io.ConfigFlags. Set by user/application. +enum ImGuiConfigFlags_ +{ + ImGuiConfigFlags_None = 0, + ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. Enable full Tabbing + directional arrows + space/enter to activate. + ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. Backend also needs to set ImGuiBackendFlags_HasGamepad. + ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct dear imgui to disable mouse inputs and interactions. + ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + ImGuiConfigFlags_NoKeyboard = 1 << 6, // Instruct dear imgui to disable keyboard inputs and interactions. This is done by ignoring keyboard events and clearing existing states. + + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are NOT used by core Dear ImGui) + ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. + ImGuiConfigFlags_IsTouchScreen = 1 << 21, // Application is using a touch screen instead of a mouse. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // [moved/renamed in 1.91.4] -> use bool io.ConfigNavMoveSetMousePos + ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // [moved/renamed in 1.91.4] -> use bool io.ConfigNavCaptureKeyboard +#endif +}; + +// Backend capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom backend. +enum ImGuiBackendFlags_ +{ + ImGuiBackendFlags_None = 0, + ImGuiBackendFlags_HasGamepad = 1 << 0, // Backend Platform supports gamepad and currently has one connected. + ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. + ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if io.ConfigNavMoveSetMousePos is set). + ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, // Title bar + ImGuiCol_TitleBgActive, // Title bar when focused + ImGuiCol_TitleBgCollapsed, // Title bar when collapsed + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, // Header* colors are used for CollapsingHeader, TreeNode, Selectable, MenuItem + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, // Resize grip in lower-right and lower-left corners of windows. + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_TabHovered, // Tab background, when hovered + ImGuiCol_Tab, // Tab background, when tab-bar is focused & tab is unselected + ImGuiCol_TabSelected, // Tab background, when tab-bar is focused & tab is selected + ImGuiCol_TabSelectedOverline, // Tab horizontal overline, when tab-bar is focused & tab is selected + ImGuiCol_TabDimmed, // Tab background, when tab-bar is unfocused & tab is unselected + ImGuiCol_TabDimmedSelected, // Tab background, when tab-bar is unfocused & tab is selected + ImGuiCol_TabDimmedSelectedOverline,//..horizontal overline, when tab-bar is unfocused & tab is selected + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableBorderStrong, // Table outer and header borders (prefer using Alpha=1.0 here) + ImGuiCol_TableBorderLight, // Table inner borders (prefer using Alpha=1.0 here) + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) + ImGuiCol_TextLink, // Hyperlink color + ImGuiCol_TextSelectedBg, + ImGuiCol_DragDropTarget, // Rectangle highlighting a drop target + ImGuiCol_NavCursor, // Color of keyboard/gamepad navigation cursor/rectangle, when visible + ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB + ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the CTRL+TAB window list, when active + ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active + ImGuiCol_COUNT, + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiCol_TabActive = ImGuiCol_TabSelected, // [renamed in 1.90.9] + ImGuiCol_TabUnfocused = ImGuiCol_TabDimmed, // [renamed in 1.90.9] + ImGuiCol_TabUnfocusedActive = ImGuiCol_TabDimmedSelected, // [renamed in 1.90.9] + ImGuiCol_NavHighlight = ImGuiCol_NavCursor, // [renamed in 1.91.4] +#endif +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. +// During initialization or between frames, feel free to just poke into ImGuiStyle directly. +// - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +// - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ +{ + // Enum name -------------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_ImageBorderSize, // float ImageBorderSize + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_TabBorderSize, // float TabBorderSize + ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize + ImGuiStyleVar_TabBarOverlineSize, // float TabBarOverlineSize + ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle + ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize + ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign + ImGuiStyleVar_SeparatorTextPadding, // ImVec2 SeparatorTextPadding + ImGuiStyleVar_COUNT +}; + +// Flags for InvisibleButton() [extended in imgui_internal.h] +enum ImGuiButtonFlags_ +{ + ImGuiButtonFlags_None = 0, + ImGuiButtonFlags_MouseButtonLeft = 1 << 0, // React on left mouse button (default) + ImGuiButtonFlags_MouseButtonRight = 1 << 1, // React on right mouse button + ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, // React on center mouse button + ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle, // [Internal] + ImGuiButtonFlags_EnableNav = 1 << 3, // InvisibleButton(): do not disable navigation/tabbing. Otherwise disabled by default. +}; + +// Flags for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() +enum ImGuiColorEditFlags_ +{ + ImGuiColorEditFlags_None = 0, + ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (will only read 3 components from the input pointer). + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on color square. + ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable color square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview color square). + ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. + ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small color square preview instead. + ImGuiColorEditFlags_NoDragDrop = 1 << 9, // // ColorEdit: disable drag and drop target. ColorButton: disable drag and drop source. + ImGuiColorEditFlags_NoBorder = 1 << 10, // // ColorButton: disable border (which is enforced by default) + + // Alpha preview + // - Prior to 1.91.8 (2025/01/21): alpha was made opaque in the preview by default using old name ImGuiColorEditFlags_AlphaPreview. + // - We now display the preview as transparent by default. You can use ImGuiColorEditFlags_AlphaOpaque to use old behavior. + // - The new flags may be combined better and allow finer controls. + ImGuiColorEditFlags_AlphaOpaque = 1 << 11, // // ColorEdit, ColorPicker, ColorButton: disable alpha in the preview,. Contrary to _NoAlpha it may still be edited when calling ColorEdit4()/ColorPicker4(). For ColorButton() this does the same as _NoAlpha. + ImGuiColorEditFlags_AlphaNoBg = 1 << 12, // // ColorEdit, ColorPicker, ColorButton: disable rendering a checkerboard background behind transparent color. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 13, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half transparent preview. + + // User Options (right-click on widget to change some of them). + ImGuiColorEditFlags_AlphaBar = 1 << 16, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. + ImGuiColorEditFlags_HDR = 1 << 19, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). + ImGuiColorEditFlags_DisplayRGB = 1 << 20, // [Display] // ColorEdit: override _display_ type among RGB/HSV/Hex. ColorPicker: select any combination using one or more of RGB/HSV/Hex. + ImGuiColorEditFlags_DisplayHSV = 1 << 21, // [Display] // " + ImGuiColorEditFlags_DisplayHex = 1 << 22, // [Display] // " + ImGuiColorEditFlags_Uint8 = 1 << 23, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0..255. + ImGuiColorEditFlags_Float = 1 << 24, // [DataType] // ColorEdit, ColorPicker, ColorButton: _display_ values formatted as 0.0f..1.0f floats instead of 0..255 integers. No round-trip of value via integers. + ImGuiColorEditFlags_PickerHueBar = 1 << 25, // [Picker] // ColorPicker: bar for Hue, rectangle for Sat/Value. + ImGuiColorEditFlags_PickerHueWheel = 1 << 26, // [Picker] // ColorPicker: wheel for Hue, triangle for Sat/Value. + ImGuiColorEditFlags_InputRGB = 1 << 27, // [Input] // ColorEdit, ColorPicker: input and output data in RGB format. + ImGuiColorEditFlags_InputHSV = 1 << 28, // [Input] // ColorEdit, ColorPicker: input and output data in HSV format. + + // Defaults Options. You can set application defaults using SetColorEditOptions(). The intent is that you probably don't want to + // override them in most of your calls. Let the user choose via the option menu and/or call SetColorEditOptions() once during startup. + ImGuiColorEditFlags_DefaultOptions_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, + + // [Internal] Masks + ImGuiColorEditFlags_AlphaMask_ = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque | ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf, + ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_DisplayHex, + ImGuiColorEditFlags_DataTypeMask_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, + ImGuiColorEditFlags_PickerMask_ = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_InputMask_ = ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiColorEditFlags_AlphaPreview = 0, // [Removed in 1.91.8] This is the default now. Will display a checkerboard unless ImGuiColorEditFlags_AlphaNoBg is set. +#endif + //ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] +}; + +// Flags for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +// We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. +// (Those are per-item flags. There is shared behavior flag too: ImGuiIO: io.ConfigDragClickToInputText) +enum ImGuiSliderFlags_ +{ + ImGuiSliderFlags_None = 0, + ImGuiSliderFlags_Logarithmic = 1 << 5, // Make the widget logarithmic (linear otherwise). Consider using ImGuiSliderFlags_NoRoundToFormat with this if using a format-string with small amount of digits. + ImGuiSliderFlags_NoRoundToFormat = 1 << 6, // Disable rounding underlying value to match precision of the display format string (e.g. %.3f values are rounded to those 3 digits). + ImGuiSliderFlags_NoInput = 1 << 7, // Disable CTRL+Click or Enter key allowing to input text directly into the widget. + ImGuiSliderFlags_WrapAround = 1 << 8, // Enable wrapping around from max to min and from min to max. Only supported by DragXXX() functions for now. + ImGuiSliderFlags_ClampOnInput = 1 << 9, // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. + ImGuiSliderFlags_ClampZeroRange = 1 << 10, // Clamp even if min==max==0.0f. Otherwise due to legacy reason DragXXX functions don't clamp with those values. When your clamping limits are dynamic you almost always want to use it. + ImGuiSliderFlags_NoSpeedTweaks = 1 << 11, // Disable keyboard modifiers altering tweak speed. Useful if you want to alter tweak speed yourself based on your own logic. + ImGuiSliderFlags_AlwaysClamp = ImGuiSliderFlags_ClampOnInput | ImGuiSliderFlags_ClampZeroRange, + ImGuiSliderFlags_InvalidMask_ = 0x7000000F, // [Internal] We treat using those bits as being potentially a 'float power' argument from the previous API that has got miscast to this enum, and will trigger an assert if needed. +}; + +// Identify a mouse button. +// Those values are guaranteed to be stable and we frequently use 0/1 directly. Named enums provided for convenience. +enum ImGuiMouseButton_ +{ + ImGuiMouseButton_Left = 0, + ImGuiMouseButton_Right = 1, + ImGuiMouseButton_Middle = 2, + ImGuiMouseButton_COUNT = 5 +}; + +// Enumeration for GetMouseCursor() +// User code may request backend to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here +enum ImGuiMouseCursor_ +{ + ImGuiMouseCursor_None = -1, + ImGuiMouseCursor_Arrow = 0, + ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. + ImGuiMouseCursor_ResizeAll, // (Unused by Dear ImGui functions) + ImGuiMouseCursor_ResizeNS, // When hovering over a horizontal border + ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column + ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window + ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window + ImGuiMouseCursor_Hand, // (Unused by Dear ImGui functions. Use for e.g. hyperlinks) + ImGuiMouseCursor_Wait, // When waiting for something to process/load. + ImGuiMouseCursor_Progress, // When waiting for something to process/load, but application is still interactive. + ImGuiMouseCursor_NotAllowed, // When hovering something with disallowed interaction. Usually a crossed circle. + ImGuiMouseCursor_COUNT +}; + +// Enumeration for AddMouseSourceEvent() actual source of Mouse Input data. +// Historically we use "Mouse" terminology everywhere to indicate pointer data, e.g. MousePos, IsMousePressed(), io.AddMousePosEvent() +// But that "Mouse" data can come from different source which occasionally may be useful for application to know about. +// You can submit a change of pointer type using io.AddMouseSourceEvent(). +enum ImGuiMouseSource : int +{ + ImGuiMouseSource_Mouse = 0, // Input is coming from an actual mouse. + ImGuiMouseSource_TouchScreen, // Input is coming from a touch screen (no hovering prior to initial press, less precise initial press aiming, dual-axis wheeling possible). + ImGuiMouseSource_Pen, // Input is coming from a pressure/magnetic pen (often used in conjunction with high-sampling rates). + ImGuiMouseSource_COUNT +}; + +// Enumeration for ImGui::SetNextWindow***(), SetWindow***(), SetNextItem***() functions +// Represent a condition. +// Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. +enum ImGuiCond_ +{ + ImGuiCond_None = 0, // No condition (always set the variable), same as _Always + ImGuiCond_Always = 1 << 0, // No condition (always set the variable), same as _None + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call will succeed) + ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the object/window has no persistently saved data (no entry in .ini file) + ImGuiCond_Appearing = 1 << 3, // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) +}; + +//----------------------------------------------------------------------------- +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +//----------------------------------------------------------------------------- + +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + // Miscellaneous + ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, // Highlight column headers when hovered (may evolve into a fuller highlight) + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ +{ + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will submit an empty label for this column. Convenient for some small columns. Name will still appear in context menu or in angled headers. You may append into this cell by calling TableSetColumnIndex() right after the TableHeadersRow() call. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + ImGuiTableColumnFlags_AngledHeader = 1 << 18, // TableHeadersRow() will submit an angled header row for this column. Note this will add an extra row. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ +{ + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs +{ + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: Debug log, memory allocations macros, ImVector<> +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Debug Logging into ShowDebugLogWindow(), tty and more. +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +#define IMGUI_DEBUG_LOG(...) ImGui::DebugLog(__VA_ARGS__) +#else +#define IMGUI_DEBUG_LOG(...) ((void)0) +#endif + +//----------------------------------------------------------------------------- +// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a custom parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +//----------------------------------------------------------------------------- + +struct ImNewWrapper {}; +inline void* operator new(size_t, ImNewWrapper, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewWrapper, void*) {} // This is only required so we can use the symmetrical new() +#define IM_ALLOC(_SIZE) ImGui::MemAlloc(_SIZE) +#define IM_FREE(_PTR) ImGui::MemFree(_PTR) +#define IM_PLACEMENT_NEW(_PTR) new(ImNewWrapper(), _PTR) +#define IM_NEW(_TYPE) new(ImNewWrapper(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +template void IM_DELETE(T* p) { if (p) { p->~T(); ImGui::MemFree(p); } } + +//----------------------------------------------------------------------------- +// ImVector<> +// Lightweight std::vector<>-like class to avoid dragging dependencies (also, some implementations of STL with debug enabled are absurdly slow, we bypass it so our code runs fast in debug). +//----------------------------------------------------------------------------- +// - You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our public structures are relying on it. +// - We use std-like naming convention here, which is a little unusual for this codebase. +// - Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. +// - Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, +// Do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. +//----------------------------------------------------------------------------- + +IM_MSVC_RUNTIME_CHECKS_OFF +template +struct ImVector +{ + int Size; + int Capacity; + T* Data; + + // Provide standard typedefs but we don't use them ourselves. + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + // Constructors, destructor + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ImVector(const ImVector& src) { Size = Capacity = 0; Data = NULL; operator=(src); } + inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); if (src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } + inline ~ImVector() { if (Data) IM_FREE(Data); } // Important: does not destruct anything + + inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } // Important: does not destruct anything + inline void clear_delete() { for (int n = 0; n < Size; n++) IM_DELETE(Data[n]); clear(); } // Important: never called automatically! always explicit. + inline void clear_destruct() { for (int n = 0; n < Size; n++) Data[n].~T(); clear(); } // Important: never called automatically! always explicit. + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int size_in_bytes() const { return Size * (int)sizeof(T); } + inline int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); } + inline int capacity() const { return Capacity; } + inline T& operator[](int i) { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + inline const T& operator[](int i) const { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return Data + Size; } + inline const T* end() const { return Data + Size; } + inline T& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const T& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline T& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const T& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; T* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; return new_capacity > sz ? new_capacity : sz; } + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } + inline void shrink(int new_size) { IM_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation + inline void reserve(int new_capacity) { if (new_capacity <= Capacity) return; T* new_data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); if (Data) { memcpy(new_data, Data, (size_t)Size * sizeof(T)); IM_FREE(Data); } Data = new_data; Capacity = new_capacity; } + inline void reserve_discard(int new_capacity) { if (new_capacity <= Capacity) return; if (Data) IM_FREE(Data); Data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); Capacity = new_capacity; } + + // NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the ImVector data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const T& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); memcpy(&Data[Size], &v, sizeof(v)); Size++; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const T& v) { if (Size == 0) push_back(v); else insert(Data, v); } + inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } + inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data + Size && it_last >= it && it_last <= Data + Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - (size_t)count) * sizeof(T)); Size -= (int)count; return Data + off; } + inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; if (it < Data + Size - 1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } + inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data + Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } + inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } + inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline int find_index(const T& v) const { const T* data_end = Data + Size; const T* it = find(v); if (it == data_end) return -1; const ptrdiff_t off = it - Data; return (int)off; } + inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } + inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiStyle +//----------------------------------------------------------------------------- +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, use ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, +// and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +//----------------------------------------------------------------------------- + +struct ImGuiStyle +{ + float Alpha; // Global alpha applies to everything in Dear ImGui. + float DisabledAlpha; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + ImVec2 WindowPadding; // Padding within a window. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float WindowBorderHoverPadding; // Hit-testing extent outside/inside resizing border. Also extend determination of hovered window. Generally meaningfully larger than WindowBorderSize to make it easy to reach borders. + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constrain individual windows, use SetNextWindowSizeConstraints(). + ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. + ImGuiDir WindowMenuButtonPosition; // Side of the collapsing/docking button in the title bar (None/Left/Right). Defaults to ImGuiDir_Left. + float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. + float ChildBorderSize; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + float PopupRounding; // Radius of popup window corners rounding. (Note that tooltip windows use WindowRounding) + float PopupBorderSize; // Thickness of border around popup/tooltip windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 FramePadding; // Padding within a framed rectangle (used by most widgets). + float FrameRounding; // Radius of frame corners rounding. Set to 0.0f to have rectangular frame (used by most widgets). + float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). + ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. + ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + float ScrollbarSize; // Width of the vertical scrollbar, Height of the horizontal scrollbar. + float ScrollbarRounding; // Radius of grab corners for scrollbar. + float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. + float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + float LogSliderDeadzone; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + float ImageBorderSize; // Thickness of border around Image() calls. + float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + float TabBorderSize; // Thickness of border around tabs. + float TabCloseButtonMinWidthSelected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. + float TabCloseButtonMinWidthUnselected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. + float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + float TabBarOverlineSize; // Thickness of tab-bar overline, which highlights the selected tab-bar. + float TableAngledHeadersAngle; // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees). + ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell + ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). + ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + float SeparatorTextBorderSize; // Thickness of border in SeparatorText() + ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + ImVec2 DisplayWindowPadding; // Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen. + ImVec2 DisplaySafeAreaPadding; // Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured). + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). We apply per-monitor DPI scaling over this scale. May be removed later. + bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Colors + ImVec4 Colors[ImGuiCol_COUNT]; + + // Behaviors + // (It is possible to modify those fields mid-frame if specific behavior need it, unlike e.g. configuration fields in ImGuiIO) + float HoverStationaryDelay; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + float HoverDelayShort; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + float HoverDelayNormal; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + ImGuiHoveredFlags HoverFlagsForTooltipNav; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + + IMGUI_API ImGuiStyle(); + IMGUI_API void ScaleAllSizes(float scale_factor); + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // TabMinWidthForCloseButton = TabCloseButtonMinWidthUnselected // Renamed in 1.91.9. +#endif +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiIO +//----------------------------------------------------------------------------- +// Communicate most settings and inputs/outputs to Dear ImGui using this structure. +// Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +// It is generally expected that: +// - initialization: backends and user code writes to ImGuiIO. +// - main loop: backends writes to ImGuiIO, user code and imgui code reads from ImGuiIO. +//----------------------------------------------------------------------------- +// Also see ImGui::GetPlatformIO() and ImGuiPlatformIO struct for OS/platform related functions: clipboard, IME etc. +//----------------------------------------------------------------------------- + +// [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. +// If prior to 1.87 you used io.KeysDownDuration[] (which was marked as internal), you should use GetKeyData(key)->DownDuration and *NOT* io.KeysData[key]->DownDuration. +struct ImGuiKeyData +{ + bool Down; // True for if key is down + float DownDuration; // Duration the key has been down (<0.0f: not pressed, 0.0f: just pressed, >0.0f: time held) + float DownDurationPrev; // Last frame duration the key has been down + float AnalogValue; // 0.0f..1.0f for gamepad values +}; + +struct ImGuiIO +{ + //------------------------------------------------------------------ + // Configuration // Default value + //------------------------------------------------------------------ + + ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Keyboard/Gamepad navigation options, etc. + ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend. + ImVec2 DisplaySize; // // Main display size, in pixels (generally == GetMainViewport()->Size). May change every frame. + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. May change every frame. + float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. + const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. + const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). + void* UserData; // = NULL // Store your own data. + + // Font system + ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. + float FontGlobalScale; // = 1.0f // Global scale all fonts + bool FontAllowUserScaling; // = false // [OBSOLETE] Allow user scaling text of individual window with CTRL+Wheel. + ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. + ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + + // Keyboard/Gamepad Navigation options + bool ConfigNavSwapGamepadButtons; // = false // Swap Activate<>Cancel (A<>B) buttons, matching typical "Nintendo/Japanese style" gamepad layout. + bool ConfigNavMoveSetMousePos; // = false // Directional/tabbing navigation teleports the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is difficult. Will update io.MousePos and set io.WantSetMousePos=true. + bool ConfigNavCaptureKeyboard; // = true // Sets io.WantCaptureKeyboard when io.NavActive is set. + bool ConfigNavEscapeClearFocusItem; // = true // Pressing Escape can clear focused item + navigation id/highlight. Set to false if you want to always keep highlight on. + bool ConfigNavEscapeClearFocusWindow;// = false // Pressing Escape can clear focused window as well (super set of io.ConfigNavEscapeClearFocusItem). + bool ConfigNavCursorVisibleAuto; // = true // Using directional navigation key makes the cursor visible. Mouse click hides the cursor. + bool ConfigNavCursorVisibleAlways; // = false // Navigation cursor is always visible. + + // Miscellaneous options + // (you can visualize and interact with all options in 'Demo->Configuration') + bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. + bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // Swap Cmd<>Ctrl keys + OS X style text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. + bool ConfigInputTrickleEventQueue; // = true // Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates. + bool ConfigInputTextCursorBlink; // = true // Enable blinking cursor (optional as some users consider it to be distracting). + bool ConfigInputTextEnterKeepActive; // = false // [BETA] Pressing Enter will keep item active and select contents (single-line only). + bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires ImGuiBackendFlags_HasMouseCursors for better mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) + bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. + bool ConfigWindowsCopyContentsWithCtrlC; // = false // [EXPERIMENTAL] CTRL+C copy the contents of focused window into the clipboard. Experimental because: (1) has known issues with nested Begin/End pairs (2) text output quality varies (3) text output is in submission order rather than spatial order. + bool ConfigScrollbarScrollByPage; // = true // Enable scrolling page by page when clicking outside the scrollbar grab. When disabled, always scroll to clicked location. When enabled, Shift+Click scrolls to clicked location. + float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + + // Inputs Behaviors + // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. + + //------------------------------------------------------------------ + // Debug options + //------------------------------------------------------------------ + + // Options to configure Error Handling and how we handle recoverable errors [EXPERIMENTAL] + // - Error recovery is provided as a way to facilitate: + // - Recovery after a programming error (native code or scripting language - the later tends to facilitate iterating on code while running). + // - Recovery after running an exception handler or any error processing which may skip code after an error has been detected. + // - Error recovery is not perfect nor guaranteed! It is a feature to ease development. + // You not are not supposed to rely on it in the course of a normal application run. + // - Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT(). + // - By design, we do NOT allow error recovery to be 100% silent. One of the three options needs to be checked! + // - Always ensure that on programmers seats you have at minimum Asserts or Tooltips enabled when making direct imgui API calls! + // Otherwise it would severely hinder your ability to catch and correct mistakes! + // Read https://github.com/ocornut/imgui/wiki/Error-Handling for details. + // - Programmer seats: keep asserts (default), or disable asserts and keep error tooltips (new and nice!) + // - Non-programmer seats: maybe disable asserts, but make sure errors are resurfaced (tooltips, visible log entries, use callback etc.) + // - Recovery after error/exception: record stack sizes with ErrorRecoveryStoreState(), disable assert, set log callback (to e.g. trigger high-level breakpoint), recover with ErrorRecoveryTryToRecoverState(), restore settings. + bool ConfigErrorRecovery; // = true // Enable error recovery support. Some errors won't be detected and lead to direct crashes if recovery is disabled. + bool ConfigErrorRecoveryEnableAssert; // = true // Enable asserts on recoverable error. By default call IM_ASSERT() when returning from a failing IM_ASSERT_USER_ERROR() + bool ConfigErrorRecoveryEnableDebugLog; // = true // Enable debug log output on recoverable errors. + bool ConfigErrorRecoveryEnableTooltip; // = true // Enable tooltip on recoverable errors. The tooltip include a way to enable asserts if they were disabled. + + // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. + // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. + // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. + // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + bool ConfigDebugIsDebuggerPresent; // = false // Enable various tools calling IM_DEBUG_BREAK(). + + // Tools to detect code submitting items with conflicting/duplicate IDs + // - Code should use PushID()/PopID() in loops, or append "##xx" to same-label identifiers. + // - Empty label e.g. Button("") == same ID as parent widget/node. Use Button("##xx") instead! + // - See FAQ https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-about-the-id-stack-system + bool ConfigDebugHighlightIdConflicts;// = true // Highlight and show an error message popup when multiple items have conflicting identifiers. + bool ConfigDebugHighlightIdConflictsShowItemPicker;//=true // Show "Item Picker" button in aforementioned popup. + + // Tools to test correct Begin/End and BeginChild/EndChild behaviors. + // - Presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() + // - This is inconsistent with other BeginXXX functions and create confusion for many users. + // - We expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. + bool ConfigDebugBeginReturnValueOnce;// = false // First-time calls to Begin()/BeginChild() will return false. NEEDS TO BE SET AT APPLICATION BOOT TIME if you don't want to miss windows. + bool ConfigDebugBeginReturnValueLoop;// = false // Some calls to Begin()/BeginChild() will return false. Will cycle through window depths then repeat. Suggested use: add "io.ConfigDebugBeginReturnValue = io.KeyShift" in your main loop then occasionally press SHIFT. Windows should be flickering while running. + + // Option to deactivate io.AddFocusEvent(false) handling. + // - May facilitate interactions with a debugger when focus loss leads to clearing inputs data. + // - Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. + bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys()/io.ClearInputMouse() in input processing. + + // Option to audit .ini data + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) + + //------------------------------------------------------------------ + // Platform Identifiers + // (the imgui_impl_xxxx backend files are setting those up for you) + //------------------------------------------------------------------ + + // Nowadays those would be stored in ImGuiPlatformIO but we are leaving them here for legacy reasons. + // Optional: Platform/Renderer backend name (informational only! will be displayed in About Window) + User data for backend/wrappers to store their own stuff. + const char* BackendPlatformName; // = NULL + const char* BackendRendererName; // = NULL + void* BackendPlatformUserData; // = NULL // User data for platform backend + void* BackendRendererUserData; // = NULL // User data for renderer backend + void* BackendLanguageUserData; // = NULL // User data for non C++ programming language backend + + //------------------------------------------------------------------ + // Input - Call before calling NewFrame() + //------------------------------------------------------------------ + + // Input Functions + IMGUI_API void AddKeyEvent(ImGuiKey key, bool down); // Queue a new key down/up event. Key should be "translated" (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) + IMGUI_API void AddKeyAnalogEvent(ImGuiKey key, bool down, float v); // Queue a new key down/up event for analog values (e.g. ImGuiKey_Gamepad_ values). Dead-zones should be handled by the backend. + IMGUI_API void AddMousePosEvent(float x, float y); // Queue a mouse position update. Use -FLT_MAX,-FLT_MAX to signify no mouse (e.g. app not focused and not hovered) + IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change + IMGUI_API void AddMouseWheelEvent(float wheel_x, float wheel_y); // Queue a mouse wheel update. wheel_y<0: scroll down, wheel_y>0: scroll up, wheel_x<0: scroll right, wheel_x>0: scroll left. + IMGUI_API void AddMouseSourceEvent(ImGuiMouseSource source); // Queue a mouse source change (Mouse/TouchScreen/Pen) + IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) + IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input + IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from a UTF-16 character, it can be a surrogate + IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue a new characters input from a UTF-8 string + + IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. + IMGUI_API void SetAppAcceptingEvents(bool accepting_events); // Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. + IMGUI_API void ClearEventsQueue(); // Clear all incoming events. + IMGUI_API void ClearInputKeys(); // Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. + IMGUI_API void ClearInputMouse(); // Clear current mouse state. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + IMGUI_API void ClearInputCharacters(); // [Obsoleted in 1.89.8] Clear the current frame text input buffer. Now included within ClearInputKeys(). +#endif + + //------------------------------------------------------------------ + // Output - Updated by NewFrame() or EndFrame()/Render() + // (when reading from the io.WantCaptureMouse, io.WantCaptureKeyboard flags to dispatch your inputs, it is + // generally easier and more correct to use their state BEFORE calling NewFrame(). See FAQ for details!) + //------------------------------------------------------------------ + + bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them to your main game/application (either way, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). + bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, in this case do not dispatch them to your main game/application (either way, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). + bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard. This is set by Dear ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantSetMousePos; // MousePos has been altered, backend should reposition mouse on next frame. Rarely used! Set only when io.ConfigNavMoveSetMousePos is enabled. + bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. Important: clear io.WantSaveIniSettings yourself after saving! + bool NavActive; // Keyboard/Gamepad navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Keyboard/Gamepad navigation highlight is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Estimate of application framerate (rolling average over 60 frames, based on io.DeltaTime), in frame per second. Solely for convenience. Slow applications may not want to use a moving average or may want to reset underlying buffers occasionally. + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsRenderWindows; // Number of visible windows + int MetricsActiveWindows; // Number of active windows + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + + //------------------------------------------------------------------ + // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! + //------------------------------------------------------------------ + + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + + // Main Input State + // (this block used to be written by backend, since 1.87 it is best to NOT write to those directly, call the AddXXX functions above instead) + // (reading from those variables is fair game, as they are extremely unlikely to be moving anywhere) + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX, -FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras (ImGuiMouseButton_COUNT == 5). Dear ImGui mostly uses left and right buttons. Other buttons allow us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll. + float MouseWheelH; // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends. + ImGuiMouseSource MouseSource; // Mouse actual input peripheral (Mouse/TouchScreen/Pen). + bool KeyCtrl; // Keyboard modifier down: Control + bool KeyShift; // Keyboard modifier down: Shift + bool KeyAlt; // Keyboard modifier down: Alt + bool KeySuper; // Keyboard modifier down: Cmd/Super/Windows + + // Other state maintained from data above + IO function calls + ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. Read-only, updated by NewFrame() + ImGuiKeyData KeysData[ImGuiKey_NamedKey_COUNT];// Key state for all known keys. Use IsKeyXXX() functions to access this. + bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. + ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + double MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down (same as MouseClickedCount[x] != 0) + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? (same as MouseClickedCount[x] == 2) + ImU16 MouseClickedCount[5]; // == 0 (not clicked), == 1 (same as MouseClicked[]), == 2 (double-clicked), == 3 (triple-clicked) etc. when going from !Down to Down + ImU16 MouseClickedLastCount[5]; // Count successive number of clicks. Stays valid after mouse release. Reset after another click is done. + bool MouseReleased[5]; // Mouse button went from Down to !Down + double MouseReleasedTime[5]; // Time of last released (rarely used! but useful to handle delayed single-click when trying to disambiguate them from double-click). + bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window or over void blocked by a popup. We don't request mouse capture from the application if click started outside ImGui bounds. + bool MouseDownOwnedUnlessPopupClose[5]; // Track if button was clicked inside a dear imgui window. + bool MouseWheelRequestAxisSwap; // On a non-Mac system, holding SHIFT requests WheelY to perform the equivalent of a WheelX event. On a Mac system this is already enforced by the system. + bool MouseCtrlLeftAsRightClick; // (OSX) Set to true when the current click was a Ctrl+click that spawned a simulated right click + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) + float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. + bool AppFocusLost; // Only modify via AddFocusEvent() + bool AppAcceptingEvents; // Only modify via SetAppAcceptingEvents() + ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() + ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). + // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) + // Old (<1.87): ImGui::IsKeyPressed(MYPLATFORM_KEY_SPACE) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) + // Read https://github.com/ocornut/imgui/issues/4921 for details. + //int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + //bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. + //float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. + //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. + + // Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO. + // As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; +#endif + + IMGUI_API ImGuiIO(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) +//----------------------------------------------------------------------------- + +// Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. +// The callback function should return 0 by default. +// Callbacks (follow a flag name and see comments in ImGuiInputTextFlags_ declarations for more details) +// - ImGuiInputTextFlags_CallbackEdit: Callback on buffer edit. Note that InputText() already returns true on edit + you can always use IsItemEdited(). The callback is useful to manipulate the underlying buffer while focus is active. +// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration +// - ImGuiInputTextFlags_CallbackCompletion: Callback on pressing TAB +// - ImGuiInputTextFlags_CallbackHistory: Callback on pressing Up/Down arrows +// - ImGuiInputTextFlags_CallbackCharFilter: Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. +// - ImGuiInputTextFlags_CallbackResize: Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. +struct ImGuiInputTextCallbackData +{ + ImGuiContext* Ctx; // Parent UI context + ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + + // Arguments for the different callback events + // - During Resize callback, Buf will be same as your input buffer. + // - However, during Completion/History/Always callback, Buf always points to our own internal data (it is not the same as your buffer)! Changes to it will be reflected into your own buffer shortly after the callback. + // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. + // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. + ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only // [Completion,History] + char* Buf; // Text buffer // Read-write // [Resize] Can replace pointer / [Completion,History,Always] Only write to pointed data, don't replace the actual pointer! + int BufTextLen; // Text length (in bytes) // Read-write // [Resize,Completion,History,Always] Exclude zero-terminator storage. In C land: == strlen(some_text), in C++ land: string.length() + int BufSize; // Buffer size (in bytes) = capacity+1 // Read-only // [Resize,Completion,History,Always] Include zero-terminator storage. In C land == ARRAYSIZE(my_char_array), in C++ land: string.capacity()+1 + bool BufDirty; // Set if you modify Buf/BufTextLen! // Write // [Completion,History,Always] + int CursorPos; // // Read-write // [Completion,History,Always] + int SelectionStart; // // Read-write // [Completion,History,Always] == to SelectionEnd when no selection) + int SelectionEnd; // // Read-write // [Completion,History,Always] + + // Helper functions for text manipulation. + // Use those function to benefit from the CallbackResize behaviors. Calling those function reset the selection. + IMGUI_API ImGuiInputTextCallbackData(); + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + void SelectAll() { SelectionStart = 0; SelectionEnd = BufTextLen; } + void ClearSelection() { SelectionStart = SelectionEnd = BufTextLen; } + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). +// NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. +struct ImGuiSizeCallbackData +{ + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints(). Generally store an integer or float in here (need reinterpret_cast<>). + ImVec2 Pos; // Read-only. Window position, for reference. + ImVec2 CurrentSize; // Read-only. Current window size. + ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. +}; + +// Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() +struct ImGuiPayload +{ + // Members + void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[32 + 1]; // Data type tag (short user-supplied string, 32 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +//----------------------------------------------------------------------------- + +// Helper: Unicode defines +#define IM_UNICODE_CODEPOINT_INVALID 0xFFFD // Invalid Unicode code point (standard value). +#ifdef IMGUI_USE_WCHAR32 +#define IM_UNICODE_CODEPOINT_MAX 0x10FFFF // Maximum Unicode code point supported by this build. +#else +#define IM_UNICODE_CODEPOINT_MAX 0xFFFF // Maximum Unicode code point supported by this build. +#endif + +// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create a UI within deep-nested code that runs multiple times every frame. +// Usage: static ImGuiOnceUponAFrame oaf; if (oaf) ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + ImGuiOnceUponAFrame() { RefFrame = -1; } + mutable int RefFrame; + operator bool() const { int current_frame = ImGui::GetFrameCount(); if (RefFrame == current_frame) return false; RefFrame = current_frame; return true; } +}; + +// Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" +struct ImGuiTextFilter +{ + IMGUI_API ImGuiTextFilter(const char* default_filter = ""); + IMGUI_API bool Draw(const char* label = "Filter (inc,-exc)", float width = 0.0f); // Helper calling InputText+Build + IMGUI_API bool PassFilter(const char* text, const char* text_end = NULL) const; + IMGUI_API void Build(); + void Clear() { InputBuf[0] = 0; Build(); } + bool IsActive() const { return !Filters.empty(); } + + // [Internal] + struct ImGuiTextRange + { + const char* b; + const char* e; + + ImGuiTextRange() { b = e = NULL; } + ImGuiTextRange(const char* _b, const char* _e) { b = _b; e = _e; } + bool empty() const { return b == e; } + IMGUI_API void split(char separator, ImVector* out) const; + }; + char InputBuf[256]; + ImVectorFilters; + int CountGrep; +}; + +// Helper: Growable text buffer for logging/accumulating text +// (this could be called 'ImGuiTextBuilder' / 'ImGuiStringBuilder') +struct ImGuiTextBuffer +{ + ImVector Buf; + IMGUI_API static char EmptyString[1]; + + ImGuiTextBuffer() { } + inline char operator[](int i) const { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } + const char* begin() const { return Buf.Data ? &Buf.front() : EmptyString; } + const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator + int size() const { return Buf.Size ? Buf.Size - 1 : 0; } + bool empty() const { return Buf.Size <= 1; } + void clear() { Buf.clear(); } + void resize(int size) { if (Buf.Size > size) Buf.Data[size] = 0; Buf.resize(size ? size + 1 : 0, 0); } // Similar to resize(0) on ImVector: empty string but don't free buffer. + void reserve(int capacity) { Buf.reserve(capacity); } + const char* c_str() const { return Buf.Data ? Buf.Data : EmptyString; } + IMGUI_API void append(const char* str, const char* str_end = NULL); + IMGUI_API void appendf(const char* fmt, ...) IM_FMTARGS(2); + IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); +}; + +// [Internal] Key+Value for ImGuiStorage +struct ImGuiStoragePair +{ + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + ImGuiStoragePair(ImGuiID _key, int _val) { key = _key; val_i = _val; } + ImGuiStoragePair(ImGuiID _key, float _val) { key = _key; val_f = _val; } + ImGuiStoragePair(ImGuiID _key, void* _val) { key = _key; val_p = _val; } +}; + +// Helper: Key->Value storage +// Typically you don't have to worry about this since a storage is held within each Window. +// We use it to e.g. store collapse state for a tree (Int 0/1) +// This is optimized for efficient lookup (dichotomy into a contiguous buffer) and rare insertion (typically tied to user interactions aka max once a frame) +// You can use it as custom user storage for temporary values. Declare your own storage if, for example: +// - You want to manipulate the open/close state of a particular sub-tree in your interface (tree node uses Int 0/1 to store their state). +// - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) +// Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. +struct ImGuiStorage +{ + // [Internal] + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Advanced: for quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); + // Obsolete: use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //typedef ::ImGuiStoragePair ImGuiStoragePair; // 1.90.8: moved type outside struct +#endif +}; + +// Helper: Manually clip large list of items. +// If you have lots evenly spaced items and you have random access to the list, you can perform coarse +// clipping based on visibility to only submit items that are in view. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally +// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily +// scale using lists with tens of thousands of items without a problem) +// Usage: +// ImGuiListClipper clipper; +// clipper.Begin(1000); // We have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// Generally what happens is: +// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. +// - User code submit that one element. +// - Clipper can measure the height of the first element +// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. +// - User code submit visible elements. +// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc. +struct ImGuiListClipper +{ + ImGuiContext* Ctx; // Parent UI context + int DisplayStart; // First item to display, updated by each call to Step() + int DisplayEnd; // End of items to display (exclusive) + int ItemsCount; // [Internal] Number of items + float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it + float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed + double StartSeekOffsetY; // [Internal] Account for frozen rows in a table and initial loss of precision in very large windows. + void* TempData; // [Internal] Internal data + + // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step, and you can call SeekCursorForItem() manually if you need) + // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). + IMGUI_API ImGuiListClipper(); + IMGUI_API ~ImGuiListClipper(); + IMGUI_API void Begin(int items_count, float items_height = -1.0f); + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. + + // Call IncludeItemByIndex() or IncludeItemsByIndex() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. + // (Due to alignment / padding of certain items it is possible that an extra item may be included on either end of the display range). + inline void IncludeItemByIndex(int item_index) { IncludeItemsByIndex(item_index, item_index + 1); } + IMGUI_API void IncludeItemsByIndex(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. + + // Seek cursor toward given item. This is automatically called while stepping. + // - The only reason to call this is: you can use ImGuiListClipper::Begin(INT_MAX) if you don't know item count ahead of time. + // - In this case, after all steps are done, you'll want to call SeekCursorForItem(item_count). + IMGUI_API void SeekCursorForItem(int item_index); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void IncludeRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9] + inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6] + //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] +#endif +}; + +// Helpers: ImVec2/ImVec4 operators +// - It is important that we are keeping those disabled by default so they don't leak in user space. +// - This is in order to allow user enabling implicit cast operators between ImVec2/ImVec4 and their own types (using IM_VEC2_CLASS_EXTRA in imconfig.h) +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +// - We intentionally provide ImVec2*float but not float*ImVec2: this is rare enough and we want to reduce the surface for possible user mistake. +#ifdef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +IM_MSVC_RUNTIME_CHECKS_OFF +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +static inline bool operator==(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; } +static inline bool operator!=(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; } +IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Helpers macros to generate 32-bit encoded colors +// - User can declare their own format by #defining the 5 _SHIFT/_MASK macros in their imconfig file. +// - Any setting other than the default will need custom backend support. The only standard backend that supports anything else than the default is DirectX9. +#ifndef IM_COL32_R_SHIFT +#ifdef IMGUI_USE_BGRA_PACKED_COLOR +#define IM_COL32_R_SHIFT 16 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 0 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#else +#define IM_COL32_R_SHIFT 0 +#define IM_COL32_G_SHIFT 8 +#define IM_COL32_B_SHIFT 16 +#define IM_COL32_A_SHIFT 24 +#define IM_COL32_A_MASK 0xFF000000 +#endif +#endif +#define IM_COL32(R,G,B,A) (((ImU32)(A)<> IM_COL32_R_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * (1.0f / 255.0f)) {} + inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } + inline operator ImVec4() const { return Value; } + + // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. + inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) +//----------------------------------------------------------------------------- + +// Multi-selection system +// Documentation at: https://github.com/ocornut/imgui/wiki/Multi-Select +// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. +// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) +// with support for clipper (skipping non-visible items), box-select and many other details. +// - Selectable(), Checkbox() are supported but custom widgets may use it as well. +// - TreeNode() is technically supported but... using this correctly is more complicated: you need some sort of linear/random access to your tree, +// which is suited to advanced trees setups also implementing filters and clipper. We will work toward simplifying and demoing it. +// - In the spirit of Dear ImGui design, your code owns actual selection data. +// This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience to quickly implement multi-selection. +// Usage: +// - Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set. +// - Store and maintain actual selection data using persistent object identifiers. +// - Usage flow: +// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (2) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. +// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (6) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 2. +// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. +// About ImGuiSelectionUserData: +// - This can store an application-defined identifier (e.g. index or pointer) submitted via SetNextItemSelectionUserData(). +// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. +// - Most applications will store an object INDEX, hence the chosen name and type. Storing an index is natural, because +// SetRange requests will give you two end-points and you will need to iterate/interpolate between them to update your selection. +// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside ImGuiSelectionUserData. +// Our system never assume that you identify items by indices, it never attempts to interpolate between two values. +// - If you enable ImGuiMultiSelectFlags_NoRangeSelect then it is guaranteed that you will never have to interpolate +// between two ImGuiSelectionUserData, which may be a convenient way to use part of the feature with less code work. +// - As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, +// being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside. + +// Flags for BeginMultiSelect() +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_None = 0, + ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. + ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). With BoxSelect is also ensure contiguous SetRange requests are not combined into one. This allows not handling interpolation in SetRange requests. + ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5, // Disable clearing selection when clicking/selecting an already selected item. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. full row Selectable()). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This is slower: alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 8, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 9, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 10, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 11, // Scope for _BoxSelect and _ClearOnClickVoid is whole window (Default). Use if BeginMultiSelect() covers a whole window or used a single time in same window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 12, // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + //ImGuiMultiSelectFlags_RangeSelect2d = 1 << 15, // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does. + ImGuiMultiSelectFlags_NavWrapX = 1 << 16, // [Temporary] Enable navigation wrapping on X axis. Provided as a convenience because we don't have a design for the general Nav API for this yet. When the more general feature be public we may obsolete this flag in favor of new one. +}; + +// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). +// This mainly contains a list of selection requests. +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only useful if your list is dynamic and allows deletion (getting post-deletion focus/state right is shown in the demo) +// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. +struct ImGuiMultiSelectIO +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (often the first selected item) must never be clipped: use clipper.IncludeItemByIndex() to ensure it is submitted. + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + int ItemsCount; // ms:w, app:r / app:r // 'int items_count' parameter to BeginMultiSelect() is copied here for convenience, allowing simpler calls to your ApplyRequests handler. Not used internally. +}; + +// Selection request type +enum ImGuiSelectionRequestType +{ + ImGuiSelectionRequestType_None = 0, + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true). We cannot set RangeFirstItem/RangeLastItem as its contents is entirely up to user (not necessarily an index) + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. +}; + +// Selection request item +struct ImGuiSelectionRequest +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. + bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) + ImS8 RangeDirection; // / ms:w app:r // Parameter for SetRange request: +1 when RangeFirstItem comes before RangeLastItem, -1 otherwise. Useful if you want to preserve selection order on a backward Shift+Click. + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom). + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top). Inclusive! +}; + +// Optional helper to store multi-selection state + apply multi-selection requests. +// - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' +// Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. +// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. +// To store a multi-selection, in your application you could: +// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - Use your own external storage: e.g. std::set, std::vector, interval trees, intrusively stored selection etc. +// In ImGuiSelectionBasicStorage we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - use decently optimized logic to allow queries and insertion of very large selection sets. +// - do not preserve selection order. +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. +// See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. +struct ImGuiSelectionBasicStorage +{ + // Members + int Size; // // Number of selected items, maintained by this helper. + bool PreserveOrder; // = false // GetNextSelectedItem() will return ordered selection (currently implemented by two additional sorts of selection. Could be improved) + void* UserData; // = NULL // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; + int _SelectionOrder;// [Internal] Increasing counter to store selection order + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). + + // Methods + IMGUI_API ImGuiSelectionBasicStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() + IMGUI_API bool Contains(ImGuiID id) const; // Query if an item id is in selection. + IMGUI_API void Clear(); // Clear selection + IMGUI_API void Swap(ImGuiSelectionBasicStorage& r); // Swap two selections + IMGUI_API void SetItemSelected(ImGuiID id, bool selected); // Add/remove an item from selection (generally done by ApplyRequests() function) + IMGUI_API bool GetNextSelectedItem(void** opaque_it, ImGuiID* out_id); // Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' + inline ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. +}; + +// Optional helper to apply multi-selection requests to existing randomly accessible storage. +// Convenient if you want to quickly wire multi-select API on e.g. an array of bool or items storing their own selection state. +struct ImGuiSelectionExternalStorage +{ + // Members + void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } + + // Methods + IMGUI_API ImGuiSelectionExternalStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests by using AdapterSetItemSelected() calls +}; + +//----------------------------------------------------------------------------- +// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) +// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. +//----------------------------------------------------------------------------- + +// The maximum line width to bake anti-aliased textures for. Build atlas with ImFontAtlasFlags_NoBakedLines to disable baking. +#ifndef IM_DRAWLIST_TEX_LINES_WIDTH_MAX +#define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (32) +#endif + +// ImDrawIdx: vertex index. [Compile-time configurable type] +// - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). +// - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibility with renderer backends) +#endif + +// ImDrawCallback: Draw callbacks for advanced uses [configurable type: override in imconfig.h] +// NB: You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering, +// you can poke into the draw list for that! Draw callback may be useful for example to: +// A) Change your GPU render state, +// B) render a complex 3D scene inside a UI element without an intermediate texture/render target, etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) { cmd.UserCallback(parent_list, cmd); } else { RenderTriangles() }' +// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering backend accordingly. +#ifndef ImDrawCallback +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); +#endif + +// Special Draw callback value to request renderer backend to reset the graphics/render state. +// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. +// This is useful, for example, if you submitted callbacks which you know have altered the render state and you want it to be restored. +// Render state is not reset by default because they are many perfectly useful way of altering render state (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) + +// Typically, 1 command = 1 GPU draw call (unless command is a callback) +// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, +// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. +// Backends made for <1.71. will typically ignore the VtxOffset fields. +// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for). +struct ImDrawCmd +{ + ImVec4 ClipRect; // 4*4 // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates + ImTextureID TextureId; // 4-8 // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + unsigned int VtxOffset; // 4 // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices. + unsigned int IdxOffset; // 4 // Start offset in index buffer. + unsigned int ElemCount; // 4 // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImDrawCallback UserCallback; // 4-8 // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // 4-8 // Callback user data (when UserCallback != NULL). If called AddCallback() with size == 0, this is a copy of the AddCallback() argument. If called AddCallback() with size > 0, this is pointing to a buffer where data is stored. + int UserCallbackDataSize; // 4 // Size of callback user data when using storage, otherwise 0. + int UserCallbackDataOffset;// 4 // [Internal] Offset of callback user data when using storage, otherwise -1. + + ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed + + // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) + inline ImTextureID GetTexID() const { return TextureId; } +}; + +// Vertex layout +#ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT +struct ImDrawVert +{ + ImVec2 pos; + ImVec2 uv; + ImU32 col; +}; +#else +// You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h +// The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. +// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared at the time you'd want to set your type up. +// NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. +IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; +#endif + +// [Internal] For use by ImDrawList +struct ImDrawCmdHeader +{ + ImVec4 ClipRect; + ImTextureID TextureId; + unsigned int VtxOffset; +}; + +// [Internal] For use by ImDrawListSplitter +struct ImDrawChannel +{ + ImVector _CmdBuffer; + ImVector _IdxBuffer; +}; + +// Split/Merge functions are used to split the draw list into different layers which can be drawn into out of order. +// This is used by the Columns/Tables API, so items of each column can be batched together in a same draw call. +struct ImDrawListSplitter +{ + int _Current; // Current channel number (0) + int _Count; // Number of active channels (1+) + ImVector _Channels; // Draw channels (not resized down so _Count might be < Channels.Size) + + inline ImDrawListSplitter() { memset(this, 0, sizeof(*this)); } + inline ~ImDrawListSplitter() { ClearFreeMemory(); } + inline void Clear() { _Current = 0; _Count = 1; } // Do not clear Channels[] so our allocations are reused next frame + IMGUI_API void ClearFreeMemory(); + IMGUI_API void Split(ImDrawList* draw_list, int count); + IMGUI_API void Merge(ImDrawList* draw_list); + IMGUI_API void SetCurrentChannel(ImDrawList* draw_list, int channel_idx); +}; + +// Flags for ImDrawList functions +// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused) +enum ImDrawFlags_ +{ + ImDrawFlags_None = 0, + ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason) + ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. + ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. + ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. + ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. + ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! + ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, + ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. + ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, +}; + +// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. +// It is however possible to temporarily alter flags between calls to ImDrawList:: functions. +enum ImDrawListFlags_ +{ + ImDrawListFlags_None = 0, + ImDrawListFlags_AntiAliasedLines = 1 << 0, // Enable anti-aliased lines/borders (*2 the number of triangles for 1.0f wide line or lines thin enough to be drawn using textures, otherwise *3 the number of triangles) + ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). + ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. +}; + +// Draw command list +// This is the low-level list of polygons that ImGui:: functions are filling. At the end of the frame, +// all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each dear imgui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to +// access the current window draw list and draw custom primitives. +// You can interleave normal ImGui:: calls and adding primitives to the current draw list. +// In single viewport mode, top-left is == GetMainViewport()->Pos (generally 0,0), bottom-right is == GetMainViewport()->Pos+Size (generally io.DisplaySize). +// You are totally free to apply whatever transformation matrix you want to the data (depending on the use of the transformation you may want to apply it to ClipRect as well!) +// Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. +struct ImDrawList +{ + // This is what you have to render + ImVector CmdBuffer; // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback. + ImVector IdxBuffer; // Index buffer. Each command consume ImDrawCmd::ElemCount of those + ImVector VtxBuffer; // Vertex buffer. + ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. + + // [Internal, used while building lists] + unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) + ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _Path; // [Internal] current path building + ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). + ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) + ImVector _ClipRectStack; // [Internal] + ImVector _TextureIdStack; // [Internal] + ImVector _CallbacksDataBuf; // [Internal] + float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + const char* _OwnerName; // Pointer to owner window's name for debugging + + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData(). + // (advanced: you may create and use your own ImDrawListSharedData so you can use ImDrawList without ImGui, but that's more involved) + IMGUI_API ImDrawList(ImDrawListSharedData* shared_data); + IMGUI_API ~ImDrawList(); + + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + IMGUI_API void PushClipRectFullScreen(); + IMGUI_API void PopClipRect(); + IMGUI_API void PushTextureID(ImTextureID texture_id); + IMGUI_API void PopTextureID(); + inline ImVec2 GetClipRectMin() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.x, cr.y); } + inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } + + // Primitives + // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In older versions (until Dear ImGui 1.77) the AddCircle functions defaulted to num_segments == 12. + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides. + IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); + IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); + IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); + IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0); + IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); + IMGUI_API void AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + + // General polygon + // - Only simple polygons are supported by filling functions (no self-intersections, no holes). + // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library. + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); + + // Image primitives + // - Read FAQ to understand what ImTextureID is. + // - "p_min" and "p_max" represent the upper-left and lower-right corners of the rectangle. + // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture. + IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); + + // Stateful path API, add points then finish with PathFillConvex() or PathStroke() + // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // so e.g. 'PathArcTo(center, radius, PI * -0.5f, PI)' is ok, whereas 'PathArcTo(center, radius, PI, PI * -0.5f)' won't have correct anti-aliasing when followed by PathFillConvex(). + inline void PathClear() { _Path.Size = 0; } + inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } + IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); + IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse + IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); + + // Advanced: Draw Callbacks + // - May be used to alter render state (change sampler, blending, current shader). May be used to emit custom rendering commands (difficult to do correctly, but possible). + // - Use special ImDrawCallback_ResetRenderState callback to instruct backend to reset its render state to the default. + // - Your rendering loop must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. All standard backends are honoring this. + // - For some backends, the callback may access selected render-states exposed by the backend in a ImGui_ImplXXXX_RenderState structure pointed to by platform_io.Renderer_RenderState. + // - IMPORTANT: please be mindful of the different level of indirection between using size==0 (copying argument) and using size>0 (copying pointed data into a buffer). + // - If userdata_size == 0: we copy/store the 'userdata' argument as-is. It will be available unmodified in ImDrawCmd::UserCallbackData during render. + // - If userdata_size > 0, we copy/store 'userdata_size' bytes pointed to by 'userdata'. We store them in a buffer stored inside the drawlist. ImDrawCmd::UserCallbackData will point inside that buffer so you have to retrieve data from there. Your callback may need to use ImDrawCmd::UserCallbackDataSize if you expect dynamically-sized data. + // - Support for userdata_size > 0 was added in v1.91.4, October 2024. So earlier code always only allowed to copy/store a simple void*. + IMGUI_API void AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size = 0); + + // Advanced: Miscellaneous + IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible + IMGUI_API ImDrawList* CloneOutput() const; // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. + + // Advanced: Channels + // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) + // - This API shouldn't have been in ImDrawList in the first place! + // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. + // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. + inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } + inline void ChannelsMerge() { _Splitter.Merge(this); } + inline void ChannelsSetCurrent(int n) { _Splitter.SetCurrentChannel(this, n); } + + // Advanced: Primitives allocations + // - We render triangles (three vertices) + // - All primitives needs to be reserved via PrimReserve() beforehand. + IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimUnreserve(int idx_count, int vtx_count); + IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) + IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); + IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index + + // Obsolete names + //inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0) { PathEllipticalArcTo(center, ImVec2(radius_x, radius_y), rot, a_min, a_max, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + //inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + + // [Internal helpers] + IMGUI_API void _ResetForNewFrame(); + IMGUI_API void _ClearFreeMemory(); + IMGUI_API void _PopUnusedDrawCmd(); + IMGUI_API void _TryMergeDrawCmds(); + IMGUI_API void _OnChangedClipRect(); + IMGUI_API void _OnChangedTextureID(); + IMGUI_API void _OnChangedVtxOffset(); + IMGUI_API void _SetTextureID(ImTextureID texture_id); + IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; + IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); + IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); +}; + +// All draw data to render a Dear ImGui frame +// (NB: the style and the naming convention here is a little inconsistent, we currently preserve them for backward compatibility purpose, +// as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) +struct ImDrawData +{ + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render (should always be == CmdLists.size) + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImVector CmdLists; // Array of ImDrawList* to render. The ImDrawLists are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + ImGuiViewport* OwnerViewport; // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not). + + // Functions + ImDrawData() { Clear(); } + IMGUI_API void Clear(); + IMGUI_API void AddDrawList(ImDrawList* draw_list); // Helper to add an external draw list into an existing ImDrawData. + IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! + IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +}; + +//----------------------------------------------------------------------------- +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) +//----------------------------------------------------------------------------- + +// A font input/source (we may rename this to ImFontSource in the future) +struct ImFontConfig +{ + void* FontData; // // TTF/OTF data + int FontDataSize; // // TTF/OTF data size + bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + bool PixelSnapH; // false // Align every glyph AdvanceX to pixel boundaries. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. + int FontNo; // 0 // Index of font within TTF/OTF file + int OversampleH; // 0 (2) // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1 or 2 depending on size. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleV; // 0 (1) // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1. This is not really useful as we don't use sub-pixel positions on the Y axis. + float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). + //ImVec2 GlyphExtraSpacing; // 0, 0 // (REMOVED IN 1.91.9: use GlyphExtraAdvanceX) + ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. + const ImWchar* GlyphRanges; // NULL // THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). + float GlyphMinAdvanceX; // 0 // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font + float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs + float GlyphExtraAdvanceX; // 0 // Extra spacing (in pixels) between glyphs. Please contact us if you are using this. + unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. + float RasterizerMultiply; // 1.0f // Linearly brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. This is a silly thing we may remove in the future. + float RasterizerDensity; // 1.0f // DPI scale for rasterization, not altering other font metrics: make it easy to swap between e.g. a 100% and a 400% fonts for a zooming display. IMPORTANT: If you increase this it is expected that you increase font scale accordingly, otherwise quality may look lowered. + ImWchar EllipsisChar; // 0 // Explicitly specify Unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. + + // [Internal] + char Name[40]; // Name (strictly to ease debugging) + ImFont* DstFont; + + IMGUI_API ImFontConfig(); +}; + +// Hold rendering data for one glyph. +// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this) +struct ImFontGlyph +{ + unsigned int Colored : 1; // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops) + unsigned int Visible : 1; // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering. + unsigned int Codepoint : 30; // 0x0000..0x10FFFF + float AdvanceX; // Horizontal distance to advance layout with + float X0, Y0, X1, Y1; // Glyph corners + float U0, V0, U1, V1; // Texture coordinates +}; + +// Helper to build glyph ranges from text/string data. Feed your application strings/characters to it then call BuildRanges(). +// This is essentially a tightly packed of vector of 64k booleans = 8KB storage. +struct ImFontGlyphRangesBuilder +{ + ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) + + ImFontGlyphRangesBuilder() { Clear(); } + inline void Clear() { int size_in_bytes = (IM_UNICODE_CODEPOINT_MAX + 1) / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } + inline bool GetBit(size_t n) const { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array + inline void SetBit(size_t n) { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array + inline void AddChar(ImWchar c) { SetBit(c); } // Add character + IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) + IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault()) to force add all of ASCII/Latin+Ext + IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges +}; + +// See ImFontAtlas::AddCustomRectXXX functions. +struct ImFontAtlasCustomRect +{ + unsigned short X, Y; // Output // Packed position in Atlas + + // [Internal] + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned int GlyphID : 31; // Input // For custom font glyphs only (ID < 0x110000) + unsigned int GlyphColored : 1; // Input // For custom font glyphs only: glyph is colored, removed tinting. + float GlyphAdvanceX; // Input // For custom font glyphs only: glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only: glyph display offset + ImFont* Font; // Input // For custom font glyphs only: target font + ImFontAtlasCustomRect() { X = Y = 0xFFFF; Width = Height = 0; GlyphID = 0; GlyphColored = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; } + bool IsPacked() const { return X != 0xFFFF; } +}; + +// Flags for ImFontAtlas build +enum ImFontAtlasFlags_ +{ + ImFontAtlasFlags_None = 0, + ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two + ImFontAtlasFlags_NoMouseCursors = 1 << 1, // Don't build software mouse cursors into the atlas (save a little texture memory) + ImFontAtlasFlags_NoBakedLines = 1 << 2, // Don't build thick line textures into the atlas (save a little texture memory, allow support for point/nearest filtering). The AntiAliasedLinesUseTex features uses them, otherwise they will be rendered using polygons (more expensive for CPU/GPU). +}; + +// Load and rasterize multiple TTF/OTF fonts into a same texture. The font atlas will build a single texture holding: +// - One or more fonts. +// - Custom graphics data needed to render the shapes needed by Dear ImGui. +// - Mouse cursor shapes for software cursor rendering (unless setting 'Flags |= ImFontAtlasFlags_NoMouseCursors' in the font atlas). +// It is the user-code responsibility to setup/build the atlas, then upload the pixel data into a texture accessible by your graphics api. +// - Optionally, call any of the AddFont*** functions. If you don't call any, the default font embedded in the code will be loaded for you. +// - Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// - Upload the pixels data into a texture within your graphics system (see imgui_impl_xxxx.cpp examples) +// - Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture in a format natural to your graphics API. +// This value will be passed back to you during rendering to identify the texture. Read FAQ entry about ImTextureID for more details. +// Common pitfalls: +// - If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the +// atlas is build (when calling GetTexData*** or Build()). We only copy the pointer, not the data. +// - Important: By default, AddFontFromMemoryTTF() takes ownership of the data. Even though we are not writing to it, we will free the pointer on destruction. +// You can set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed, +// - Even though many functions are suffixed with "TTF", OTF data is supported just as well. +// - This is an old API and it is currently awkward for those and various other reasons! We will address them in the future! +struct ImFontAtlas +{ + IMGUI_API ImFontAtlas(); + IMGUI_API ~ImFontAtlas(); + IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); + IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); + IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. + IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. + IMGUI_API void ClearFonts(); // Clear input+output font data (same as ClearInputData() + glyphs storage, UV coordinates). + IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. + IMGUI_API void Clear(); // Clear all input and output. + + // Build atlas, retrieve pixel data. + // User is in charge of copying the pixels into graphics memory (e.g. create a texture with your engine). Then store your texture handle with SetTexID(). + // The pitch is always = Width * BytesPerPixels (1 or 4) + // Building in RGBA32 format is provided for convenience and compatibility, but note that unless you manually manipulate or copy color data into + // the texture (e.g. when using the AddCustomRect*** api), then the RGB pixels emitted will always be white (~75% of memory/bandwidth waste. + IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. + IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel + IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel + bool IsBuilt() const { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent... + void SetTexID(ImTextureID id) { TexID = id; } + + //------------------------------------------- + // Glyph Ranges + //------------------------------------------- + + // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) + // NB: Make sure that your string are UTF-8 and NOT in your local code page. + // Read https://github.com/ocornut/imgui/blob/master/docs/FONTS.md/#about-utf-8-encoding for details. + // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data. + IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesGreek(); // Default + Greek and Coptic + IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseFull(); // Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs + IMGUI_API const ImWchar* GetGlyphRangesChineseSimplifiedCommon();// Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese + IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters + IMGUI_API const ImWchar* GetGlyphRangesThai(); // Default + Thai characters + IMGUI_API const ImWchar* GetGlyphRangesVietnamese(); // Default + Vietnamese characters + + //------------------------------------------- + // [ALPHA] Custom Rectangles/Glyphs API + //------------------------------------------- + + // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. + // - After calling Build(), you can query the rectangle position and render your pixels. + // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of preferred texture format. + // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), + // so you can render e.g. custom colorful icons and use them as regular glyphs. + // - Read docs/FONTS.md for more details about using colorful icons. + // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings. + IMGUI_API int AddCustomRectRegular(int width, int height); + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0)); + ImFontAtlasCustomRect* GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; } + + // [Internal] + IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const; + + //------------------------------------------- + // Members + //------------------------------------------- + + // Input + ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) + ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. + int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. + int TexGlyphPadding; // FIXME: Should be called "TexPackPadding". Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0 (will also need to set AntiAliasedLinesUseTex = false). + void* UserData; // Store your own atlas related user-data (if e.g. you have multiple font atlas). + + // [Internal] + // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + bool TexReady; // Set when texture was built matching current font input + bool TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format. + unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight + unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 + int TexWidth; // Texture width calculated during Build(). + int TexHeight; // Texture height calculated during Build(). + ImVec2 TexUvScale; // = (1.0f/TexWidth, 1.0f/TexHeight) + ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel + ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. + ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. + ImVector Sources; // Source/configuration data + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + + // [Internal] Font builder + const ImFontBuilderIO* FontBuilderIO; // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE). + unsigned int FontBuilderFlags; // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig. + + // [Internal] Packing data + int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors + int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines + + // [Obsolete] + //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ + //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ +}; + +// Font runtime data and rendering +// ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). +struct ImFont +{ + // [Internal] Members: Hot ~20/24 bytes (for CalcTextSize) + ImVector IndexAdvanceX; // 12-16 // out // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this info, and are often bottleneck in large UI). + float FallbackAdvanceX; // 4 // out // = FallbackGlyph->AdvanceX + float FontSize; // 4 // in // Height of characters/line, set during loading (don't change after loading) + + // [Internal] Members: Hot ~28/40 bytes (for RenderText loop) + ImVector IndexLookup; // 12-16 // out // Sparse. Index glyphs by Unicode code-point. + ImVector Glyphs; // 12-16 // out // All glyphs. + ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) + + // [Internal] Members: Cold ~32/40 bytes + // Conceptually Sources[] is the list of font sources merged to create this font. + ImFontAtlas* ContainerAtlas; // 4-8 // out // What we has been loaded into + ImFontConfig* Sources; // 4-8 // in // Pointer within ContainerAtlas->Sources[], to SourcesCount instances + short SourcesCount; // 2 // in // Number of ImFontConfig involved in creating this font. Usually 1, or >1 when merging multiple font sources into one ImFont. + short EllipsisCharCount; // 1 // out // 1 or 3 + ImWchar EllipsisChar; // 2-4 // out // Character used for ellipsis rendering ('...'). + ImWchar FallbackChar; // 2-4 // out // Character used if a glyph isn't found (U+FFFD, '?') + float EllipsisWidth; // 4 // out // Total ellipsis Width + float EllipsisCharStep; // 4 // out // Step between characters when EllipsisCount > 0 + float Scale; // 4 // in // Base font scale (1.0f), multiplied by the per-window font scale which you can adjust with SetWindowFontScale() + float Ascent, Descent; // 4+4 // out // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] (unscaled) + int MetricsTotalSurface;// 4 // out // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + bool DirtyLookupTables; // 1 // out // + ImU8 Used8kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/8192/8]; // 1 bytes if ImWchar=ImWchar16, 16 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints. + + // Methods + IMGUI_API ImFont(); + IMGUI_API ~ImFont(); + IMGUI_API ImFontGlyph* FindGlyph(ImWchar c); + IMGUI_API ImFontGlyph* FindGlyphNoFallback(ImWchar c); + float GetCharAdvance(ImWchar c) { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return Sources ? Sources->Name : ""; } + + // [Internal] Don't use! + // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. + // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL); // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c); + IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false); + + // [Internal] Don't use! + IMGUI_API void BuildLookupTable(); + IMGUI_API void ClearOutputData(); + IMGUI_API void GrowIndex(int new_size); + IMGUI_API void AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. + IMGUI_API bool IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Viewports +//----------------------------------------------------------------------------- + +// Flags stored in ImGuiViewport::Flags, giving indications to the platform backends. +enum ImGuiViewportFlags_ +{ + ImGuiViewportFlags_None = 0, + ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window + ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) + ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: Is created/managed by the application (rather than a dear imgui backend) +}; + +// - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. +// - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. +// - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. +// - About Main Area vs Work Area: +// - Main Area = entire viewport. +// - Work Area = entire viewport minus sections used by main menu bars (for platform windows), or by task bar (for platform monitor). +// - Windows are generally trying to stay within the Work Area of their host viewport. +struct ImGuiViewport +{ + ImGuiID ID; // Unique identifier for the viewport + ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ + ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) + ImVec2 Size; // Main Area: Size of the viewport. + ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) + ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + + // Platform/Backend Dependent Data + void* PlatformHandle; // void* to hold higher-level, platform window handle (e.g. HWND, GLFWWindow*, SDL_Window*) + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (under Win32 this is expected to be a HWND, unused for other platforms) + + ImGuiViewport() { memset(this, 0, sizeof(*this)); } + + // Helpers + ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } + ImVec2 GetWorkCenter() const { return ImVec2(WorkPos.x + WorkSize.x * 0.5f, WorkPos.y + WorkSize.y * 0.5f); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Platform Dependent Interfaces +//----------------------------------------------------------------------------- + +// Access via ImGui::GetPlatformIO() +struct ImGuiPlatformIO +{ + IMGUI_API ImGuiPlatformIO(); + + //------------------------------------------------------------------ + // Input - Interface with OS and Platform backend (most common stuff) + //------------------------------------------------------------------ + + // Optional: Access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*Platform_GetClipboardTextFn)(ImGuiContext* ctx); + void (*Platform_SetClipboardTextFn)(ImGuiContext* ctx, const char* text); + void* Platform_ClipboardUserData; + + // Optional: Open link/folder/file in OS Shell + // (default to use ShellExecuteW() on Windows, system() on Linux/Mac) + bool (*Platform_OpenInShellFn)(ImGuiContext* ctx, const char* path); + void* Platform_OpenInShellUserData; + + // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) + // (default to use native imm32 api on Windows) + void (*Platform_SetImeDataFn)(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + void* Platform_ImeUserData; + //void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); // [Renamed to platform_io.PlatformSetImeDataFn in 1.91.1] + + // Optional: Platform locale + // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point + ImWchar Platform_LocaleDecimalPoint; // '.' + + //------------------------------------------------------------------ + // Input - Interface with Renderer Backend + //------------------------------------------------------------------ + + // Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure. + void* Renderer_RenderState; +}; + +// (Optional) Support for IME (Input Method Editor) via the platform_io.Platform_SetImeDataFn() function. +struct ImGuiPlatformImeData +{ + bool WantVisible; // A widget wants the IME to be visible + ImVec2 InputPos; // Position of the input cursor + float InputLineHeight; // Line height + + ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Obsolete functions and types +// (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +namespace ImGui +{ + // OBSOLETED in 1.91.9 (from February 2025) + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col); // <-- border_col was removed in favor of ImGuiCol_ImageBorder. + // OBSOLETED in 1.91.0 (from July 2024) + static inline void PushButtonRepeat(bool repeat) { PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); } + static inline void PopButtonRepeat() { PopItemFlag(); } + static inline void PushTabStop(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopTabStop() { PopItemFlag(); } + IMGUI_API ImVec2 GetContentRegionMax(); // Content boundaries max (e.g. window boundaries including scrolling, or current column boundaries). You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMin(); // Content boundaries min for the window (roughly (0,0)-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMax(); // Content boundaries max for the window (roughly (0,0)+Size-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + // OBSOLETED in 1.90.0 (from September 2023) + static inline bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0) { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); } + static inline void EndChildFrame() { EndChild(); } + //static inline bool BeginChild(const char* str_id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + //static inline bool BeginChild(ImGuiID id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags) { return BeginChild(id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + static inline void ShowStackToolWindow(bool* p_open = NULL) { ShowIDStackToolWindow(p_open); } + IMGUI_API bool Combo(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int height_in_items = -1); + // OBSOLETED in 1.89.7 (from June 2023) + IMGUI_API void SetItemAllowOverlap(); // Use SetNextItemAllowOverlap() before item. + // OBSOLETED in 1.89.4 (from March 2023) + static inline void PushAllowKeyboardFocus(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopAllowKeyboardFocus() { PopItemFlag(); } + + // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //-- OBSOLETED in 1.89 (from August 2022) + //IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // --> Use new ImageButton() signature (explicit item id, regular FramePadding). Refer to code in 1.91 if you want to grab a copy of this version. + //-- OBSOLETED in 1.88 (from May 2022) + //static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. + //static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. + //-- OBSOLETED in 1.87 (from February 2022, more formally obsoleted April 2024) + //IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); const ImGuiKeyData* key_data = GetKeyData(key); return (ImGuiKey)(key_data - g.IO.KeysData); } // Map ImGuiKey_* values into legacy native key index. == io.KeyMap[key]. When using a 1.87+ backend using io.AddKeyEvent(), calling GetKeyIndex() with ANY ImGuiKey_XXXX values will return the same value! + //static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); return key; } + //-- OBSOLETED in 1.86 (from November 2021) + //IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Code removed, see 1.90 for last version of the code. Calculate range of visible items for large list of evenly sized items. Prefer using ImGuiListClipper. + //-- OBSOLETED in 1.85 (from August 2021) + //static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } + //-- OBSOLETED in 1.81 (from February 2021) + //static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } + //static inline bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1) { float height = GetTextLineHeightWithSpacing() * ((height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f) + GetStyle().FramePadding.y * 2.0f; return BeginListBox(label, ImVec2(0.0f, height)); } // Helper to calculate size from items_count and height_in_items + //static inline void ListBoxFooter() { EndListBox(); } + //-- OBSOLETED in 1.79 (from August 2020) + //static inline void OpenPopupContextItem(const char* str_id = NULL, ImGuiMouseButton mb = 1) { OpenPopupOnItemClick(str_id, mb); } // Bool return value removed. Use IsWindowAppearing() in BeginPopup() instead. Renamed in 1.77, renamed back in 1.79. Sorry! + //-- OBSOLETED in 1.78 (from June 2020): Old drag/sliders functions that took a 'float power > 1.0f' argument instead of ImGuiSliderFlags_Logarithmic. See github.com/ocornut/imgui/issues/3361 for details. + //IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f) // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //-- OBSOLETED in 1.77 and before + //static inline bool BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mb, bool over_items) { return BeginPopupContextWindow(str_id, mb | (over_items ? 0 : ImGuiPopupFlags_NoOpenOverItems)); } // OBSOLETED in 1.77 (from June 2020) + //static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } // OBSOLETED in 1.72 (from July 2019) + //static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } // OBSOLETED in 1.71 (from June 2019) + //static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } // OBSOLETED in 1.70 (from May 2019) + //static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } // OBSOLETED in 1.69 (from Mar 2019) + //static inline void SetScrollHere(float ratio = 0.5f) { SetScrollHereY(ratio); } // OBSOLETED in 1.66 (from Nov 2018) + //static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } // OBSOLETED in 1.63 (from Aug 2018) + //-- OBSOLETED in 1.60 and before + //static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } // OBSOLETED in 1.60 (from Apr 2018) + //static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) + //static inline void ShowTestWindow() { return ShowDemoWindow(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //IMGUI_API bool Begin(char* name, bool* p_open, ImVec2 size_first_use, float bg_alpha = -1.0f, ImGuiWindowFlags flags=0); // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017): Equivalent of using SetNextWindowSize(size, ImGuiCond_FirstUseEver) and SetNextWindowBgAlpha(). + //static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void SetNextWindowPosCenter(ImGuiCond c=0) { SetNextWindowPos(GetMainViewport()->GetCenter(), c, ImVec2(0.5f,0.5f)); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline bool IsItemHoveredRect() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsPosHoveringAnyWindow(const ImVec2&) { IM_ASSERT(0); return false; } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017): This was misleading and partly broken. You probably want to use the io.WantCaptureMouse flag instead. + //static inline bool IsMouseHoveringAnyWindow() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsMouseHoveringWindow() { return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //-- OBSOLETED in 1.50 and before + //static inline bool CollapsingHeader(char* label, const char* str_id, bool framed = true, bool default_open = false) { return CollapsingHeader(label, (default_open ? (1 << 5) : 0)); } // OBSOLETED in 1.49 + //static inline ImFont*GetWindowFont() { return GetFont(); } // OBSOLETED in 1.48 + //static inline float GetWindowFontSize() { return GetFontSize(); } // OBSOLETED in 1.48 + //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 +} + +//-- OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +//typedef ImDrawFlags ImDrawCornerFlags; +//enum ImDrawCornerFlags_ +//{ +// ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit +// ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). +// ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. +// ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. +// ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. +// ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 +// ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, +// ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, +// ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, +// ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, +//}; + +// RENAMED and MERGED both ImGuiKey_ModXXX and ImGuiModFlags_XXX into ImGuiMod_XXX (from September 2022) +// RENAMED ImGuiKeyModFlags -> ImGuiModFlags in 1.88 (from April 2022). Exceptionally commented out ahead of obscolescence schedule to reduce confusion and because they were not meant to be used in the first place. +//typedef ImGuiKeyChord ImGuiModFlags; // == int. We generally use ImGuiKeyChord to mean "a ImGuiKey or-ed with any number of ImGuiMod_XXX value", so you may store mods in there. +//enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiModFlags_Shift = ImGuiMod_Shift, ImGuiModFlags_Alt = ImGuiMod_Alt, ImGuiModFlags_Super = ImGuiMod_Super }; +//typedef ImGuiKeyChord ImGuiKeyModFlags; // == int +//enum ImGuiKeyModFlags_ { ImGuiKeyModFlags_None = 0, ImGuiKeyModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiKeyModFlags_Shift = ImGuiMod_Shift, ImGuiKeyModFlags_Alt = ImGuiMod_Alt, ImGuiKeyModFlags_Super = ImGuiMod_Super }; + +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // OBSOLETED IN 1.90 (now using C++11 standard version) + +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// RENAMED IMGUI_DISABLE_METRICS_WINDOW > IMGUI_DISABLE_DEBUG_TOOLS in 1.88 (from June 2022) +#ifdef IMGUI_DISABLE_METRICS_WINDOW +#error IMGUI_DISABLE_METRICS_WINDOW was renamed to IMGUI_DISABLE_DEBUG_TOOLS, please use new name. +#endif + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +// Include imgui_user.h at the end of imgui.h +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. +#ifdef IMGUI_INCLUDE_IMGUI_USER_H +#ifdef IMGUI_USER_H_FILENAME +#include IMGUI_USER_H_FILENAME +#else +#include "imgui_user.h" +#endif +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imgui_draw.cpp b/thirdparty/imgui/imgui_draw.cpp new file mode 100644 index 00000000..27310152 --- /dev/null +++ b/thirdparty/imgui/imgui_draw.cpp @@ -0,0 +1,4828 @@ +// dear imgui, v1.91.9b +// (drawing and font code) + +/* + +Index of this file: + +// [SECTION] STB libraries implementation +// [SECTION] Style functions +// [SECTION] ImDrawList +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +// [SECTION] ImDrawListSplitter +// [SECTION] ImDrawData +// [SECTION] Helpers ShadeVertsXXX functions +// [SECTION] ImFontConfig +// [SECTION] ImFontAtlas +// [SECTION] ImFontAtlas: glyph ranges helpers +// [SECTION] ImFontGlyphRangesBuilder +// [SECTION] ImFont +// [SECTION] ImGui Internal Render Helpers +// [SECTION] Decompression code +// [SECTION] Default font data (ProggyClean.ttf) + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" +#ifdef IMGUI_ENABLE_FREETYPE +#include "misc/freetype/imgui_freetype.h" +#endif + +#include // vsnprintf, sscanf, printf + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wcomma" // warning: possible misuse of comma operator here +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wcast-qual" // warning: cast from 'const xxxx *' to 'xxx *' drops const qualifier +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wstack-protector" // warning: stack protector not protecting local variables: variable length buffer +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +//------------------------------------------------------------------------- +// [SECTION] STB libraries implementation (for stb_truetype and stb_rect_pack) +//------------------------------------------------------------------------- + +// Compile time options: +//#define IMGUI_STB_NAMESPACE ImStb +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION + +#ifdef IMGUI_STB_NAMESPACE +namespace IMGUI_STB_NAMESPACE +{ +#endif + +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (disable: 6011) // (stb_rectpack) Dereferencing NULL pointer 'cur->next'. +#pragma warning (disable: 6385) // (stb_truetype) Reading invalid data from 'buffer': the readable size is '_Old_3`kernel_width' bytes, but '3' bytes may be read. +#pragma warning (disable: 28182) // (stb_rectpack) Dereferencing NULL pointer. 'cur' contains the same NULL value as 'cur->next' did. +#endif + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#endif + +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" // warning: this statement may fall through +#endif + +#ifndef STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBRP_STATIC +#define STBRP_ASSERT(x) do { IM_ASSERT(x); } while (0) +#define STBRP_SORT ImQsort +#define STB_RECT_PACK_IMPLEMENTATION +#endif +#ifdef IMGUI_STB_RECT_PACK_FILENAME +#include IMGUI_STB_RECT_PACK_FILENAME +#else +#include "imstb_rectpack.h" +#endif +#endif + +#ifdef IMGUI_ENABLE_STB_TRUETYPE +#ifndef STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in another compilation unit +#define STBTT_malloc(x,u) ((void)(u), IM_ALLOC(x)) +#define STBTT_free(x,u) ((void)(u), IM_FREE(x)) +#define STBTT_assert(x) do { IM_ASSERT(x); } while(0) +#define STBTT_fmod(x,y) ImFmod(x,y) +#define STBTT_sqrt(x) ImSqrt(x) +#define STBTT_pow(x,y) ImPow(x,y) +#define STBTT_fabs(x) ImFabs(x) +#define STBTT_ifloor(x) ((int)ImFloor(x)) +#define STBTT_iceil(x) ((int)ImCeil(x)) +#define STBTT_strlen(x) ImStrlen(x) +#define STBTT_STATIC +#define STB_TRUETYPE_IMPLEMENTATION +#else +#define STBTT_DEF extern +#endif +#ifdef IMGUI_STB_TRUETYPE_FILENAME +#include IMGUI_STB_TRUETYPE_FILENAME +#else +#include "imstb_truetype.h" +#endif +#endif +#endif // IMGUI_ENABLE_STB_TRUETYPE + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif + +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImStb +using namespace IMGUI_STB_NAMESPACE; +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Style functions +//----------------------------------------------------------------------------- + +void ImGui::StyleColorsDark(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + colors[ImGuiCol_Border] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.16f, 0.29f, 0.48f, 0.54f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.16f, 0.29f, 0.48f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.24f, 0.52f, 0.88f, 1.00f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.20f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.50f, 0.50f, 0.50f, 0.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavCursor] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); +} + +void ImGui::StyleColorsClassic(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); + colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.47f, 0.47f, 0.69f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.41f, 0.64f, 0.69f); + colors[ImGuiCol_TitleBg] = ImVec4(0.27f, 0.27f, 0.54f, 0.83f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.32f, 0.32f, 0.63f, 0.87f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.40f, 0.40f, 0.80f, 0.20f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.40f, 0.40f, 0.55f, 0.80f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.25f, 0.30f, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.40f, 0.40f, 0.80f, 0.30f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.40f, 0.80f, 0.40f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_CheckMark] = ImVec4(0.90f, 0.90f, 0.90f, 0.50f); + colors[ImGuiCol_SliderGrab] = ImVec4(1.00f, 1.00f, 1.00f, 0.30f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.41f, 0.39f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.35f, 0.40f, 0.61f, 0.62f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.40f, 0.48f, 0.71f, 0.79f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.46f, 0.54f, 0.80f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.40f, 0.40f, 0.90f, 0.45f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.45f, 0.45f, 0.90f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.53f, 0.53f, 0.87f, 0.80f); + colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.53f, 0.53f, 0.87f, 0.00f); + colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.27f, 0.27f, 0.38f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.45f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); + colors[ImGuiCol_NavCursor] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +// Those light colors are better suited with a thicker font than the default one + FrameBorder +void ImGui::StyleColorsLight(ImGuiStyle* dst) +{ + ImGuiStyle* style = dst ? dst : &ImGui::GetStyle(); + ImVec4* colors = style->Colors; + + colors[ImGuiCol_Text] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.94f, 0.94f, 0.94f, 1.00f); + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f); + colors[ImGuiCol_Border] = ImVec4(0.00f, 0.00f, 0.00f, 0.30f); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + colors[ImGuiCol_FrameBgHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_FrameBgActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_TitleBg] = ImVec4(0.96f, 0.96f, 0.96f, 1.00f); + colors[ImGuiCol_TitleBgActive] = ImVec4(0.82f, 0.82f, 0.82f, 1.00f); + colors[ImGuiCol_TitleBgCollapsed] = ImVec4(1.00f, 1.00f, 1.00f, 0.51f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.86f, 0.86f, 0.86f, 1.00f); + colors[ImGuiCol_ScrollbarBg] = ImVec4(0.98f, 0.98f, 0.98f, 0.53f); + colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.69f, 0.69f, 0.69f, 0.80f); + colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.49f, 0.49f, 0.49f, 0.80f); + colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.49f, 0.49f, 0.49f, 1.00f); + colors[ImGuiCol_CheckMark] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_SliderGrab] = ImVec4(0.26f, 0.59f, 0.98f, 0.78f); + colors[ImGuiCol_SliderGrabActive] = ImVec4(0.46f, 0.54f, 0.80f, 0.60f); + colors[ImGuiCol_Button] = ImVec4(0.26f, 0.59f, 0.98f, 0.40f); + colors[ImGuiCol_ButtonHovered] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_ButtonActive] = ImVec4(0.06f, 0.53f, 0.98f, 1.00f); + colors[ImGuiCol_Header] = ImVec4(0.26f, 0.59f, 0.98f, 0.31f); + colors[ImGuiCol_HeaderHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.80f); + colors[ImGuiCol_HeaderActive] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 0.62f); + colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); + colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.35f, 0.35f, 0.35f, 0.17f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); + colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.90f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.26f, 0.59f, 1.00f, 0.00f); + colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.78f, 0.87f, 0.98f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.57f, 0.57f, 0.64f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.68f, 0.68f, 0.74f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.30f, 0.30f, 0.30f, 0.09f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); + colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); + colors[ImGuiCol_NavCursor] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList +//----------------------------------------------------------------------------- + +ImDrawListSharedData::ImDrawListSharedData() +{ + memset(this, 0, sizeof(*this)); + InitialFringeScale = 1.0f; + for (int i = 0; i < IM_ARRAYSIZE(ArcFastVtx); i++) + { + const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(ArcFastVtx); + ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a)); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) +{ + if (CircleSegmentMaxError == max_error) + return; + + IM_ASSERT(max_error > 0.0f); + CircleSegmentMaxError = max_error; + for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) + { + const float radius = (float)i; + CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : IM_DRAWLIST_ARCFAST_SAMPLE_MAX); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +ImDrawList::ImDrawList(ImDrawListSharedData* shared_data) +{ + memset(this, 0, sizeof(*this)); + _Data = shared_data; +} + +ImDrawList::~ImDrawList() +{ + _ClearFreeMemory(); +} + +// Initialize before use in a new frame. We always have a command ready in the buffer. +// In the majority of cases, you would want to call PushClipRect() and PushTextureID() after this. +void ImDrawList::_ResetForNewFrame() +{ + // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. + IM_STATIC_ASSERT(offsetof(ImDrawCmd, ClipRect) == 0); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, TextureId) == sizeof(ImVec4)); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); + if (_Splitter._Count > 1) + _Splitter.Merge(this); + + CmdBuffer.resize(0); + IdxBuffer.resize(0); + VtxBuffer.resize(0); + Flags = _Data->InitialFlags; + memset(&_CmdHeader, 0, sizeof(_CmdHeader)); + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.resize(0); + _TextureIdStack.resize(0); + _CallbacksDataBuf.resize(0); + _Path.resize(0); + _Splitter.Clear(); + CmdBuffer.push_back(ImDrawCmd()); + _FringeScale = _Data->InitialFringeScale; +} + +void ImDrawList::_ClearFreeMemory() +{ + CmdBuffer.clear(); + IdxBuffer.clear(); + VtxBuffer.clear(); + Flags = ImDrawListFlags_None; + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.clear(); + _TextureIdStack.clear(); + _CallbacksDataBuf.clear(); + _Path.clear(); + _Splitter.ClearFreeMemory(); +} + +ImDrawList* ImDrawList::CloneOutput() const +{ + ImDrawList* dst = IM_NEW(ImDrawList(_Data)); + dst->CmdBuffer = CmdBuffer; + dst->IdxBuffer = IdxBuffer; + dst->VtxBuffer = VtxBuffer; + dst->Flags = Flags; + return dst; +} + +void ImDrawList::AddDrawCmd() +{ + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = _CmdHeader.ClipRect; // Same as calling ImDrawCmd_HeaderCopy() + draw_cmd.TextureId = _CmdHeader.TextureId; + draw_cmd.VtxOffset = _CmdHeader.VtxOffset; + draw_cmd.IdxOffset = IdxBuffer.Size; + + IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); + CmdBuffer.push_back(draw_cmd); +} + +// Pop trailing draw command (used before merging or presenting to user) +// Note that this leaves the ImDrawList in a state unfit for further commands, as most code assume that CmdBuffer.Size > 0 && CmdBuffer.back().UserCallback == NULL +void ImDrawList::_PopUnusedDrawCmd() +{ + while (CmdBuffer.Size > 0) + { + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 || curr_cmd->UserCallback != NULL) + return;// break; + CmdBuffer.pop_back(); + } +} + +void ImDrawList::AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size) +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + IM_ASSERT(curr_cmd->UserCallback == NULL); + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + } + + curr_cmd->UserCallback = callback; + if (userdata_size == 0) + { + // Store user data directly in command (no indirection) + curr_cmd->UserCallbackData = userdata; + curr_cmd->UserCallbackDataSize = 0; + curr_cmd->UserCallbackDataOffset = -1; + } + else + { + // Copy and store user data in a buffer + IM_ASSERT(userdata != NULL); + IM_ASSERT(userdata_size < (1u << 31)); + curr_cmd->UserCallbackData = NULL; // Will be resolved during Render() + curr_cmd->UserCallbackDataSize = (int)userdata_size; + curr_cmd->UserCallbackDataOffset = _CallbacksDataBuf.Size; + _CallbacksDataBuf.resize(_CallbacksDataBuf.Size + (int)userdata_size); + memcpy(_CallbacksDataBuf.Data + (size_t)curr_cmd->UserCallbackDataOffset, userdata, userdata_size); + } + + AddDrawCmd(); // Force a new command after us (see comment below) +} + +// Compare ClipRect, TextureId and VtxOffset with a single memcmp() +#define ImDrawCmd_HeaderSize (offsetof(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) +#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize)) // Compare ClipRect, TextureId, VtxOffset +#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize)) // Copy ClipRect, TextureId, VtxOffset +#define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset) + +// Try to merge two last draw commands +void ImDrawList::_TryMergeDrawCmds() +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (ImDrawCmd_HeaderCompare(curr_cmd, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && curr_cmd->UserCallback == NULL && prev_cmd->UserCallback == NULL) + { + prev_cmd->ElemCount += curr_cmd->ElemCount; + CmdBuffer.pop_back(); + } +} + +// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. +// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. +void ImDrawList::_OnChangedClipRect() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &_CmdHeader.ClipRect, sizeof(ImVec4)) != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + curr_cmd->ClipRect = _CmdHeader.ClipRect; +} + +void ImDrawList::_OnChangedTextureID() +{ + // If current command is used with different settings we need to add a new command + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != _CmdHeader.TextureId) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { + CmdBuffer.pop_back(); + return; + } + curr_cmd->TextureId = _CmdHeader.TextureId; +} + +void ImDrawList::_OnChangedVtxOffset() +{ + // We don't need to compare curr_cmd->VtxOffset != _CmdHeader.VtxOffset because we know it'll be different at the time we call this. + _VtxCurrentIdx = 0; + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + //IM_ASSERT(curr_cmd->VtxOffset != _CmdHeader.VtxOffset); // See #3349 + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + curr_cmd->VtxOffset = _CmdHeader.VtxOffset; +} + +int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const +{ + // Automatic segment count + const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy + if (radius_idx >= 0 && radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + return _Data->CircleSegmentCounts[radius_idx]; // Use cached value + else + return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); +} + +// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) +void ImDrawList::PushClipRect(const ImVec2& cr_min, const ImVec2& cr_max, bool intersect_with_current_clip_rect) +{ + ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); + if (intersect_with_current_clip_rect) + { + ImVec4 current = _CmdHeader.ClipRect; + if (cr.x < current.x) cr.x = current.x; + if (cr.y < current.y) cr.y = current.y; + if (cr.z > current.z) cr.z = current.z; + if (cr.w > current.w) cr.w = current.w; + } + cr.z = ImMax(cr.x, cr.z); + cr.w = ImMax(cr.y, cr.w); + + _ClipRectStack.push_back(cr); + _CmdHeader.ClipRect = cr; + _OnChangedClipRect(); +} + +void ImDrawList::PushClipRectFullScreen() +{ + PushClipRect(ImVec2(_Data->ClipRectFullscreen.x, _Data->ClipRectFullscreen.y), ImVec2(_Data->ClipRectFullscreen.z, _Data->ClipRectFullscreen.w)); +} + +void ImDrawList::PopClipRect() +{ + _ClipRectStack.pop_back(); + _CmdHeader.ClipRect = (_ClipRectStack.Size == 0) ? _Data->ClipRectFullscreen : _ClipRectStack.Data[_ClipRectStack.Size - 1]; + _OnChangedClipRect(); +} + +void ImDrawList::PushTextureID(ImTextureID texture_id) +{ + _TextureIdStack.push_back(texture_id); + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +void ImDrawList::PopTextureID() +{ + _TextureIdStack.pop_back(); + _CmdHeader.TextureId = (_TextureIdStack.Size == 0) ? (ImTextureID)NULL : _TextureIdStack.Data[_TextureIdStack.Size - 1]; + _OnChangedTextureID(); +} + +// This is used by ImGui::PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTextureID()/PopTextureID(). +void ImDrawList::_SetTextureID(ImTextureID texture_id) +{ + if (_CmdHeader.TextureId == texture_id) + return; + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +// Reserve space for a number of vertices and indices. +// You must finish filling your reserved data before calling PrimReserve() again, as it may reallocate or +// submit the intermediate results. PrimUnreserve() can be used to release unused allocations. +void ImDrawList::PrimReserve(int idx_count, int vtx_count) +{ + // Large mesh support (when enabled) + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + if (sizeof(ImDrawIdx) == 2 && (_VtxCurrentIdx + vtx_count >= (1 << 16)) && (Flags & ImDrawListFlags_AllowVtxOffset)) + { + // FIXME: In theory we should be testing that vtx_count <64k here. + // In practice, RenderText() relies on reserving ahead for a worst case scenario so it is currently useful for us + // to not make that check until we rework the text functions to handle clipping and large horizontal lines better. + _CmdHeader.VtxOffset = VtxBuffer.Size; + _OnChangedVtxOffset(); + } + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount += idx_count; + + int vtx_buffer_old_size = VtxBuffer.Size; + VtxBuffer.resize(vtx_buffer_old_size + vtx_count); + _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; +} + +// Release the number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). +void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) +{ + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount -= idx_count; + VtxBuffer.shrink(VtxBuffer.Size - vtx_count); + IdxBuffer.shrink(IdxBuffer.Size - idx_count); +} + +// Fully unrolled with inline call to keep our debug builds decently fast. +void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv(_Data->TexUvWhitePixel); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimRectUV(const ImVec2& a, const ImVec2& c, const ImVec2& uv_a, const ImVec2& uv_c, ImU32 col) +{ + ImVec2 b(c.x, a.y), d(a.x, c.y), uv_b(uv_c.x, uv_a.y), uv_d(uv_a.x, uv_c.y); + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col) +{ + ImDrawIdx idx = (ImDrawIdx)_VtxCurrentIdx; + _IdxWritePtr[0] = idx; _IdxWritePtr[1] = (ImDrawIdx)(idx+1); _IdxWritePtr[2] = (ImDrawIdx)(idx+2); + _IdxWritePtr[3] = idx; _IdxWritePtr[4] = (ImDrawIdx)(idx+2); _IdxWritePtr[5] = (ImDrawIdx)(idx+3); + _VtxWritePtr[0].pos = a; _VtxWritePtr[0].uv = uv_a; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = b; _VtxWritePtr[1].uv = uv_b; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = c; _VtxWritePtr[2].uv = uv_c; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = d; _VtxWritePtr[3].uv = uv_d; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + _VtxCurrentIdx += 4; + _IdxWritePtr += 6; +} + +// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superfluous function calls to optimize debug/non-inlined builds. +// - Those macros expects l-values and need to be used as their own statement. +// - Those macros are intentionally not surrounded by the 'do {} while (0)' idiom because even that translates to runtime with debug compilers. +#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = ImRsqrt(d2); VX *= inv_len; VY *= inv_len; } } (void)0 +#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366) +#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0 + +// TODO: Thickness anti-aliased lines cap are missing their AA fringe. +// We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds. +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness) +{ + if (points_count < 2 || (col & IM_COL32_A_MASK) == 0) + return; + + const bool closed = (flags & ImDrawFlags_Closed) != 0; + const ImVec2 opaque_uv = _Data->TexUvWhitePixel; + const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw + const bool thick_line = (thickness > _FringeScale); + + if (Flags & ImDrawListFlags_AntiAliasedLines) + { + // Anti-aliased stroke + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + // Thicknesses <1.0 should behave like thickness 1.0 + thickness = ImMax(thickness, 1.0f); + const int integer_thickness = (int)thickness; + const float fractional_thickness = thickness - integer_thickness; + + // Do we want to draw this line using a texture? + // - For now, only draw integer-width lines using textures to avoid issues with the way scaling occurs, could be improved. + // - If AA_SIZE is not 1.0f we cannot use the texture path. + const bool use_texture = (Flags & ImDrawListFlags_AntiAliasedLinesUseTex) && (integer_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX) && (fractional_thickness <= 0.00001f) && (AA_SIZE == 1.0f); + + // We should never hit this, because NewFrame() doesn't set ImDrawListFlags_AntiAliasedLinesUseTex unless ImFontAtlasFlags_NoBakedLines is off + IM_ASSERT_PARANOID(!use_texture || !(_Data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)); + + const int idx_count = use_texture ? (count * 6) : (thick_line ? count * 18 : count * 12); + const int vtx_count = use_texture ? (points_count * 2) : (thick_line ? points_count * 4 : points_count * 3); + PrimReserve(idx_count, vtx_count); + + // Temporary buffer + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + _Data->TempBuffer.reserve_discard(points_count * ((use_texture || !thick_line) ? 3 : 5)); + ImVec2* temp_normals = _Data->TempBuffer.Data; + ImVec2* temp_points = temp_normals + points_count; + + // Calculate normals (tangents) for each line segment + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + float dx = points[i2].x - points[i1].x; + float dy = points[i2].y - points[i1].y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i1].x = dy; + temp_normals[i1].y = -dx; + } + if (!closed) + temp_normals[points_count - 1] = temp_normals[points_count - 2]; + + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (use_texture || !thick_line) + { + // [PATH 1] Texture-based lines (thick or non-thick) + // [PATH 2] Non texture-based lines (non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA. + // - In the texture-based path, we don't use AA_SIZE here because the +1 is tied to the generated texture + // (see ImFontAtlasBuildRenderLinesTexData() function), and so alternate values won't work without changes to that code. + // - In the non texture-based paths, we would allow AA_SIZE to potentially be != 1.0f with a patch (e.g. fringe_scale patch to + // allow scaling geometry while preserving one-screen-pixel AA fringe). + const float half_draw_size = use_texture ? ((thickness * 0.5f) + 1) : AA_SIZE; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * half_draw_size; + temp_points[1] = points[0] - temp_normals[0] * half_draw_size; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * half_draw_size; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * half_draw_size; + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; // i2 is the second point of the line segment + const unsigned int idx2 = ((i1 + 1) == points_count) ? _VtxCurrentIdx : (idx1 + (use_texture ? 2 : 3)); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= half_draw_size; // dm_x, dm_y are offset to the outer edge of the AA area + dm_y *= half_draw_size; + + // Add temporary vertexes for the outer edges + ImVec2* out_vtx = &temp_points[i2 * 2]; + out_vtx[0].x = points[i2].x + dm_x; + out_vtx[0].y = points[i2].y + dm_y; + out_vtx[1].x = points[i2].x - dm_x; + out_vtx[1].y = points[i2].y - dm_y; + + if (use_texture) + { + // Add indices for two triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 1); // Right tri + _IdxWritePtr[3] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[4] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Left tri + _IdxWritePtr += 6; + } + else + { + // Add indexes for four triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); // Right tri 1 + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Right tri 2 + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); // Left tri 1 + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); // Left tri 2 + _IdxWritePtr += 12; + } + + idx1 = idx2; + } + + // Add vertexes for each point on the line + if (use_texture) + { + // If we're using textures we only need to emit the left/right edge vertices + ImVec4 tex_uvs = _Data->TexUvLines[integer_thickness]; + /*if (fractional_thickness != 0.0f) // Currently always zero when use_texture==false! + { + const ImVec4 tex_uvs_1 = _Data->TexUvLines[integer_thickness + 1]; + tex_uvs.x = tex_uvs.x + (tex_uvs_1.x - tex_uvs.x) * fractional_thickness; // inlined ImLerp() + tex_uvs.y = tex_uvs.y + (tex_uvs_1.y - tex_uvs.y) * fractional_thickness; + tex_uvs.z = tex_uvs.z + (tex_uvs_1.z - tex_uvs.z) * fractional_thickness; + tex_uvs.w = tex_uvs.w + (tex_uvs_1.w - tex_uvs.w) * fractional_thickness; + }*/ + ImVec2 tex_uv0(tex_uvs.x, tex_uvs.y); + ImVec2 tex_uv1(tex_uvs.z, tex_uvs.w); + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 2 + 0]; _VtxWritePtr[0].uv = tex_uv0; _VtxWritePtr[0].col = col; // Left-side outer edge + _VtxWritePtr[1].pos = temp_points[i * 2 + 1]; _VtxWritePtr[1].uv = tex_uv1; _VtxWritePtr[1].col = col; // Right-side outer edge + _VtxWritePtr += 2; + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; // Center of line + _VtxWritePtr[1].pos = temp_points[i * 2 + 0]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col_trans; // Left-side outer edge + _VtxWritePtr[2].pos = temp_points[i * 2 + 1]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col_trans; // Right-side outer edge + _VtxWritePtr += 3; + } + } + } + else + { + // [PATH 2] Non texture-based lines (thick): we need to draw the solid line core and thus require four vertices per point + const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend + if (!closed) + { + const int points_last = points_count - 1; + temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); + temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); + temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 0] = points[points_last] + temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 1] = points[points_last] + temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 2] = points[points_last] - temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 3] = points[points_last] - temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + } + + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment + { + const int i2 = (i1 + 1) == points_count ? 0 : (i1 + 1); // i2 is the second point of the line segment + const unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : (idx1 + 4); // Vertex index for end of segment + + // Average normals + float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; + float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + float dm_out_x = dm_x * (half_inner_thickness + AA_SIZE); + float dm_out_y = dm_y * (half_inner_thickness + AA_SIZE); + float dm_in_x = dm_x * half_inner_thickness; + float dm_in_y = dm_y * half_inner_thickness; + + // Add temporary vertices + ImVec2* out_vtx = &temp_points[i2 * 4]; + out_vtx[0].x = points[i2].x + dm_out_x; + out_vtx[0].y = points[i2].y + dm_out_y; + out_vtx[1].x = points[i2].x + dm_in_x; + out_vtx[1].y = points[i2].y + dm_in_y; + out_vtx[2].x = points[i2].x - dm_in_x; + out_vtx[2].y = points[i2].y - dm_in_y; + out_vtx[3].x = points[i2].x - dm_out_x; + out_vtx[3].y = points[i2].y - dm_out_y; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[13] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[14] = (ImDrawIdx)(idx1 + 3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1 + 3); _IdxWritePtr[16] = (ImDrawIdx)(idx2 + 3); _IdxWritePtr[17] = (ImDrawIdx)(idx2 + 2); + _IdxWritePtr += 18; + + idx1 = idx2; + } + + // Add vertices + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 4 + 0]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i * 4 + 1]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i * 4 + 2]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i * 4 + 3]; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr += 4; + } + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // [PATH 4] Non texture-based, Non anti-aliased lines + const int idx_count = count * 6; + const int vtx_count = count * 4; // FIXME-OPT: Not sharing edges + PrimReserve(idx_count, vtx_count); + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; + const ImVec2& p1 = points[i1]; + const ImVec2& p2 = points[i2]; + + float dx = p2.x - p1.x; + float dy = p2.y - p1.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + dx *= (thickness * 0.5f); + dy *= (thickness * 0.5f); + + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + 2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx + 2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx + 3); + _IdxWritePtr += 6; + _VtxCurrentIdx += 4; + } + } +} + +// - We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. +// - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2)*3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + ((i - 1) << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (i << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2)*3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + i - 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + i); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +void ImDrawList::_PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + // Calculate arc auto segment step size + if (a_step <= 0) + a_step = IM_DRAWLIST_ARCFAST_SAMPLE_MAX / _CalcCircleAutoSegmentCount(radius); + + // Make sure we never do steps larger than one quarter of the circle + a_step = ImClamp(a_step, 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); + + const int sample_range = ImAbs(a_max_sample - a_min_sample); + const int a_next_step = a_step; + + int samples = sample_range + 1; + bool extra_max_sample = false; + if (a_step > 1) + { + samples = sample_range / a_step + 1; + const int overstep = sample_range % a_step; + + if (overstep > 0) + { + extra_max_sample = true; + samples++; + + // When we have overstep to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + if (sample_range > 0) + a_step -= (a_step - overstep) / 2; + } + } + + _Path.resize(_Path.Size + samples); + ImVec2* out_ptr = _Path.Data + (_Path.Size - samples); + + int sample_index = a_min_sample; + if (sample_index < 0 || sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + { + sample_index = sample_index % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + } + + if (a_max_sample >= a_min_sample) + { + for (int a = a_min_sample; a <= a_max_sample; a += a_step, sample_index += a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + sample_index -= IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + else + { + for (int a = a_min_sample; a >= a_max_sample; a -= a_step, sample_index -= a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + + if (extra_max_sample) + { + int normalized_max_sample = a_max_sample % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (normalized_max_sample < 0) + normalized_max_sample += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[normalized_max_sample]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + + IM_ASSERT_PARANOID(_Path.Data + _Path.Size == out_ptr); +} + +void ImDrawList::_PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + // Note that we are adding a point at both a_min and a_max. + // If you are trying to draw a full closed circle you don't want the overlapping points! + _Path.reserve(_Path.Size + (num_segments + 1)); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + _Path.push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius)); + } +} + +// 0: East, 3: South, 6: West, 9: North, 12: East +void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + _PathArcToFastEx(center, radius, a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0); +} + +void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + if (num_segments > 0) + { + _PathArcToN(center, radius, a_min, a_max, num_segments); + return; + } + + // Automatic segment count + if (radius <= _Data->ArcFastRadiusCutoff) + { + const bool a_is_reverse = a_max < a_min; + + // We are going to use precomputed values for mid samples. + // Determine first and last sample in lookup table that belong to the arc. + const float a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0f); + const float a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0f); + + const int a_min_sample = a_is_reverse ? (int)ImFloor(a_min_sample_f) : (int)ImCeil(a_min_sample_f); + const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloor(a_max_sample_f); + const int a_mid_samples = a_is_reverse ? ImMax(a_min_sample - a_max_sample, 0) : ImMax(a_max_sample - a_min_sample, 0); + + const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const float a_max_segment_angle = a_max_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const bool a_emit_start = ImAbs(a_min_segment_angle - a_min) >= 1e-5f; + const bool a_emit_end = ImAbs(a_max - a_max_segment_angle) >= 1e-5f; + + _Path.reserve(_Path.Size + (a_mid_samples + 1 + (a_emit_start ? 1 : 0) + (a_emit_end ? 1 : 0))); + if (a_emit_start) + _Path.push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius)); + if (a_mid_samples > 0) + _PathArcToFastEx(center, radius, a_min_sample, a_max_sample, 0); + if (a_emit_end) + _Path.push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius)); + } + else + { + const float arc_length = ImAbs(a_max - a_min); + const int circle_segment_count = _CalcCircleAutoSegmentCount(radius); + const int arc_segment_count = ImMax((int)ImCeil(circle_segment_count * arc_length / (IM_PI * 2.0f)), (int)(2.0f * IM_PI / arc_length)); + _PathArcToN(center, radius, a_min, a_max, arc_segment_count); + } +} + +void ImDrawList::PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments) +{ + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + _Path.reserve(_Path.Size + (num_segments + 1)); + + const float cos_rot = ImCos(rot); + const float sin_rot = ImSin(rot); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + ImVec2 point(ImCos(a) * radius.x, ImSin(a) * radius.y); + const ImVec2 rel((point.x * cos_rot) - (point.y * sin_rot), (point.x * sin_rot) + (point.y * cos_rot)); + point.x = rel.x + center.x; + point.y = rel.y + center.y; + _Path.push_back(point); + } +} + +ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t) +{ + float u = 1.0f - t; + float w1 = u * u * u; + float w2 = 3 * u * u * t; + float w3 = 3 * u * t * t; + float w4 = t * t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y); +} + +ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t) +{ + float u = 1.0f - t; + float w1 = u * u; + float w2 = 2 * u * t; + float w3 = t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x, w1 * p1.y + w2 * p2.y + w3 * p3.y); +} + +// Closely mimics ImBezierCubicClosestPointCasteljau() in imgui.cpp +static void PathBezierCubicCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = (x2 - x4) * dy - (y2 - y4) * dx; + float d3 = (x3 - x4) * dy - (y3 - y4) * dx; + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x4, y4)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x34 = (x3 + x4) * 0.5f, y34 = (y3 + y4) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + float x234 = (x23 + x34) * 0.5f, y234 = (y23 + y34) * 0.5f; + float x1234 = (x123 + x234) * 0.5f, y1234 = (y123 + y234) * 0.5f; + PathBezierCubicCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + PathBezierCubicCurveToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +static void PathBezierQuadraticCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float tess_tol, int level) +{ + float dx = x3 - x1, dy = y3 - y1; + float det = (x2 - x3) * dy - (y2 - y3) * dx; + if (det * det * 4.0f < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x3, y3)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + PathBezierQuadraticCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, tess_tol, level + 1); + PathBezierQuadraticCurveToCasteljau(path, x123, y123, x23, y23, x3, y3, tess_tol, level + 1); + } +} + +void ImDrawList::PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierCubicCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); // Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step)); + } +} + +void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierQuadraticCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, _Data->CurveTessellationTol, 0);// Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierQuadraticCalc(p1, p2, p3, t_step * i_step)); + } +} + +static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) +{ + /* + IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Obsoleted in 1.82 (from February 2021). This code was stripped/simplified and mostly commented in 1.90 (from September 2023) + // - Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) + if (flags == ~0) { return ImDrawFlags_RoundCornersAll; } + // - Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations). Read details in older version of this code. + if (flags >= 0x01 && flags <= 0x0F) { return (flags << 4); } + // We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f' +#endif + */ + // If this assert triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. + // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc. anyway. + // See details in 1.82 Changelog as well as 2021/03/12 and 2023/09/08 entries in "API BREAKING CHANGES" section. + IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); + + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + return flags; +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) +{ + if (rounding >= 0.5f) + { + flags = FixRectCornerFlags(flags); + rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + } + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PathLineTo(a); + PathLineTo(ImVec2(b.x, a.y)); + PathLineTo(b); + PathLineTo(ImVec2(a.x, b.y)); + } + else + { + const float rounding_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? rounding : 0.0f; + const float rounding_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? rounding : 0.0f; + const float rounding_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? rounding : 0.0f; + const float rounding_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? rounding : 0.0f; + PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); + PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); + PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); + PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); + } +} + +void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + PathLineTo(p1 + ImVec2(0.5f, 0.5f)); + PathLineTo(p2 + ImVec2(0.5f, 0.5f)); + PathStroke(col, 0, thickness); +} + +// p_min = upper-left, p_max = lower-right +// Note we don't render 1 pixels sized rectangles properly. +void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (Flags & ImDrawListFlags_AntiAliasedLines) + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags); + else + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + PrimReserve(6, 4); + PrimRect(p_min, p_max, col); + } + else + { + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + } +} + +// p_min = upper-left, p_max = lower-right +void ImDrawList::AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left) +{ + if (((col_upr_left | col_upr_right | col_bot_right | col_bot_left) & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + PrimReserve(6, 4); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 3)); + PrimWriteVtx(p_min, uv, col_upr_left); + PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv, col_upr_right); + PrimWriteVtx(p_max, uv, col_bot_right); + PrimWriteVtx(ImVec2(p_min.x, p_max.y), uv, col_bot_left); +} + +void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathLineTo(p4); + PathFillConvex(col); +} + +void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathLineTo(p2); + PathLineTo(p3); + PathFillConvex(col); +} + +void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius - 0.5f, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + } + + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + } + + PathFillConvex(col); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) + return; + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Ellipse +void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Cubic Bezier takes 4 controls points +void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierCubicCurveTo(p2, p3, p4, num_segments); + PathStroke(col, 0, thickness); +} + +// Quadratic Bezier takes 3 controls points +void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierQuadraticCurveTo(p2, p3, num_segments); + PathStroke(col, 0, thickness); +} + +void ImDrawList::AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + // Accept null ranges + if (text_begin == text_end || text_begin[0] == 0) + return; + // No need to strlen() here: font->RenderText() will do it and may early out. + + // Pull default font/size from the shared ImDrawListSharedData instance + if (font == NULL) + font = _Data->Font; + if (font_size == 0.0f) + font_size = _Data->FontSize; + + IM_ASSERT(font->ContainerAtlas->TexID == _CmdHeader.TextureId); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. + + ImVec4 clip_rect = _CmdHeader.ClipRect; + if (cpu_fine_clip_rect) + { + clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); + clip_rect.y = ImMax(clip_rect.y, cpu_fine_clip_rect->y); + clip_rect.z = ImMin(clip_rect.z, cpu_fine_clip_rect->z); + clip_rect.w = ImMin(clip_rect.w, cpu_fine_clip_rect->w); + } + font->RenderText(this, font_size, pos, col, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL); +} + +void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +{ + AddText(_Data->Font, _Data->FontSize, pos, col, text_begin, text_end); +} + +void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimRectUV(p_min, p_max, uv_min, uv_max, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1, const ImVec2& uv2, const ImVec2& uv3, const ImVec2& uv4, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + PrimReserve(6, 4); + PrimQuadUV(p1, p2, p3, p4, uv1, uv2, uv3, uv4, col); + + if (push_texture_id) + PopTextureID(); +} + +void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + flags = FixRectCornerFlags(flags); + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) + { + AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); + return; + } + + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; + if (push_texture_id) + PushTextureID(user_texture_id); + + int vert_start_idx = VtxBuffer.Size; + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + int vert_end_idx = VtxBuffer.Size; + ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true); + + if (push_texture_id) + PopTextureID(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +//----------------------------------------------------------------------------- +// Triangulate concave polygons. Based on "Triangulation by Ear Clipping" paper, O(N^2) complexity. +// Reference: https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf +// Provided as a convenience for user but not used by main library. +//----------------------------------------------------------------------------- +// - ImTriangulator [Internal] +// - AddConcavePolyFilled() +//----------------------------------------------------------------------------- + +enum ImTriangulatorNodeType +{ + ImTriangulatorNodeType_Convex, + ImTriangulatorNodeType_Ear, + ImTriangulatorNodeType_Reflex +}; + +struct ImTriangulatorNode +{ + ImTriangulatorNodeType Type; + int Index; + ImVec2 Pos; + ImTriangulatorNode* Next; + ImTriangulatorNode* Prev; + + void Unlink() { Next->Prev = Prev; Prev->Next = Next; } +}; + +struct ImTriangulatorNodeSpan +{ + ImTriangulatorNode** Data = NULL; + int Size = 0; + + void push_back(ImTriangulatorNode* node) { Data[Size++] = node; } + void find_erase_unsorted(int idx) { for (int i = Size - 1; i >= 0; i--) if (Data[i]->Index == idx) { Data[i] = Data[Size - 1]; Size--; return; } } +}; + +struct ImTriangulator +{ + static int EstimateTriangleCount(int points_count) { return (points_count < 3) ? 0 : points_count - 2; } + static int EstimateScratchBufferSize(int points_count) { return sizeof(ImTriangulatorNode) * points_count + sizeof(ImTriangulatorNode*) * points_count * 2; } + + void Init(const ImVec2* points, int points_count, void* scratch_buffer); + void GetNextTriangle(unsigned int out_triangle[3]); // Return relative indexes for next triangle + + // Internal functions + void BuildNodes(const ImVec2* points, int points_count); + void BuildReflexes(); + void BuildEars(); + void FlipNodeList(); + bool IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const; + void ReclassifyNode(ImTriangulatorNode* node); + + // Internal members + int _TrianglesLeft = 0; + ImTriangulatorNode* _Nodes = NULL; + ImTriangulatorNodeSpan _Ears; + ImTriangulatorNodeSpan _Reflexes; +}; + +// Distribute storage for nodes, ears and reflexes. +// FIXME-OPT: if everything is convex, we could report it to caller and let it switch to an convex renderer +// (this would require first building reflexes to bail to convex if empty, without even building nodes) +void ImTriangulator::Init(const ImVec2* points, int points_count, void* scratch_buffer) +{ + IM_ASSERT(scratch_buffer != NULL && points_count >= 3); + _TrianglesLeft = EstimateTriangleCount(points_count); + _Nodes = (ImTriangulatorNode*)scratch_buffer; // points_count x Node + _Ears.Data = (ImTriangulatorNode**)(_Nodes + points_count); // points_count x Node* + _Reflexes.Data = (ImTriangulatorNode**)(_Nodes + points_count) + points_count; // points_count x Node* + BuildNodes(points, points_count); + BuildReflexes(); + BuildEars(); +} + +void ImTriangulator::BuildNodes(const ImVec2* points, int points_count) +{ + for (int i = 0; i < points_count; i++) + { + _Nodes[i].Type = ImTriangulatorNodeType_Convex; + _Nodes[i].Index = i; + _Nodes[i].Pos = points[i]; + _Nodes[i].Next = _Nodes + i + 1; + _Nodes[i].Prev = _Nodes + i - 1; + } + _Nodes[0].Prev = _Nodes + points_count - 1; + _Nodes[points_count - 1].Next = _Nodes; +} + +void ImTriangulator::BuildReflexes() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (ImTriangleIsClockwise(n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Reflex; + _Reflexes.push_back(n1); + } +} + +void ImTriangulator::BuildEars() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (n1->Type != ImTriangulatorNodeType_Convex) + continue; + if (!IsEar(n1->Prev->Index, n1->Index, n1->Next->Index, n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Ear; + _Ears.push_back(n1); + } +} + +void ImTriangulator::GetNextTriangle(unsigned int out_triangle[3]) +{ + if (_Ears.Size == 0) + { + FlipNodeList(); + + ImTriangulatorNode* node = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, node = node->Next) + node->Type = ImTriangulatorNodeType_Convex; + _Reflexes.Size = 0; + BuildReflexes(); + BuildEars(); + + // If we still don't have ears, it means geometry is degenerated. + if (_Ears.Size == 0) + { + // Return first triangle available, mimicking the behavior of convex fill. + IM_ASSERT(_TrianglesLeft > 0); // Geometry is degenerated + _Ears.Data[0] = _Nodes; + _Ears.Size = 1; + } + } + + ImTriangulatorNode* ear = _Ears.Data[--_Ears.Size]; + out_triangle[0] = ear->Prev->Index; + out_triangle[1] = ear->Index; + out_triangle[2] = ear->Next->Index; + + ear->Unlink(); + if (ear == _Nodes) + _Nodes = ear->Next; + + ReclassifyNode(ear->Prev); + ReclassifyNode(ear->Next); + _TrianglesLeft--; +} + +void ImTriangulator::FlipNodeList() +{ + ImTriangulatorNode* prev = _Nodes; + ImTriangulatorNode* temp = _Nodes; + ImTriangulatorNode* current = _Nodes->Next; + prev->Next = prev; + prev->Prev = prev; + while (current != _Nodes) + { + temp = current->Next; + + current->Next = prev; + prev->Prev = current; + _Nodes->Next = current; + current->Prev = _Nodes; + + prev = current; + current = temp; + } + _Nodes = prev; +} + +// A triangle is an ear is no other vertex is inside it. We can test reflexes vertices only (see reference algorithm) +bool ImTriangulator::IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const +{ + ImTriangulatorNode** p_end = _Reflexes.Data + _Reflexes.Size; + for (ImTriangulatorNode** p = _Reflexes.Data; p < p_end; p++) + { + ImTriangulatorNode* reflex = *p; + if (reflex->Index != i0 && reflex->Index != i1 && reflex->Index != i2) + if (ImTriangleContainsPoint(v0, v1, v2, reflex->Pos)) + return false; + } + return true; +} + +void ImTriangulator::ReclassifyNode(ImTriangulatorNode* n1) +{ + // Classify node + ImTriangulatorNodeType type; + const ImTriangulatorNode* n0 = n1->Prev; + const ImTriangulatorNode* n2 = n1->Next; + if (!ImTriangleIsClockwise(n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Reflex; + else if (IsEar(n0->Index, n1->Index, n2->Index, n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Ear; + else + type = ImTriangulatorNodeType_Convex; + + // Update lists when a type changes + if (type == n1->Type) + return; + if (n1->Type == ImTriangulatorNodeType_Reflex) + _Reflexes.find_erase_unsorted(n1->Index); + else if (n1->Type == ImTriangulatorNodeType_Ear) + _Ears.find_erase_unsorted(n1->Index); + if (type == ImTriangulatorNodeType_Reflex) + _Reflexes.push_back(n1); + else if (type == ImTriangulatorNodeType_Ear) + _Ears.push_back(n1); + n1->Type = type; +} + +// Use ear-clipping algorithm to triangulate a simple polygon (no self-interaction, no holes). +// (Reminder: we don't perform any coarse clipping/culling in ImDrawList layer! +// It is up to caller to ensure not making costly calls that will be outside of visible area. +// As concave fill is noticeably more expensive than other primitives, be mindful of this... +// Caller can build AABB of points, and avoid filling if 'draw_list->_CmdHeader.ClipRect.Overlays(points_bb) == false') +void ImDrawList::AddConcavePolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + ImTriangulator triangulator; + unsigned int triangle[3]; + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2) * 3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (triangle[0] << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (triangle[1] << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (triangle[2] << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2) * 3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx + triangle[0]); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + triangle[1]); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + triangle[2]); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawListSplitter +//----------------------------------------------------------------------------- +// FIXME: This may be a little confusing, trying to be a little too low-level/optimal instead of just doing vector swap.. +//----------------------------------------------------------------------------- + +void ImDrawListSplitter::ClearFreeMemory() +{ + for (int i = 0; i < _Channels.Size; i++) + { + if (i == _Current) + memset(&_Channels[i], 0, sizeof(_Channels[i])); // Current channel is a copy of CmdBuffer/IdxBuffer, don't destruct again + _Channels[i]._CmdBuffer.clear(); + _Channels[i]._IdxBuffer.clear(); + } + _Current = 0; + _Count = 1; + _Channels.clear(); +} + +void ImDrawListSplitter::Split(ImDrawList* draw_list, int channels_count) +{ + IM_UNUSED(draw_list); + IM_ASSERT(_Current == 0 && _Count <= 1 && "Nested channel splitting is not supported. Please use separate instances of ImDrawListSplitter."); + int old_channels_count = _Channels.Size; + if (old_channels_count < channels_count) + { + _Channels.reserve(channels_count); // Avoid over reserving since this is likely to stay stable + _Channels.resize(channels_count); + } + _Count = channels_count; + + // Channels[] (24/32 bytes each) hold storage that we'll swap with draw_list->_CmdBuffer/_IdxBuffer + // The content of Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. + // When we switch to the next channel, we'll copy draw_list->_CmdBuffer/_IdxBuffer into Channels[0] and then Channels[1] into draw_list->CmdBuffer/_IdxBuffer + memset(&_Channels[0], 0, sizeof(ImDrawChannel)); + for (int i = 1; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&_Channels[i]) ImDrawChannel(); + } + else + { + _Channels[i]._CmdBuffer.resize(0); + _Channels[i]._IdxBuffer.resize(0); + } + } +} + +void ImDrawListSplitter::Merge(ImDrawList* draw_list) +{ + // Note that we never use or rely on _Channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + if (_Count <= 1) + return; + + SetCurrentChannel(draw_list, 0); + draw_list->_PopUnusedDrawCmd(); + + // Calculate our final buffer sizes. Also fix the incorrect IdxOffset values in each command. + int new_cmd_buffer_count = 0; + int new_idx_buffer_count = 0; + ImDrawCmd* last_cmd = (_Count > 0 && draw_list->CmdBuffer.Size > 0) ? &draw_list->CmdBuffer.back() : NULL; + int idx_offset = last_cmd ? last_cmd->IdxOffset + last_cmd->ElemCount : 0; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (ch._CmdBuffer.Size > 0 && ch._CmdBuffer.back().ElemCount == 0 && ch._CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() + ch._CmdBuffer.pop_back(); + + if (ch._CmdBuffer.Size > 0 && last_cmd != NULL) + { + // Do not include ImDrawCmd_AreSequentialIdxOffset() in the compare as we rebuild IdxOffset values ourselves. + // Manipulating IdxOffset (e.g. by reordering draw commands like done by RenderDimmedBackgroundBehindWindow()) is not supported within a splitter. + ImDrawCmd* next_cmd = &ch._CmdBuffer[0]; + if (ImDrawCmd_HeaderCompare(last_cmd, next_cmd) == 0 && last_cmd->UserCallback == NULL && next_cmd->UserCallback == NULL) + { + // Merge previous channel last draw command with current channel first draw command if matching. + last_cmd->ElemCount += next_cmd->ElemCount; + idx_offset += next_cmd->ElemCount; + ch._CmdBuffer.erase(ch._CmdBuffer.Data); // FIXME-OPT: Improve for multiple merges. + } + } + if (ch._CmdBuffer.Size > 0) + last_cmd = &ch._CmdBuffer.back(); + new_cmd_buffer_count += ch._CmdBuffer.Size; + new_idx_buffer_count += ch._IdxBuffer.Size; + for (int cmd_n = 0; cmd_n < ch._CmdBuffer.Size; cmd_n++) + { + ch._CmdBuffer.Data[cmd_n].IdxOffset = idx_offset; + idx_offset += ch._CmdBuffer.Data[cmd_n].ElemCount; + } + } + draw_list->CmdBuffer.resize(draw_list->CmdBuffer.Size + new_cmd_buffer_count); + draw_list->IdxBuffer.resize(draw_list->IdxBuffer.Size + new_idx_buffer_count); + + // Write commands and indices in order (they are fairly small structures, we don't copy vertices only indices) + ImDrawCmd* cmd_write = draw_list->CmdBuffer.Data + draw_list->CmdBuffer.Size - new_cmd_buffer_count; + ImDrawIdx* idx_write = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size - new_idx_buffer_count; + for (int i = 1; i < _Count; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (int sz = ch._CmdBuffer.Size) { memcpy(cmd_write, ch._CmdBuffer.Data, sz * sizeof(ImDrawCmd)); cmd_write += sz; } + if (int sz = ch._IdxBuffer.Size) { memcpy(idx_write, ch._IdxBuffer.Data, sz * sizeof(ImDrawIdx)); idx_write += sz; } + } + draw_list->_IdxWritePtr = idx_write; + + // Ensure there's always a non-callback draw command trailing the command-buffer + if (draw_list->CmdBuffer.Size == 0 || draw_list->CmdBuffer.back().UserCallback != NULL) + draw_list->AddDrawCmd(); + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); + + _Count = 1; +} + +void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) +{ + IM_ASSERT(idx >= 0 && idx < _Count); + if (_Current == idx) + return; + + // Overwrite ImVector (12/16 bytes), four times. This is merely a silly optimization instead of doing .swap() + memcpy(&_Channels.Data[_Current]._CmdBuffer, &draw_list->CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&_Channels.Data[_Current]._IdxBuffer, &draw_list->IdxBuffer, sizeof(draw_list->IdxBuffer)); + _Current = idx; + memcpy(&draw_list->CmdBuffer, &_Channels.Data[idx]._CmdBuffer, sizeof(draw_list->CmdBuffer)); + memcpy(&draw_list->IdxBuffer, &_Channels.Data[idx]._IdxBuffer, sizeof(draw_list->IdxBuffer)); + draw_list->_IdxWritePtr = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size; + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = (draw_list->CmdBuffer.Size == 0) ? NULL : &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd == NULL) + draw_list->AddDrawCmd(); + else if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawData +//----------------------------------------------------------------------------- + +void ImDrawData::Clear() +{ + Valid = false; + CmdListsCount = TotalIdxCount = TotalVtxCount = 0; + CmdLists.resize(0); // The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them. + DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.0f, 0.0f); + OwnerViewport = NULL; +} + +// Important: 'out_list' is generally going to be draw_data->CmdLists, but may be another temporary list +// as long at it is expected that the result will be later merged into draw_data->CmdLists[]. +void ImGui::AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.Size == 0) + return; + if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) + return; + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. + // May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. + // Be mindful that the lower-level ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. + // - If you want large meshes with more than 64K vertices, you can either: + // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. + // Most example backends already support this from 1.71. Pre-1.71 backends won't. + // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. + // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. + // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. + // 2 and 4 bytes indices are generally supported by most graphics API. + // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching + // the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + // Resolve callback data pointers + if (draw_list->_CallbacksDataBuf.Size > 0) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + if (cmd.UserCallback != NULL && cmd.UserCallbackDataOffset != -1 && cmd.UserCallbackDataSize > 0) + cmd.UserCallbackData = draw_list->_CallbacksDataBuf.Data + cmd.UserCallbackDataOffset; + + // Add to output list + records state in ImDrawData + out_list->push_back(draw_list); + draw_data->CmdListsCount++; + draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; + draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; +} + +void ImDrawData::AddDrawList(ImDrawList* draw_list) +{ + IM_ASSERT(CmdLists.Size == CmdListsCount); + draw_list->_PopUnusedDrawCmd(); + ImGui::AddDrawListToDrawDataEx(this, &CmdLists, draw_list); +} + +// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! +void ImDrawData::DeIndexAllBuffers() +{ + ImVector new_vtx_buffer; + TotalVtxCount = TotalIdxCount = 0; + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + if (cmd_list->IdxBuffer.empty()) + continue; + new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); + for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) + new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; + cmd_list->VtxBuffer.swap(new_vtx_buffer); + cmd_list->IdxBuffer.resize(0); + TotalVtxCount += cmd_list->VtxBuffer.Size; + } +} + +// Helper to scale the ClipRect field of each ImDrawCmd. +// Use if your final output buffer is at a different scale than draw_data->DisplaySize, +// or if there is a difference between your window resolution and framebuffer resolution. +void ImDrawData::ScaleClipRects(const ImVec2& fb_scale) +{ + for (ImDrawList* draw_list : CmdLists) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + cmd.ClipRect = ImVec4(cmd.ClipRect.x * fb_scale.x, cmd.ClipRect.y * fb_scale.y, cmd.ClipRect.z * fb_scale.x, cmd.ClipRect.w * fb_scale.y); +} + +//----------------------------------------------------------------------------- +// [SECTION] Helpers ShadeVertsXXX functions +//----------------------------------------------------------------------------- + +// Generic linear color gradient, write to RGB fields, leave A untouched. +void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) +{ + ImVec2 gradient_extent = gradient_p1 - gradient_p0; + float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + const int col0_r = (int)(col0 >> IM_COL32_R_SHIFT) & 0xFF; + const int col0_g = (int)(col0 >> IM_COL32_G_SHIFT) & 0xFF; + const int col0_b = (int)(col0 >> IM_COL32_B_SHIFT) & 0xFF; + const int col_delta_r = ((int)(col1 >> IM_COL32_R_SHIFT) & 0xFF) - col0_r; + const int col_delta_g = ((int)(col1 >> IM_COL32_G_SHIFT) & 0xFF) - col0_g; + const int col_delta_b = ((int)(col1 >> IM_COL32_B_SHIFT) & 0xFF) - col0_b; + for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) + { + float d = ImDot(vert->pos - gradient_p0, gradient_extent); + float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + int r = (int)(col0_r + col_delta_r * t); + int g = (int)(col0_g + col_delta_g * t); + int b = (int)(col0_b + col_delta_b * t); + vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); + } +} + +// Distribute UV over (a, b) rectangle +void ImGui::ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp) +{ + const ImVec2 size = b - a; + const ImVec2 uv_size = uv_b - uv_a; + const ImVec2 scale = ImVec2( + size.x != 0.0f ? (uv_size.x / size.x) : 0.0f, + size.y != 0.0f ? (uv_size.y / size.y) : 0.0f); + + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + if (clamp) + { + const ImVec2 min = ImMin(uv_a, uv_b); + const ImVec2 max = ImMax(uv_a, uv_b); + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = ImClamp(uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale), min, max); + } + else + { + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->uv = uv_a + ImMul(ImVec2(vertex->pos.x, vertex->pos.y) - a, scale); + } +} + +void ImGui::ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out) +{ + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->pos = ImRotate(vertex->pos- pivot_in, cos_a, sin_a) + pivot_out; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontConfig +//----------------------------------------------------------------------------- + +ImFontConfig::ImFontConfig() +{ + memset(this, 0, sizeof(*this)); + FontDataOwnedByAtlas = true; + OversampleH = 0; // Auto == 1 or 2 depending on size + OversampleV = 0; // Auto == 1 + GlyphMaxAdvanceX = FLT_MAX; + RasterizerMultiply = 1.0f; + RasterizerDensity = 1.0f; + EllipsisChar = 0; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas +//----------------------------------------------------------------------------- +// - Default texture data encoded in ASCII +// - ImFontAtlas::ClearInputData() +// - ImFontAtlas::ClearTexData() +// - ImFontAtlas::ClearFonts() +// - ImFontAtlas::Clear() +// - ImFontAtlas::GetTexDataAsAlpha8() +// - ImFontAtlas::GetTexDataAsRGBA32() +// - ImFontAtlas::AddFont() +// - ImFontAtlas::AddFontDefault() +// - ImFontAtlas::AddFontFromFileTTF() +// - ImFontAtlas::AddFontFromMemoryTTF() +// - ImFontAtlas::AddFontFromMemoryCompressedTTF() +// - ImFontAtlas::AddFontFromMemoryCompressedBase85TTF() +// - ImFontAtlas::AddCustomRectRegular() +// - ImFontAtlas::AddCustomRectFontGlyph() +// - ImFontAtlas::CalcCustomRectUV() +// - ImFontAtlasGetMouseCursorTexData() +// - ImFontAtlas::Build() +// - ImFontAtlasBuildMultiplyCalcLookupTable() +// - ImFontAtlasBuildMultiplyRectAlpha8() +// - ImFontAtlasBuildWithStbTruetype() +// - ImFontAtlasGetBuilderForStbTruetype() +// - ImFontAtlasUpdateSourcesPointers() +// - ImFontAtlasBuildSetupFont() +// - ImFontAtlasBuildPackCustomRects() +// - ImFontAtlasBuildRender8bppRectFromString() +// - ImFontAtlasBuildRender32bppRectFromString() +// - ImFontAtlasBuildRenderDefaultTexData() +// - ImFontAtlasBuildRenderLinesTexData() +// - ImFontAtlasBuildInit() +// - ImFontAtlasBuildFinish() +//----------------------------------------------------------------------------- + +// A work of art lies ahead! (. = white layer, X = black layer, others are blank) +// The 2x2 white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. +// (This is used when io.MouseDrawCursor = true) +const int FONT_ATLAS_DEFAULT_TEX_DATA_W = 122; // Actual texture will be 2 times that + 1 spacing. +const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; +static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +{ + "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX - XX XX " + "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X -X..X X..X" + "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X -X...X X...X" + "X - X.X - X.....X - X.....X -X...X - X...X- X..X - X...X X...X " + "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X - X...X...X " + "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX - X.....X " + "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX - X...X " + "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX - X.X " + "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X - X...X " + "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X- X.....X " + "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X- X...X...X " + "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X- X...X X...X " + "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X-X...X X...X" + "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X-X..X X..X" + "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X- XX XX " + "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X--------------" + "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X - " + "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X - " + "X.X X..X - -X.......X- X.......X - XX XX - - X..........X - " + "XX X..X - - X.....X - X.....X - X.X X.X - - X........X - " + " X..X - - X...X - X...X - X..X X..X - - X........X - " + " XX - - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX - " + "------------- - X - X -X.....................X- ------------------- " + " ----------------------------------- X...XXXXXXXXXXXXX...X - " + " - X..X X..X - " + " - X.X X.X - " + " - XX XX - " +}; + +static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_COUNT][3] = +{ + // Pos ........ Size ......... Offset ...... + { ImVec2( 0,3), ImVec2(12,19), ImVec2( 0, 0) }, // ImGuiMouseCursor_Arrow + { ImVec2(13,0), ImVec2( 7,16), ImVec2( 1, 8) }, // ImGuiMouseCursor_TextInput + { ImVec2(31,0), ImVec2(23,23), ImVec2(11,11) }, // ImGuiMouseCursor_ResizeAll + { ImVec2(21,0), ImVec2( 9,23), ImVec2( 4,11) }, // ImGuiMouseCursor_ResizeNS + { ImVec2(55,18),ImVec2(23, 9), ImVec2(11, 4) }, // ImGuiMouseCursor_ResizeEW + { ImVec2(73,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNESW + { ImVec2(55,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNWSE + { ImVec2(91,0), ImVec2(17,22), ImVec2( 5, 0) }, // ImGuiMouseCursor_Hand + { ImVec2(0,3), ImVec2(12,19), ImVec2(0, 0) }, // ImGuiMouseCursor_Wait // Arrow + custom code in ImGui::RenderMouseCursor() + { ImVec2(0,3), ImVec2(12,19), ImVec2(0, 0) }, // ImGuiMouseCursor_Progress // Arrow + custom code in ImGui::RenderMouseCursor() + { ImVec2(109,0),ImVec2(13,15), ImVec2( 6, 7) }, // ImGuiMouseCursor_NotAllowed +}; + +ImFontAtlas::ImFontAtlas() +{ + memset(this, 0, sizeof(*this)); + TexGlyphPadding = 1; + PackIdMouseCursors = PackIdLines = -1; +} + +ImFontAtlas::~ImFontAtlas() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + Clear(); +} + +void ImFontAtlas::ClearInputData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + for (ImFontConfig& font_cfg : Sources) + if (font_cfg.FontData && font_cfg.FontDataOwnedByAtlas) + { + IM_FREE(font_cfg.FontData); + font_cfg.FontData = NULL; + } + + // When clearing this we lose access to the font name and other information used to build the font. + for (ImFont* font : Fonts) + if (font->Sources >= Sources.Data && font->Sources < Sources.Data + Sources.Size) + { + font->Sources = NULL; + font->SourcesCount = 0; + } + Sources.clear(); + CustomRects.clear(); + PackIdMouseCursors = PackIdLines = -1; + // Important: we leave TexReady untouched +} + +void ImFontAtlas::ClearTexData() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + if (TexPixelsAlpha8) + IM_FREE(TexPixelsAlpha8); + if (TexPixelsRGBA32) + IM_FREE(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; + TexPixelsUseColors = false; + // Important: we leave TexReady untouched +} + +void ImFontAtlas::ClearFonts() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + ClearInputData(); + Fonts.clear_delete(); + TexReady = false; +} + +void ImFontAtlas::Clear() +{ + ClearInputData(); + ClearTexData(); + ClearFonts(); +} + +void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Build atlas on demand + if (TexPixelsAlpha8 == NULL) + Build(); + + *out_pixels = TexPixelsAlpha8; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 1; +} + +void ImFontAtlas::GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel) +{ + // Convert to RGBA32 format on demand + // Although it is likely to be the most commonly used format, our font rendering is 1 channel / 8 bpp + if (!TexPixelsRGBA32) + { + unsigned char* pixels = NULL; + GetTexDataAsAlpha8(&pixels, NULL, NULL); + if (pixels) + { + TexPixelsRGBA32 = (unsigned int*)IM_ALLOC((size_t)TexWidth * (size_t)TexHeight * 4); + const unsigned char* src = pixels; + unsigned int* dst = TexPixelsRGBA32; + for (int n = TexWidth * TexHeight; n > 0; n--) + *dst++ = IM_COL32(255, 255, 255, (unsigned int)(*src++)); + } + } + + *out_pixels = (unsigned char*)TexPixelsRGBA32; + if (out_width) *out_width = TexWidth; + if (out_height) *out_height = TexHeight; + if (out_bytes_per_pixel) *out_bytes_per_pixel = 4; +} + +ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); + IM_ASSERT(font_cfg->SizePixels > 0.0f && "Is ImFontConfig struct correctly initialized?"); + IM_ASSERT(font_cfg->RasterizerDensity > 0.0f && "Is ImFontConfig struct correctly initialized?"); + + // Create new font + if (!font_cfg->MergeMode) + Fonts.push_back(IM_NEW(ImFont)); + else + IM_ASSERT(Fonts.Size > 0 && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. + + Sources.push_back(*font_cfg); + ImFontConfig& new_font_cfg = Sources.back(); + if (new_font_cfg.DstFont == NULL) + new_font_cfg.DstFont = Fonts.back(); + if (!new_font_cfg.FontDataOwnedByAtlas) + { + new_font_cfg.FontData = IM_ALLOC(new_font_cfg.FontDataSize); + new_font_cfg.FontDataOwnedByAtlas = true; + memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize); + } + + // Round font size + // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet. + // - Note that using io.FontGlobalScale or SetWindowFontScale(), with are legacy-ish, partially supported features, can still lead to unrounded sizes. + // - We may support it better later and remove this rounding. + new_font_cfg.SizePixels = ImTrunc(new_font_cfg.SizePixels); + + // Pointers to Sources data are otherwise dangling + ImFontAtlasUpdateSourcesPointers(this); + + // Invalidate texture + TexReady = false; + ClearTexData(); + return new_font_cfg.DstFont; +} + +// Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) +static unsigned int stb_decompress_length(const unsigned char* input); +static unsigned int stb_decompress(unsigned char* output, const unsigned char* input, unsigned int length); +static unsigned int Decode85Byte(char c) { return c >= '\\' ? c-36 : c-35; } +static void Decode85(const unsigned char* src, unsigned char* dst) +{ + while (*src) + { + unsigned int tmp = Decode85Byte(src[0]) + 85 * (Decode85Byte(src[1]) + 85 * (Decode85Byte(src[2]) + 85 * (Decode85Byte(src[3]) + 85 * Decode85Byte(src[4])))); + dst[0] = ((tmp >> 0) & 0xFF); dst[1] = ((tmp >> 8) & 0xFF); dst[2] = ((tmp >> 16) & 0xFF); dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. + src += 5; + dst += 4; + } +} +#ifndef IMGUI_DISABLE_DEFAULT_FONT +static const char* GetDefaultCompressedFontDataTTF(int* out_size); +#endif + +// Load embedded ProggyClean.ttf at size 13, disable oversampling +ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) +{ +#ifndef IMGUI_DISABLE_DEFAULT_FONT + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (!font_cfg_template) + { + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + } + if (font_cfg.SizePixels <= 0.0f) + font_cfg.SizePixels = 13.0f * 1.0f; + if (font_cfg.Name[0] == '\0') + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); + font_cfg.EllipsisChar = (ImWchar)0x0085; + font_cfg.GlyphOffset.y = 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units + + int ttf_compressed_size = 0; + const char* ttf_compressed = GetDefaultCompressedFontDataTTF(&ttf_compressed_size); + const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); + ImFont* font = AddFontFromMemoryCompressedTTF(ttf_compressed, ttf_compressed_size, font_cfg.SizePixels, &font_cfg, glyph_ranges); + return font; +#else + IM_ASSERT(0 && "AddFontDefault() disabled in this build."); + IM_UNUSED(font_cfg_template); + return NULL; +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT +} + +ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + size_t data_size = 0; + void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); + if (!data) + { + IM_ASSERT_USER_ERROR(0, "Could not load font file!"); + return NULL; + } + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (font_cfg.Name[0] == '\0') + { + // Store a short copy of filename into into the font name for convenience + const char* p; + for (p = filename + ImStrlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); + } + return AddFontFromMemoryTTF(data, (int)data_size, size_pixels, &font_cfg, glyph_ranges); +} + +// NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + IM_ASSERT(font_data_size > 100 && "Incorrect value for font_data_size!"); // Heuristic to prevent accidentally passing a wrong value to font_data_size. + font_cfg.FontData = font_data; + font_cfg.FontDataSize = font_data_size; + font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; + if (glyph_ranges) + font_cfg.GlyphRanges = glyph_ranges; + return AddFont(&font_cfg); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + const unsigned int buf_decompressed_size = stb_decompress_length((const unsigned char*)compressed_ttf_data); + unsigned char* buf_decompressed_data = (unsigned char*)IM_ALLOC(buf_decompressed_size); + stb_decompress(buf_decompressed_data, (const unsigned char*)compressed_ttf_data, (unsigned int)compressed_ttf_size); + + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + IM_ASSERT(font_cfg.FontData == NULL); + font_cfg.FontDataOwnedByAtlas = true; + return AddFontFromMemoryTTF(buf_decompressed_data, (int)buf_decompressed_size, size_pixels, &font_cfg, glyph_ranges); +} + +ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) +{ + int compressed_ttf_size = (((int)ImStrlen(compressed_ttf_data_base85) + 4) / 5) * 4; + void* compressed_ttf = IM_ALLOC((size_t)compressed_ttf_size); + Decode85((const unsigned char*)compressed_ttf_data_base85, (unsigned char*)compressed_ttf); + ImFont* font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); + IM_FREE(compressed_ttf); + return font; +} + +int ImFontAtlas::AddCustomRectRegular(int width, int height) +{ + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) +{ +#ifdef IMGUI_USE_WCHAR32 + IM_ASSERT(id <= IM_UNICODE_CODEPOINT_MAX); +#endif + IM_ASSERT(font != NULL); + IM_ASSERT(width > 0 && width <= 0xFFFF); + IM_ASSERT(height > 0 && height <= 0xFFFF); + ImFontAtlasCustomRect r; + r.Width = (unsigned short)width; + r.Height = (unsigned short)height; + r.GlyphID = id; + r.GlyphColored = 0; // Set to 1 manually to mark glyph as colored // FIXME: No official API for that (#8133) + r.GlyphAdvanceX = advance_x; + r.GlyphOffset = offset; + r.Font = font; + CustomRects.push_back(r); + return CustomRects.Size - 1; // Return index +} + +void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const +{ + IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates + IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed + *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y); + *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); +} + +bool ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +{ + if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_COUNT) + return false; + if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors) + return false; + + IM_ASSERT(atlas->PackIdMouseCursors != -1); + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->X, (float)r->Y); + ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; + *out_size = size; + *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; + out_uv_border[0] = (pos) * atlas->TexUvScale; + out_uv_border[1] = (pos + size) * atlas->TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + out_uv_fill[0] = (pos) * atlas->TexUvScale; + out_uv_fill[1] = (pos + size) * atlas->TexUvScale; + return true; +} + +bool ImFontAtlas::Build() +{ + IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); + + // Default font is none are specified + if (Sources.Size == 0) + AddFontDefault(); + + // Select builder + // - Note that we do not reassign to atlas->FontBuilderIO, since it is likely to point to static data which + // may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are + // using a hot-reloading scheme that messes up static data, store your own instance of ImFontBuilderIO somewhere + // and point to it instead of pointing directly to return value of the GetBuilderXXX functions. + const ImFontBuilderIO* builder_io = FontBuilderIO; + if (builder_io == NULL) + { +#ifdef IMGUI_ENABLE_FREETYPE + builder_io = ImGuiFreeType::GetBuilderForFreeType(); +#elif defined(IMGUI_ENABLE_STB_TRUETYPE) + builder_io = ImFontAtlasGetBuilderForStbTruetype(); +#else + IM_ASSERT(0); // Invalid Build function +#endif + } + + // Build + return builder_io->FontBuilder_Build(this); +} + +void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) +{ + for (unsigned int i = 0; i < 256; i++) + { + unsigned int value = (unsigned int)(i * in_brighten_factor); + out_table[i] = value > 255 ? 255 : (value & 0xFF); + } +} + +void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) +{ + IM_ASSERT_PARANOID(w <= stride); + unsigned char* data = pixels + x + y * stride; + for (int j = h; j > 0; j--, data += stride - w) + for (int i = w; i > 0; i--, data++) + *data = table[*data]; +} + +void ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v) +{ + // Automatically disable horizontal oversampling over size 36 + *out_oversample_h = (src->OversampleH != 0) ? src->OversampleH : (src->SizePixels * src->RasterizerDensity > 36.0f || src->PixelSnapH) ? 1 : 2; + *out_oversample_v = (src->OversampleV != 0) ? src->OversampleV : 1; +} + +#ifdef IMGUI_ENABLE_STB_TRUETYPE +// Temporary data for one source font (multiple source fonts can be merged into one destination ImFont) +// (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.) +struct ImFontBuildSrcData +{ + stbtt_fontinfo FontInfo; + stbtt_pack_range PackRange; // Hold the list of codepoints to pack (essentially points to Codepoints.Data) + stbrp_rect* Rects; // Rectangle to pack. We first fill in their size and the packer will give us their position. + stbtt_packedchar* PackedChars; // Output glyphs + const ImWchar* SrcRanges; // Ranges as requested by user (user is allowed to request too much, e.g. 0x0020..0xFFFF) + int DstIndex; // Index into atlas->Fonts[] and dst_tmp_array[] + int GlyphsHighest; // Highest requested codepoint + int GlyphsCount; // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font) + ImBitVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB) + ImVector GlyphsList; // Glyph codepoints list (flattened version of GlyphsSet) +}; + +// Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont) +struct ImFontBuildDstData +{ + int SrcCount; // Number of source fonts targeting this destination font. + int GlyphsHighest; + int GlyphsCount; + ImBitVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font. +}; + +static void UnpackBitVectorToFlatIndexList(const ImBitVector* in, ImVector* out) +{ + IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int)); + const ImU32* it_begin = in->Storage.begin(); + const ImU32* it_end = in->Storage.end(); + for (const ImU32* it = it_begin; it < it_end; it++) + if (ImU32 entries_32 = *it) + for (ImU32 bit_n = 0; bit_n < 32; bit_n++) + if (entries_32 & ((ImU32)1 << bit_n)) + out->push_back((int)(((it - it_begin) << 5) + bit_n)); +} + +static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->Sources.Size > 0); + + ImFontAtlasBuildInit(atlas); + + // Clear atlas + atlas->TexID = (ImTextureID)NULL; + atlas->TexWidth = atlas->TexHeight = 0; + atlas->TexUvScale = ImVec2(0.0f, 0.0f); + atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); + atlas->ClearTexData(); + + // Temporary storage for building + ImVector src_tmp_array; + ImVector dst_tmp_array; + src_tmp_array.resize(atlas->Sources.Size); + dst_tmp_array.resize(atlas->Fonts.Size); + memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes()); + memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes()); + + // 1. Initialize font loading structure, check font data validity + for (int src_i = 0; src_i < atlas->Sources.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontConfig& src = atlas->Sources[src_i]; + IM_ASSERT(src.DstFont && (!src.DstFont->IsLoaded() || src.DstFont->ContainerAtlas == atlas)); + + // Find index from src.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices) + src_tmp.DstIndex = -1; + for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++) + if (src.DstFont == atlas->Fonts[output_i]) + src_tmp.DstIndex = output_i; + if (src_tmp.DstIndex == -1) + { + IM_ASSERT(src_tmp.DstIndex != -1); // src.DstFont not pointing within atlas->Fonts[] array? + return false; + } + // Initialize helper structure for font loading and verify that the TTF/OTF data is correct + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)src.FontData, src.FontNo); + IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found."); + if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)src.FontData, font_offset)) + { + IM_ASSERT(0 && "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize."); + return false; + } + + // Measure highest codepoints + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.SrcRanges = src.GlyphRanges ? src.GlyphRanges : atlas->GetGlyphRangesDefault(); + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + { + // Check for valid range. This may also help detect *some* dangling pointers, because a common + // user error is to setup ImFontConfig::GlyphRanges with a pointer to data that isn't persistent, + // or to forget to zero-terminate the glyph range array. + IM_ASSERT(src_range[0] <= src_range[1] && "Invalid range: is your glyph range array persistent? it is zero-terminated?"); + src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]); + } + dst_tmp.SrcCount++; + dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest); + } + + // 2. For every requested codepoint, check for their presence in the font data, and handle redundancy or overlaps between source fonts to avoid unused glyphs. + int total_glyphs_count = 0; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; + src_tmp.GlyphsSet.Create(src_tmp.GlyphsHighest + 1); + if (dst_tmp.GlyphsSet.Storage.empty()) + dst_tmp.GlyphsSet.Create(dst_tmp.GlyphsHighest + 1); + + for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + for (unsigned int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++) + { + if (dst_tmp.GlyphsSet.TestBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true) + continue; + if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint)) // It is actually in the font? + continue; + + // Add to avail set/counters + src_tmp.GlyphsCount++; + dst_tmp.GlyphsCount++; + src_tmp.GlyphsSet.SetBit(codepoint); + dst_tmp.GlyphsSet.SetBit(codepoint); + total_glyphs_count++; + } + } + + // 3. Unpack our bit map into a flat list (we now have all the Unicode points that we know are requested _and_ available _and_ not overlapping another) + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount); + UnpackBitVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList); + src_tmp.GlyphsSet.Clear(); + IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount); + } + for (int dst_i = 0; dst_i < dst_tmp_array.Size; dst_i++) + dst_tmp_array[dst_i].GlyphsSet.Clear(); + dst_tmp_array.clear(); + + // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) + // (We technically don't need to zero-clear buf_rects, but let's do it for the sake of sanity) + ImVector buf_rects; + ImVector buf_packedchars; + buf_rects.resize(total_glyphs_count); + buf_packedchars.resize(total_glyphs_count); + memset(buf_rects.Data, 0, (size_t)buf_rects.size_in_bytes()); + memset(buf_packedchars.Data, 0, (size_t)buf_packedchars.size_in_bytes()); + + // 4. Gather glyphs sizes so we can pack them in our virtual canvas. + int total_surface = 0; + int buf_rects_out_n = 0; + int buf_packedchars_out_n = 0; + const int pack_padding = atlas->TexGlyphPadding; + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + src_tmp.Rects = &buf_rects[buf_rects_out_n]; + src_tmp.PackedChars = &buf_packedchars[buf_packedchars_out_n]; + buf_rects_out_n += src_tmp.GlyphsCount; + buf_packedchars_out_n += src_tmp.GlyphsCount; + + // Automatic selection of oversampling parameters + ImFontConfig& src = atlas->Sources[src_i]; + int oversample_h, oversample_v; + ImFontAtlasBuildGetOversampleFactors(&src, &oversample_h, &oversample_v); + + // Convert our ranges in the format stb_truetype wants + src_tmp.PackRange.font_size = src.SizePixels * src.RasterizerDensity; + src_tmp.PackRange.first_unicode_codepoint_in_range = 0; + src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data; + src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size; + src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars; + src_tmp.PackRange.h_oversample = (unsigned char)oversample_h; + src_tmp.PackRange.v_oversample = (unsigned char)oversample_v; + + // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects) + const float scale = (src.SizePixels > 0.0f) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels * src.RasterizerDensity) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -src.SizePixels * src.RasterizerDensity); + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++) + { + int x0, y0, x1, y1; + const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]); + IM_ASSERT(glyph_index_in_font != 0); + stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * oversample_h, scale * oversample_v, 0, 0, &x0, &y0, &x1, &y1); + src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + pack_padding + oversample_h - 1); + src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + pack_padding + oversample_v - 1); + total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h; + } + } + for (int i = 0; i < atlas->CustomRects.Size; i++) + total_surface += (atlas->CustomRects[i].Width + pack_padding) * (atlas->CustomRects[i].Height + pack_padding); + + // We need a width for the skyline algorithm, any width! + // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. + // User can override TexDesiredWidth and TexGlyphPadding if they wish, otherwise we use a simple heuristic to select the width based on expected surface. + const int surface_sqrt = (int)ImSqrt((float)total_surface) + 1; + atlas->TexHeight = 0; + if (atlas->TexDesiredWidth > 0) + atlas->TexWidth = atlas->TexDesiredWidth; + else + atlas->TexWidth = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; + + // 5. Start packing + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + const int TEX_HEIGHT_MAX = 1024 * 32; + stbtt_pack_context spc = {}; + stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, 0, NULL); + spc.padding = atlas->TexGlyphPadding; // Because we mixup stbtt_PackXXX and stbrp_PackXXX there's a bit of a hack here, not passing the value to stbtt_PackBegin() allows us to still pack a TexWidth-1 wide item. (#8107) + ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); + + // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point. + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbrp_pack_rects((stbrp_context*)spc.pack_info, src_tmp.Rects, src_tmp.GlyphsCount); + + // Extend texture height and mark missing glyphs as non-packed so we won't render them. + // FIXME: We are not handling packing failure here (would happen if we got off TEX_HEIGHT_MAX or if a single if larger than TexWidth?) + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + if (src_tmp.Rects[glyph_i].was_packed) + atlas->TexHeight = ImMax(atlas->TexHeight, src_tmp.Rects[glyph_i].y + src_tmp.Rects[glyph_i].h); + } + + // 7. Allocate texture + atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); + atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = (unsigned char*)IM_ALLOC(atlas->TexWidth * atlas->TexHeight); + memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); + spc.pixels = atlas->TexPixelsAlpha8; + spc.height = atlas->TexHeight; + + // 8. Render/rasterize font characters into the texture + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + ImFontConfig& src = atlas->Sources[src_i]; + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + if (src_tmp.GlyphsCount == 0) + continue; + + stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects); + + // Apply multiply operator + if (src.RasterizerMultiply != 1.0f) + { + unsigned char multiply_table[256]; + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, src.RasterizerMultiply); + stbrp_rect* r = &src_tmp.Rects[0]; + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++) + if (r->was_packed) + ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, atlas->TexPixelsAlpha8, r->x, r->y, r->w, r->h, atlas->TexWidth * 1); + } + src_tmp.Rects = NULL; + } + + // End packing + stbtt_PackEnd(&spc); + buf_rects.clear(); + + // 9. Setup ImFont and glyphs for runtime + for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) + { + // When merging fonts with MergeMode=true: + // - We can have multiple input fonts writing into a same destination font. + // - dst_font->Sources is != from src which is our source configuration. + ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; + ImFontConfig& src = atlas->Sources[src_i]; + ImFont* dst_font = src.DstFont; + + const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels); + int unscaled_ascent, unscaled_descent, unscaled_line_gap; + stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); + + const float ascent = ImCeil(unscaled_ascent * font_scale); + const float descent = ImFloor(unscaled_descent * font_scale); + ImFontAtlasBuildSetupFont(atlas, dst_font, &src, ascent, descent); + const float font_off_x = src.GlyphOffset.x; + const float font_off_y = src.GlyphOffset.y + IM_ROUND(dst_font->Ascent); + + const float inv_rasterization_scale = 1.0f / src.RasterizerDensity; + + for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) + { + // Register glyph + const int codepoint = src_tmp.GlyphsList[glyph_i]; + const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i]; + stbtt_aligned_quad q; + float unused_x = 0.0f, unused_y = 0.0f; + stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0); + float x0 = q.x0 * inv_rasterization_scale + font_off_x; + float y0 = q.y0 * inv_rasterization_scale + font_off_y; + float x1 = q.x1 * inv_rasterization_scale + font_off_x; + float y1 = q.y1 * inv_rasterization_scale + font_off_y; + dst_font->AddGlyph(&src, (ImWchar)codepoint, x0, y0, x1, y1, q.s0, q.t0, q.s1, q.t1, pc.xadvance * inv_rasterization_scale); + } + } + + // Cleanup + src_tmp_array.clear_destruct(); + + ImFontAtlasBuildFinish(atlas); + return true; +} + +const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype() +{ + static ImFontBuilderIO io; + io.FontBuilder_Build = ImFontAtlasBuildWithStbTruetype; + return &io; +} + +#endif // IMGUI_ENABLE_STB_TRUETYPE + +void ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas) +{ + for (ImFontConfig& src : atlas->Sources) + { + ImFont* font = src.DstFont; + if (!src.MergeMode) + { + font->Sources = &src; + font->SourcesCount = 0; + } + font->SourcesCount++; + } +} + +void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) +{ + if (!font_config->MergeMode) + { + font->ClearOutputData(); + font->FontSize = font_config->SizePixels; + IM_ASSERT(font->Sources == font_config); + font->ContainerAtlas = atlas; + font->Ascent = ascent; + font->Descent = descent; + } +} + +void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque) +{ + stbrp_context* pack_context = (stbrp_context*)stbrp_context_opaque; + IM_ASSERT(pack_context != NULL); + + ImVector& user_rects = atlas->CustomRects; + IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. +#ifdef __GNUC__ + if (user_rects.Size < 1) { __builtin_unreachable(); } // Workaround for GCC bug if IM_ASSERT() is defined to conditionally throw (see #5343) +#endif + + const int pack_padding = atlas->TexGlyphPadding; + ImVector pack_rects; + pack_rects.resize(user_rects.Size); + memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes()); + for (int i = 0; i < user_rects.Size; i++) + { + pack_rects[i].w = user_rects[i].Width + pack_padding; + pack_rects[i].h = user_rects[i].Height + pack_padding; + } + stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); + for (int i = 0; i < pack_rects.Size; i++) + if (pack_rects[i].was_packed) + { + user_rects[i].X = (unsigned short)pack_rects[i].x; + user_rects[i].Y = (unsigned short)pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width + pack_padding && pack_rects[i].h == user_rects[i].Height + pack_padding); + atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); + } +} + +void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned char* out_pixel = atlas->TexPixelsAlpha8 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : 0x00; +} + +void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned int* out_pixel = atlas->TexPixelsRGBA32 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : IM_COL32_BLACK_TRANS; +} + +static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) +{ + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + IM_ASSERT(r->IsPacked()); + + const int w = atlas->TexWidth; + if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors) + { + // White pixels only + IM_ASSERT(r->Width == 2 && r->Height == 2); + const int offset = (int)r->X + (int)r->Y * w; + if (atlas->TexPixelsAlpha8 != NULL) + { + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + else + { + atlas->TexPixelsRGBA32[offset] = atlas->TexPixelsRGBA32[offset + 1] = atlas->TexPixelsRGBA32[offset + w] = atlas->TexPixelsRGBA32[offset + w + 1] = IM_COL32_WHITE; + } + } + else + { + // White pixels and mouse cursor + IM_ASSERT(r->Width == FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1 && r->Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + const int x_for_white = r->X; + const int x_for_black = r->X + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + if (atlas->TexPixelsAlpha8 != NULL) + { + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', 0xFF); + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', 0xFF); + } + else + { + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', IM_COL32_WHITE); + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', IM_COL32_WHITE); + } + } + atlas->TexUvWhitePixel = ImVec2((r->X + 0.5f) * atlas->TexUvScale.x, (r->Y + 0.5f) * atlas->TexUvScale.y); +} + +static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas) +{ + if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) + return; + + // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdLines); + IM_ASSERT(r->IsPacked()); + for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row + { + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + int y = n; + int line_width = n; + int pad_left = (r->Width - line_width) / 2; + int pad_right = r->Width - (pad_left + line_width); + + // Write each slice + IM_ASSERT(pad_left + line_width + pad_right == r->Width && y < r->Height); // Make sure we're inside the texture bounds before we start writing pixels + if (atlas->TexPixelsAlpha8 != NULL) + { + unsigned char* write_ptr = &atlas->TexPixelsAlpha8[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = 0x00; + + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = 0xFF; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = 0x00; + } + else + { + unsigned int* write_ptr = &atlas->TexPixelsRGBA32[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = IM_COL32(255, 255, 255, 0); + + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = IM_COL32_WHITE; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0); + } + + // Calculate UVs for this line + ImVec2 uv0 = ImVec2((float)(r->X + pad_left - 1), (float)(r->Y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r->X + pad_left + line_width + 1), (float)(r->Y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); + } +} + +// Note: this is called / shared by both the stb_truetype and the FreeType builder +void ImFontAtlasBuildInit(ImFontAtlas* atlas) +{ + // Register texture region for mouse cursors or standard white pixels + if (atlas->PackIdMouseCursors < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(2, 2); + } + + // Register texture region for thick lines + // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row + if (atlas->PackIdLines < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoBakedLines)) + atlas->PackIdLines = atlas->AddCustomRectRegular(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1); + } +} + +// This is called/shared by both the stb_truetype and the FreeType builder. +void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +{ + // Render into our custom data blocks + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL || atlas->TexPixelsRGBA32 != NULL); + ImFontAtlasBuildRenderDefaultTexData(atlas); + ImFontAtlasBuildRenderLinesTexData(atlas); + + // Register custom rectangle glyphs + for (int i = 0; i < atlas->CustomRects.Size; i++) + { + const ImFontAtlasCustomRect* r = &atlas->CustomRects[i]; + if (r->Font == NULL || r->GlyphID == 0) + continue; + + // Will ignore ImFontConfig settings: GlyphMinAdvanceX, GlyphMinAdvanceY, PixelSnapH + IM_ASSERT(r->Font->ContainerAtlas == atlas); + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(r, &uv0, &uv1); + r->Font->AddGlyph(NULL, (ImWchar)r->GlyphID, r->GlyphOffset.x, r->GlyphOffset.y, r->GlyphOffset.x + r->Width, r->GlyphOffset.y + r->Height, uv0.x, uv0.y, uv1.x, uv1.y, r->GlyphAdvanceX); + if (r->GlyphColored) + r->Font->Glyphs.back().Colored = 1; + } + + // Build all fonts lookup tables + for (ImFont* font : atlas->Fonts) + if (font->DirtyLookupTables) + font->BuildLookupTable(); + + atlas->TexReady = true; +} + +//------------------------------------------------------------------------- +// [SECTION] ImFontAtlas: glyph ranges helpers +//------------------------------------------------------------------------- +// - GetGlyphRangesDefault() +// - GetGlyphRangesGreek() +// - GetGlyphRangesKorean() +// - GetGlyphRangesChineseFull() +// - GetGlyphRangesChineseSimplifiedCommon() +// - GetGlyphRangesJapanese() +// - GetGlyphRangesCyrillic() +// - GetGlyphRangesThai() +// - GetGlyphRangesVietnamese() +//----------------------------------------------------------------------------- + +// Retrieve list of range (2 int per range, values are inclusive) +const ImWchar* ImFontAtlas::GetGlyphRangesDefault() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesGreek() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0370, 0x03FF, // Greek and Coptic + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3131, 0x3163, // Korean alphabets + 0xAC00, 0xD7A3, // Korean characters + 0xFFFD, 0xFFFD, // Invalid + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesChineseFull() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD, // Invalid + 0x4e00, 0x9FAF, // CJK Ideograms + 0, + }; + return &ranges[0]; +} + +static void UnpackAccumulativeOffsetsIntoRanges(int base_codepoint, const short* accumulative_offsets, int accumulative_offsets_count, ImWchar* out_ranges) +{ + for (int n = 0; n < accumulative_offsets_count; n++, out_ranges += 2) + { + out_ranges[0] = out_ranges[1] = (ImWchar)(base_codepoint + accumulative_offsets[n]); + base_codepoint += accumulative_offsets[n]; + } + out_ranges[0] = 0; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() +{ + // Store 2500 regularly used characters for Simplified Chinese. + // Sourced from https://zh.wiktionary.org/wiki/%E9%99%84%E5%BD%95:%E7%8E%B0%E4%BB%A3%E6%B1%89%E8%AF%AD%E5%B8%B8%E7%94%A8%E5%AD%97%E8%A1%A8 + // This table covers 97.97% of all characters used during the month in July, 1987. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = + { + 0,1,2,4,1,1,1,1,2,1,3,2,1,2,2,1,1,1,1,1,5,2,1,2,3,3,3,2,2,4,1,1,1,2,1,5,2,3,1,2,1,2,1,1,2,1,1,2,2,1,4,1,1,1,1,5,10,1,2,19,2,1,2,1,2,1,2,1,2, + 1,5,1,6,3,2,1,2,2,1,1,1,4,8,5,1,1,4,1,1,3,1,2,1,5,1,2,1,1,1,10,1,1,5,2,4,6,1,4,2,2,2,12,2,1,1,6,1,1,1,4,1,1,4,6,5,1,4,2,2,4,10,7,1,1,4,2,4, + 2,1,4,3,6,10,12,5,7,2,14,2,9,1,1,6,7,10,4,7,13,1,5,4,8,4,1,1,2,28,5,6,1,1,5,2,5,20,2,2,9,8,11,2,9,17,1,8,6,8,27,4,6,9,20,11,27,6,68,2,2,1,1, + 1,2,1,2,2,7,6,11,3,3,1,1,3,1,2,1,1,1,1,1,3,1,1,8,3,4,1,5,7,2,1,4,4,8,4,2,1,2,1,1,4,5,6,3,6,2,12,3,1,3,9,2,4,3,4,1,5,3,3,1,3,7,1,5,1,1,1,1,2, + 3,4,5,2,3,2,6,1,1,2,1,7,1,7,3,4,5,15,2,2,1,5,3,22,19,2,1,1,1,1,2,5,1,1,1,6,1,1,12,8,2,9,18,22,4,1,1,5,1,16,1,2,7,10,15,1,1,6,2,4,1,2,4,1,6, + 1,1,3,2,4,1,6,4,5,1,2,1,1,2,1,10,3,1,3,2,1,9,3,2,5,7,2,19,4,3,6,1,1,1,1,1,4,3,2,1,1,1,2,5,3,1,1,1,2,2,1,1,2,1,1,2,1,3,1,1,1,3,7,1,4,1,1,2,1, + 1,2,1,2,4,4,3,8,1,1,1,2,1,3,5,1,3,1,3,4,6,2,2,14,4,6,6,11,9,1,15,3,1,28,5,2,5,5,3,1,3,4,5,4,6,14,3,2,3,5,21,2,7,20,10,1,2,19,2,4,28,28,2,3, + 2,1,14,4,1,26,28,42,12,40,3,52,79,5,14,17,3,2,2,11,3,4,6,3,1,8,2,23,4,5,8,10,4,2,7,3,5,1,1,6,3,1,2,2,2,5,28,1,1,7,7,20,5,3,29,3,17,26,1,8,4, + 27,3,6,11,23,5,3,4,6,13,24,16,6,5,10,25,35,7,3,2,3,3,14,3,6,2,6,1,4,2,3,8,2,1,1,3,3,3,4,1,1,13,2,2,4,5,2,1,14,14,1,2,2,1,4,5,2,3,1,14,3,12, + 3,17,2,16,5,1,2,1,8,9,3,19,4,2,2,4,17,25,21,20,28,75,1,10,29,103,4,1,2,1,1,4,2,4,1,2,3,24,2,2,2,1,1,2,1,3,8,1,1,1,2,1,1,3,1,1,1,6,1,5,3,1,1, + 1,3,4,1,1,5,2,1,5,6,13,9,16,1,1,1,1,3,2,3,2,4,5,2,5,2,2,3,7,13,7,2,2,1,1,1,1,2,3,3,2,1,6,4,9,2,1,14,2,14,2,1,18,3,4,14,4,11,41,15,23,15,23, + 176,1,3,4,1,1,1,1,5,3,1,2,3,7,3,1,1,2,1,2,4,4,6,2,4,1,9,7,1,10,5,8,16,29,1,1,2,2,3,1,3,5,2,4,5,4,1,1,2,2,3,3,7,1,6,10,1,17,1,44,4,6,2,1,1,6, + 5,4,2,10,1,6,9,2,8,1,24,1,2,13,7,8,8,2,1,4,1,3,1,3,3,5,2,5,10,9,4,9,12,2,1,6,1,10,1,1,7,7,4,10,8,3,1,13,4,3,1,6,1,3,5,2,1,2,17,16,5,2,16,6, + 1,4,2,1,3,3,6,8,5,11,11,1,3,3,2,4,6,10,9,5,7,4,7,4,7,1,1,4,2,1,3,6,8,7,1,6,11,5,5,3,24,9,4,2,7,13,5,1,8,82,16,61,1,1,1,4,2,2,16,10,3,8,1,1, + 6,4,2,1,3,1,1,1,4,3,8,4,2,2,1,1,1,1,1,6,3,5,1,1,4,6,9,2,1,1,1,2,1,7,2,1,6,1,5,4,4,3,1,8,1,3,3,1,3,2,2,2,2,3,1,6,1,2,1,2,1,3,7,1,8,2,1,2,1,5, + 2,5,3,5,10,1,2,1,1,3,2,5,11,3,9,3,5,1,1,5,9,1,2,1,5,7,9,9,8,1,3,3,3,6,8,2,3,2,1,1,32,6,1,2,15,9,3,7,13,1,3,10,13,2,14,1,13,10,2,1,3,10,4,15, + 2,15,15,10,1,3,9,6,9,32,25,26,47,7,3,2,3,1,6,3,4,3,2,8,5,4,1,9,4,2,2,19,10,6,2,3,8,1,2,2,4,2,1,9,4,4,4,6,4,8,9,2,3,1,1,1,1,3,5,5,1,3,8,4,6, + 2,1,4,12,1,5,3,7,13,2,5,8,1,6,1,2,5,14,6,1,5,2,4,8,15,5,1,23,6,62,2,10,1,1,8,1,2,2,10,4,2,2,9,2,1,1,3,2,3,1,5,3,3,2,1,3,8,1,1,1,11,3,1,1,4, + 3,7,1,14,1,2,3,12,5,2,5,1,6,7,5,7,14,11,1,3,1,8,9,12,2,1,11,8,4,4,2,6,10,9,13,1,1,3,1,5,1,3,2,4,4,1,18,2,3,14,11,4,29,4,2,7,1,3,13,9,2,2,5, + 3,5,20,7,16,8,5,72,34,6,4,22,12,12,28,45,36,9,7,39,9,191,1,1,1,4,11,8,4,9,2,3,22,1,1,1,1,4,17,1,7,7,1,11,31,10,2,4,8,2,3,2,1,4,2,16,4,32,2, + 3,19,13,4,9,1,5,2,14,8,1,1,3,6,19,6,5,1,16,6,2,10,8,5,1,2,3,1,5,5,1,11,6,6,1,3,3,2,6,3,8,1,1,4,10,7,5,7,7,5,8,9,2,1,3,4,1,1,3,1,3,3,2,6,16, + 1,4,6,3,1,10,6,1,3,15,2,9,2,10,25,13,9,16,6,2,2,10,11,4,3,9,1,2,6,6,5,4,30,40,1,10,7,12,14,33,6,3,6,7,3,1,3,1,11,14,4,9,5,12,11,49,18,51,31, + 140,31,2,2,1,5,1,8,1,10,1,4,4,3,24,1,10,1,3,6,6,16,3,4,5,2,1,4,2,57,10,6,22,2,22,3,7,22,6,10,11,36,18,16,33,36,2,5,5,1,1,1,4,10,1,4,13,2,7, + 5,2,9,3,4,1,7,43,3,7,3,9,14,7,9,1,11,1,1,3,7,4,18,13,1,14,1,3,6,10,73,2,2,30,6,1,11,18,19,13,22,3,46,42,37,89,7,3,16,34,2,2,3,9,1,7,1,1,1,2, + 2,4,10,7,3,10,3,9,5,28,9,2,6,13,7,3,1,3,10,2,7,2,11,3,6,21,54,85,2,1,4,2,2,1,39,3,21,2,2,5,1,1,1,4,1,1,3,4,15,1,3,2,4,4,2,3,8,2,20,1,8,7,13, + 4,1,26,6,2,9,34,4,21,52,10,4,4,1,5,12,2,11,1,7,2,30,12,44,2,30,1,1,3,6,16,9,17,39,82,2,2,24,7,1,7,3,16,9,14,44,2,1,2,1,2,3,5,2,4,1,6,7,5,3, + 2,6,1,11,5,11,2,1,18,19,8,1,3,24,29,2,1,3,5,2,2,1,13,6,5,1,46,11,3,5,1,1,5,8,2,10,6,12,6,3,7,11,2,4,16,13,2,5,1,1,2,2,5,2,28,5,2,23,10,8,4, + 4,22,39,95,38,8,14,9,5,1,13,5,4,3,13,12,11,1,9,1,27,37,2,5,4,4,63,211,95,2,2,2,1,3,5,2,1,1,2,2,1,1,1,3,2,4,1,2,1,1,5,2,2,1,1,2,3,1,3,1,1,1, + 3,1,4,2,1,3,6,1,1,3,7,15,5,3,2,5,3,9,11,4,2,22,1,6,3,8,7,1,4,28,4,16,3,3,25,4,4,27,27,1,4,1,2,2,7,1,3,5,2,28,8,2,14,1,8,6,16,25,3,3,3,14,3, + 3,1,1,2,1,4,6,3,8,4,1,1,1,2,3,6,10,6,2,3,18,3,2,5,5,4,3,1,5,2,5,4,23,7,6,12,6,4,17,11,9,5,1,1,10,5,12,1,1,11,26,33,7,3,6,1,17,7,1,5,12,1,11, + 2,4,1,8,14,17,23,1,2,1,7,8,16,11,9,6,5,2,6,4,16,2,8,14,1,11,8,9,1,1,1,9,25,4,11,19,7,2,15,2,12,8,52,7,5,19,2,16,4,36,8,1,16,8,24,26,4,6,2,9, + 5,4,36,3,28,12,25,15,37,27,17,12,59,38,5,32,127,1,2,9,17,14,4,1,2,1,1,8,11,50,4,14,2,19,16,4,17,5,4,5,26,12,45,2,23,45,104,30,12,8,3,10,2,2, + 3,3,1,4,20,7,2,9,6,15,2,20,1,3,16,4,11,15,6,134,2,5,59,1,2,2,2,1,9,17,3,26,137,10,211,59,1,2,4,1,4,1,1,1,2,6,2,3,1,1,2,3,2,3,1,3,4,4,2,3,3, + 1,4,3,1,7,2,2,3,1,2,1,3,3,3,2,2,3,2,1,3,14,6,1,3,2,9,6,15,27,9,34,145,1,1,2,1,1,1,1,2,1,1,1,1,2,2,2,3,1,2,1,1,1,2,3,5,8,3,5,2,4,1,3,2,2,2,12, + 4,1,1,1,10,4,5,1,20,4,16,1,15,9,5,12,2,9,2,5,4,2,26,19,7,1,26,4,30,12,15,42,1,6,8,172,1,1,4,2,1,1,11,2,2,4,2,1,2,1,10,8,1,2,1,4,5,1,2,5,1,8, + 4,1,3,4,2,1,6,2,1,3,4,1,2,1,1,1,1,12,5,7,2,4,3,1,1,1,3,3,6,1,2,2,3,3,3,2,1,2,12,14,11,6,6,4,12,2,8,1,7,10,1,35,7,4,13,15,4,3,23,21,28,52,5, + 26,5,6,1,7,10,2,7,53,3,2,1,1,1,2,163,532,1,10,11,1,3,3,4,8,2,8,6,2,2,23,22,4,2,2,4,2,1,3,1,3,3,5,9,8,2,1,2,8,1,10,2,12,21,20,15,105,2,3,1,1, + 3,2,3,1,1,2,5,1,4,15,11,19,1,1,1,1,5,4,5,1,1,2,5,3,5,12,1,2,5,1,11,1,1,15,9,1,4,5,3,26,8,2,1,3,1,1,15,19,2,12,1,2,5,2,7,2,19,2,20,6,26,7,5, + 2,2,7,34,21,13,70,2,128,1,1,2,1,1,2,1,1,3,2,2,2,15,1,4,1,3,4,42,10,6,1,49,85,8,1,2,1,1,4,4,2,3,6,1,5,7,4,3,211,4,1,2,1,2,5,1,2,4,2,2,6,5,6, + 10,3,4,48,100,6,2,16,296,5,27,387,2,2,3,7,16,8,5,38,15,39,21,9,10,3,7,59,13,27,21,47,5,21,6 + }; + static ImWchar base_ranges[] = // not zero-terminated + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x2000, 0x206F, // General Punctuation + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00) * 2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() +{ + // 2999 ideograms code points for Japanese + // - 2136 Joyo (meaning "for regular use" or "for common use") Kanji code points + // - 863 Jinmeiyo (meaning "for personal name") Kanji code points + // - Sourced from official information provided by the government agencies of Japan: + // - List of Joyo Kanji by the Agency for Cultural Affairs + // - https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kijun/naikaku/kanji/ + // - List of Jinmeiyo Kanji by the Ministry of Justice + // - http://www.moj.go.jp/MINJI/minji86.html + // - Available under the terms of the Creative Commons Attribution 4.0 International (CC BY 4.0). + // - https://creativecommons.org/licenses/by/4.0/legalcode + // - You can generate this code by the script at: + // - https://github.com/vaiorabbit/everyday_use_kanji + // - References: + // - List of Joyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/List_of_j%C5%8Dy%C5%8D_kanji + // - List of Jinmeiyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/Jinmeiy%C5%8D_kanji + // - Missing 1 Joyo Kanji: U+20B9F (Kun'yomi: Shikaru, On'yomi: Shitsu,shichi), see https://github.com/ocornut/imgui/pull/3627 for details. + // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. + // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) + static const short accumulative_offsets_from_0x4E00[] = + { + 0,1,2,4,1,1,1,1,2,1,3,3,2,2,1,5,3,5,7,5,6,1,2,1,7,2,6,3,1,8,1,1,4,1,1,18,2,11,2,6,2,1,2,1,5,1,2,1,3,1,2,1,2,3,3,1,1,2,3,1,1,1,12,7,9,1,4,5,1, + 1,2,1,10,1,1,9,2,2,4,5,6,9,3,1,1,1,1,9,3,18,5,2,2,2,2,1,6,3,7,1,1,1,1,2,2,4,2,1,23,2,10,4,3,5,2,4,10,2,4,13,1,6,1,9,3,1,1,6,6,7,6,3,1,2,11,3, + 2,2,3,2,15,2,2,5,4,3,6,4,1,2,5,2,12,16,6,13,9,13,2,1,1,7,16,4,7,1,19,1,5,1,2,2,7,7,8,2,6,5,4,9,18,7,4,5,9,13,11,8,15,2,1,1,1,2,1,2,2,1,2,2,8, + 2,9,3,3,1,1,4,4,1,1,1,4,9,1,4,3,5,5,2,7,5,3,4,8,2,1,13,2,3,3,1,14,1,1,4,5,1,3,6,1,5,2,1,1,3,3,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1,1,1,1,12,3,3,9,5, + 2,6,1,5,6,1,2,3,18,2,4,14,4,1,3,6,1,1,6,3,5,5,3,2,2,2,2,12,3,1,4,2,3,2,3,11,1,7,4,1,2,1,3,17,1,9,1,24,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,2,4,15,1, + 1,2,1,1,2,1,5,2,5,20,2,5,9,1,10,8,7,6,1,1,1,1,1,1,6,2,1,2,8,1,1,1,1,5,1,1,3,1,1,1,1,3,1,1,12,4,1,3,1,1,1,1,1,10,3,1,7,5,13,1,2,3,4,6,1,1,30, + 2,9,9,1,15,38,11,3,1,8,24,7,1,9,8,10,2,1,9,31,2,13,6,2,9,4,49,5,2,15,2,1,10,2,1,1,1,2,2,6,15,30,35,3,14,18,8,1,16,10,28,12,19,45,38,1,3,2,3, + 13,2,1,7,3,6,5,3,4,3,1,5,7,8,1,5,3,18,5,3,6,1,21,4,24,9,24,40,3,14,3,21,3,2,1,2,4,2,3,1,15,15,6,5,1,1,3,1,5,6,1,9,7,3,3,2,1,4,3,8,21,5,16,4, + 5,2,10,11,11,3,6,3,2,9,3,6,13,1,2,1,1,1,1,11,12,6,6,1,4,2,6,5,2,1,1,3,3,6,13,3,1,1,5,1,2,3,3,14,2,1,2,2,2,5,1,9,5,1,1,6,12,3,12,3,4,13,2,14, + 2,8,1,17,5,1,16,4,2,2,21,8,9,6,23,20,12,25,19,9,38,8,3,21,40,25,33,13,4,3,1,4,1,2,4,1,2,5,26,2,1,1,2,1,3,6,2,1,1,1,1,1,1,2,3,1,1,1,9,2,3,1,1, + 1,3,6,3,2,1,1,6,6,1,8,2,2,2,1,4,1,2,3,2,7,3,2,4,1,2,1,2,2,1,1,1,1,1,3,1,2,5,4,10,9,4,9,1,1,1,1,1,1,5,3,2,1,6,4,9,6,1,10,2,31,17,8,3,7,5,40,1, + 7,7,1,6,5,2,10,7,8,4,15,39,25,6,28,47,18,10,7,1,3,1,1,2,1,1,1,3,3,3,1,1,1,3,4,2,1,4,1,3,6,10,7,8,6,2,2,1,3,3,2,5,8,7,9,12,2,15,1,1,4,1,2,1,1, + 1,3,2,1,3,3,5,6,2,3,2,10,1,4,2,8,1,1,1,11,6,1,21,4,16,3,1,3,1,4,2,3,6,5,1,3,1,1,3,3,4,6,1,1,10,4,2,7,10,4,7,4,2,9,4,3,1,1,1,4,1,8,3,4,1,3,1, + 6,1,4,2,1,4,7,2,1,8,1,4,5,1,1,2,2,4,6,2,7,1,10,1,1,3,4,11,10,8,21,4,6,1,3,5,2,1,2,28,5,5,2,3,13,1,2,3,1,4,2,1,5,20,3,8,11,1,3,3,3,1,8,10,9,2, + 10,9,2,3,1,1,2,4,1,8,3,6,1,7,8,6,11,1,4,29,8,4,3,1,2,7,13,1,4,1,6,2,6,12,12,2,20,3,2,3,6,4,8,9,2,7,34,5,1,18,6,1,1,4,4,5,7,9,1,2,2,4,3,4,1,7, + 2,2,2,6,2,3,25,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,5,3,4,4,3,2,1,1,4,1,2,1,1,3,1,11,1,6,3,1,7,3,6,2,8,8,6,9,3,4,11,3,2,10,12,2,5,11,1,6,4,5, + 3,1,8,5,4,6,6,3,5,1,1,3,2,1,2,2,6,17,12,1,10,1,6,12,1,6,6,19,9,6,16,1,13,4,4,15,7,17,6,11,9,15,12,6,7,2,1,2,2,15,9,3,21,4,6,49,18,7,3,2,3,1, + 6,8,2,2,6,2,9,1,3,6,4,4,1,2,16,2,5,2,1,6,2,3,5,3,1,2,5,1,2,1,9,3,1,8,6,4,8,11,3,1,1,1,1,3,1,13,8,4,1,3,2,2,1,4,1,11,1,5,2,1,5,2,5,8,6,1,1,7, + 4,3,8,3,2,7,2,1,5,1,5,2,4,7,6,2,8,5,1,11,4,5,3,6,18,1,2,13,3,3,1,21,1,1,4,1,4,1,1,1,8,1,2,2,7,1,2,4,2,2,9,2,1,1,1,4,3,6,3,12,5,1,1,1,5,6,3,2, + 4,8,2,2,4,2,7,1,8,9,5,2,3,2,1,3,2,13,7,14,6,5,1,1,2,1,4,2,23,2,1,1,6,3,1,4,1,15,3,1,7,3,9,14,1,3,1,4,1,1,5,8,1,3,8,3,8,15,11,4,14,4,4,2,5,5, + 1,7,1,6,14,7,7,8,5,15,4,8,6,5,6,2,1,13,1,20,15,11,9,2,5,6,2,11,2,6,2,5,1,5,8,4,13,19,25,4,1,1,11,1,34,2,5,9,14,6,2,2,6,1,1,14,1,3,14,13,1,6, + 12,21,14,14,6,32,17,8,32,9,28,1,2,4,11,8,3,1,14,2,5,15,1,1,1,1,3,6,4,1,3,4,11,3,1,1,11,30,1,5,1,4,1,5,8,1,1,3,2,4,3,17,35,2,6,12,17,3,1,6,2, + 1,1,12,2,7,3,3,2,1,16,2,8,3,6,5,4,7,3,3,8,1,9,8,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,4,3,7,5,8,3,3,3,3,3,3,1,23,10,3,1,2,2,6,3,1,16,1,16, + 22,3,10,4,11,6,9,7,7,3,6,2,2,2,4,10,2,1,1,2,8,7,1,6,4,1,3,3,3,5,10,12,12,2,3,12,8,15,1,1,16,6,6,1,5,9,11,4,11,4,2,6,12,1,17,5,13,1,4,9,5,1,11, + 2,1,8,1,5,7,28,8,3,5,10,2,17,3,38,22,1,2,18,12,10,4,38,18,1,4,44,19,4,1,8,4,1,12,1,4,31,12,1,14,7,75,7,5,10,6,6,13,3,2,11,11,3,2,5,28,15,6,18, + 18,5,6,4,3,16,1,7,18,7,36,3,5,3,1,7,1,9,1,10,7,2,4,2,6,2,9,7,4,3,32,12,3,7,10,2,23,16,3,1,12,3,31,4,11,1,3,8,9,5,1,30,15,6,12,3,2,2,11,19,9, + 14,2,6,2,3,19,13,17,5,3,3,25,3,14,1,1,1,36,1,3,2,19,3,13,36,9,13,31,6,4,16,34,2,5,4,2,3,3,5,1,1,1,4,3,1,17,3,2,3,5,3,1,3,2,3,5,6,3,12,11,1,3, + 1,2,26,7,12,7,2,14,3,3,7,7,11,25,25,28,16,4,36,1,2,1,6,2,1,9,3,27,17,4,3,4,13,4,1,3,2,2,1,10,4,2,4,6,3,8,2,1,18,1,1,24,2,2,4,33,2,3,63,7,1,6, + 40,7,3,4,4,2,4,15,18,1,16,1,1,11,2,41,14,1,3,18,13,3,2,4,16,2,17,7,15,24,7,18,13,44,2,2,3,6,1,1,7,5,1,7,1,4,3,3,5,10,8,2,3,1,8,1,1,27,4,2,1, + 12,1,2,1,10,6,1,6,7,5,2,3,7,11,5,11,3,6,6,2,3,15,4,9,1,1,2,1,2,11,2,8,12,8,5,4,2,3,1,5,2,2,1,14,1,12,11,4,1,11,17,17,4,3,2,5,5,7,3,1,5,9,9,8, + 2,5,6,6,13,13,2,1,2,6,1,2,2,49,4,9,1,2,10,16,7,8,4,3,2,23,4,58,3,29,1,14,19,19,11,11,2,7,5,1,3,4,6,2,18,5,12,12,17,17,3,3,2,4,1,6,2,3,4,3,1, + 1,1,1,5,1,1,9,1,3,1,3,6,1,8,1,1,2,6,4,14,3,1,4,11,4,1,3,32,1,2,4,13,4,1,2,4,2,1,3,1,11,1,4,2,1,4,4,6,3,5,1,6,5,7,6,3,23,3,5,3,5,3,3,13,3,9,10, + 1,12,10,2,3,18,13,7,160,52,4,2,2,3,2,14,5,4,12,4,6,4,1,20,4,11,6,2,12,27,1,4,1,2,2,7,4,5,2,28,3,7,25,8,3,19,3,6,10,2,2,1,10,2,5,4,1,3,4,1,5, + 3,2,6,9,3,6,2,16,3,3,16,4,5,5,3,2,1,2,16,15,8,2,6,21,2,4,1,22,5,8,1,1,21,11,2,1,11,11,19,13,12,4,2,3,2,3,6,1,8,11,1,4,2,9,5,2,1,11,2,9,1,1,2, + 14,31,9,3,4,21,14,4,8,1,7,2,2,2,5,1,4,20,3,3,4,10,1,11,9,8,2,1,4,5,14,12,14,2,17,9,6,31,4,14,1,20,13,26,5,2,7,3,6,13,2,4,2,19,6,2,2,18,9,3,5, + 12,12,14,4,6,2,3,6,9,5,22,4,5,25,6,4,8,5,2,6,27,2,35,2,16,3,7,8,8,6,6,5,9,17,2,20,6,19,2,13,3,1,1,1,4,17,12,2,14,7,1,4,18,12,38,33,2,10,1,1, + 2,13,14,17,11,50,6,33,20,26,74,16,23,45,50,13,38,33,6,6,7,4,4,2,1,3,2,5,8,7,8,9,3,11,21,9,13,1,3,10,6,7,1,2,2,18,5,5,1,9,9,2,68,9,19,13,2,5, + 1,4,4,7,4,13,3,9,10,21,17,3,26,2,1,5,2,4,5,4,1,7,4,7,3,4,2,1,6,1,1,20,4,1,9,2,2,1,3,3,2,3,2,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,3,2,10,3,5,3,4,4, + 3,4,16,1,6,1,10,2,4,2,1,1,2,10,11,2,2,3,1,24,31,4,10,10,2,5,12,16,164,15,4,16,7,9,15,19,17,1,2,1,1,5,1,1,1,1,1,3,1,4,3,1,3,1,3,1,2,1,1,3,3,7, + 2,8,1,2,2,2,1,3,4,3,7,8,12,92,2,10,3,1,3,14,5,25,16,42,4,7,7,4,2,21,5,27,26,27,21,25,30,31,2,1,5,13,3,22,5,6,6,11,9,12,1,5,9,7,5,5,22,60,3,5, + 13,1,1,8,1,1,3,3,2,1,9,3,3,18,4,1,2,3,7,6,3,1,2,3,9,1,3,1,3,2,1,3,1,1,1,2,1,11,3,1,6,9,1,3,2,3,1,2,1,5,1,1,4,3,4,1,2,2,4,4,1,7,2,1,2,2,3,5,13, + 18,3,4,14,9,9,4,16,3,7,5,8,2,6,48,28,3,1,1,4,2,14,8,2,9,2,1,15,2,4,3,2,10,16,12,8,7,1,1,3,1,1,1,2,7,4,1,6,4,38,39,16,23,7,15,15,3,2,12,7,21, + 37,27,6,5,4,8,2,10,8,8,6,5,1,2,1,3,24,1,16,17,9,23,10,17,6,1,51,55,44,13,294,9,3,6,2,4,2,2,15,1,1,1,13,21,17,68,14,8,9,4,1,4,9,3,11,7,1,1,1, + 5,6,3,2,1,1,1,2,3,8,1,2,2,4,1,5,5,2,1,4,3,7,13,4,1,4,1,3,1,1,1,5,5,10,1,6,1,5,2,1,5,2,4,1,4,5,7,3,18,2,9,11,32,4,3,3,2,4,7,11,16,9,11,8,13,38, + 32,8,4,2,1,1,2,1,2,4,4,1,1,1,4,1,21,3,11,1,16,1,1,6,1,3,2,4,9,8,57,7,44,1,3,3,13,3,10,1,1,7,5,2,7,21,47,63,3,15,4,7,1,16,1,1,2,8,2,3,42,15,4, + 1,29,7,22,10,3,78,16,12,20,18,4,67,11,5,1,3,15,6,21,31,32,27,18,13,71,35,5,142,4,10,1,2,50,19,33,16,35,37,16,19,27,7,1,133,19,1,4,8,7,20,1,4, + 4,1,10,3,1,6,1,2,51,5,40,15,24,43,22928,11,1,13,154,70,3,1,1,7,4,10,1,2,1,1,2,1,2,1,2,2,1,1,2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1, + 3,2,1,1,1,1,2,1,1, + }; + static ImWchar base_ranges[] = // not zero-terminated + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid + }; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00)*2 + 1] = { 0 }; + if (!full_ranges[0]) + { + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + UnpackAccumulativeOffsetsIntoRanges(0x4E00, accumulative_offsets_from_0x4E00, IM_ARRAYSIZE(accumulative_offsets_from_0x4E00), full_ranges + IM_ARRAYSIZE(base_ranges)); + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesCyrillic() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement + 0x2DE0, 0x2DFF, // Cyrillic Extended-A + 0xA640, 0xA69F, // Cyrillic Extended-B + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesThai() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x2010, 0x205E, // Punctuations + 0x0E00, 0x0E7F, // Thai + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesVietnamese() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x0102, 0x0103, + 0x0110, 0x0111, + 0x0128, 0x0129, + 0x0168, 0x0169, + 0x01A0, 0x01A1, + 0x01AF, 0x01B0, + 0x1EA0, 0x1EF9, + 0, + }; + return &ranges[0]; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontGlyphRangesBuilder +//----------------------------------------------------------------------------- + +void ImFontGlyphRangesBuilder::AddText(const char* text, const char* text_end) +{ + while (text_end ? (text < text_end) : *text) + { + unsigned int c = 0; + int c_len = ImTextCharFromUtf8(&c, text, text_end); + text += c_len; + if (c_len == 0) + break; + AddChar((ImWchar)c); + } +} + +void ImFontGlyphRangesBuilder::AddRanges(const ImWchar* ranges) +{ + for (; ranges[0]; ranges += 2) + for (unsigned int c = ranges[0]; c <= ranges[1] && c <= IM_UNICODE_CODEPOINT_MAX; c++) //-V560 + AddChar((ImWchar)c); +} + +void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) +{ + const int max_codepoint = IM_UNICODE_CODEPOINT_MAX; + for (int n = 0; n <= max_codepoint; n++) + if (GetBit(n)) + { + out_ranges->push_back((ImWchar)n); + while (n < max_codepoint && GetBit(n + 1)) + n++; + out_ranges->push_back((ImWchar)n); + } + out_ranges->push_back(0); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFont +//----------------------------------------------------------------------------- + +ImFont::ImFont() +{ + memset(this, 0, sizeof(*this)); + Scale = 1.0f; +} + +ImFont::~ImFont() +{ + ClearOutputData(); +} + +void ImFont::ClearOutputData() +{ + FontSize = 0.0f; + FallbackAdvanceX = 0.0f; + Glyphs.clear(); + IndexAdvanceX.clear(); + IndexLookup.clear(); + FallbackGlyph = NULL; + ContainerAtlas = NULL; + DirtyLookupTables = true; + Ascent = Descent = 0.0f; + MetricsTotalSurface = 0; + memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap)); +} + +static ImWchar FindFirstExistingGlyph(ImFont* font, const ImWchar* candidate_chars, int candidate_chars_count) +{ + for (int n = 0; n < candidate_chars_count; n++) + if (font->FindGlyphNoFallback(candidate_chars[n]) != NULL) + return candidate_chars[n]; + return 0; +} + +void ImFont::BuildLookupTable() +{ + int max_codepoint = 0; + for (int i = 0; i != Glyphs.Size; i++) + max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); + + // Build lookup table + IM_ASSERT(Glyphs.Size > 0 && "Font has not loaded glyph!"); + IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved + IndexAdvanceX.clear(); + IndexLookup.clear(); + DirtyLookupTables = false; + memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap)); + GrowIndex(max_codepoint + 1); + for (int i = 0; i < Glyphs.Size; i++) + { + int codepoint = (int)Glyphs[i].Codepoint; + IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; + IndexLookup[codepoint] = (ImU16)i; + + // Mark 4K page as used + const int page_n = codepoint / 8192; + Used8kPagesMap[page_n >> 3] |= 1 << (page_n & 7); + } + + // Create a glyph to handle TAB + // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) + if (FindGlyph((ImWchar)' ')) + { + if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times (FIXME: Flaky) + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& tab_glyph = Glyphs.back(); + tab_glyph = *FindGlyph((ImWchar)' '); + tab_glyph.Codepoint = '\t'; + tab_glyph.AdvanceX *= IM_TABSIZE; + IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX; + IndexLookup[(int)tab_glyph.Codepoint] = (ImU16)(Glyphs.Size - 1); + } + + // Mark special glyphs as not visible (note that AddGlyph already mark as non-visible glyphs with zero-size polygons) + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)' ')) + glyph->Visible = false; + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)'\t')) + glyph->Visible = false; + + // Setup Fallback character + const ImWchar fallback_chars[] = { (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' }; + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackChar = FindFirstExistingGlyph(this, fallback_chars, IM_ARRAYSIZE(fallback_chars)); + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackGlyph = &Glyphs.back(); + FallbackChar = (ImWchar)FallbackGlyph->Codepoint; + } + } + FallbackAdvanceX = FallbackGlyph->AdvanceX; + for (int i = 0; i < max_codepoint + 1; i++) + if (IndexAdvanceX[i] < 0.0f) + IndexAdvanceX[i] = FallbackAdvanceX; + + // Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). + // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. + // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots. + const ImWchar ellipsis_chars[] = { Sources->EllipsisChar, (ImWchar)0x2026, (ImWchar)0x0085 }; + const ImWchar dots_chars[] = { (ImWchar)'.', (ImWchar)0xFF0E }; + if (EllipsisChar == 0) + EllipsisChar = FindFirstExistingGlyph(this, ellipsis_chars, IM_ARRAYSIZE(ellipsis_chars)); + const ImWchar dot_char = FindFirstExistingGlyph(this, dots_chars, IM_ARRAYSIZE(dots_chars)); + if (EllipsisChar != 0) + { + EllipsisCharCount = 1; + EllipsisWidth = EllipsisCharStep = FindGlyph(EllipsisChar)->X1; + } + else if (dot_char != 0) + { + const ImFontGlyph* dot_glyph = FindGlyph(dot_char); + EllipsisChar = dot_char; + EllipsisCharCount = 3; + EllipsisCharStep = (float)(int)(dot_glyph->X1 - dot_glyph->X0) + 1.0f; + EllipsisWidth = ImMax(dot_glyph->AdvanceX, dot_glyph->X0 + EllipsisCharStep * 3.0f - 1.0f); // FIXME: Slightly odd for normally mono-space fonts but since this is used for trailing contents. + } +} + +// API is designed this way to avoid exposing the 8K page size +// e.g. use with IsGlyphRangeUnused(0, 255) +bool ImFont::IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last) +{ + unsigned int page_begin = (c_begin / 8192); + unsigned int page_last = (c_last / 8192); + for (unsigned int page_n = page_begin; page_n <= page_last; page_n++) + if ((page_n >> 3) < sizeof(Used8kPagesMap)) + if (Used8kPagesMap[page_n >> 3] & (1 << (page_n & 7))) + return false; + return true; +} + +void ImFont::GrowIndex(int new_size) +{ + IM_ASSERT(IndexAdvanceX.Size == IndexLookup.Size); + if (new_size <= IndexLookup.Size) + return; + IndexAdvanceX.resize(new_size, -1.0f); + IndexLookup.resize(new_size, (ImU16)-1); +} + +// x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero. +// Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis). +// 'src' is not necessarily == 'this->Sources' because multiple source fonts+configs can be used to build one target font. +void ImFont::AddGlyph(const ImFontConfig* src, ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) +{ + if (src != NULL) + { + // Clamp & recenter if needed + const float advance_x_original = advance_x; + advance_x = ImClamp(advance_x, src->GlyphMinAdvanceX, src->GlyphMaxAdvanceX); + if (advance_x != advance_x_original) + { + float char_off_x = src->PixelSnapH ? ImTrunc((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; + x0 += char_off_x; + x1 += char_off_x; + } + + // Snap to pixel + if (src->PixelSnapH) + advance_x = IM_ROUND(advance_x); + + // Bake extra spacing + advance_x += src->GlyphExtraAdvanceX; + } + + int glyph_idx = Glyphs.Size; + Glyphs.resize(Glyphs.Size + 1); + ImFontGlyph& glyph = Glyphs[glyph_idx]; + glyph.Codepoint = (unsigned int)codepoint; + glyph.Visible = (x0 != x1) && (y0 != y1); + glyph.Colored = false; + glyph.X0 = x0; + glyph.Y0 = y0; + glyph.X1 = x1; + glyph.Y1 = y1; + glyph.U0 = u0; + glyph.V0 = v0; + glyph.U1 = u1; + glyph.V1 = v1; + glyph.AdvanceX = advance_x; + IM_ASSERT(Glyphs.Size < 0xFFFF); // IndexLookup[] hold 16-bit values and -1 is reserved. + + // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) + // We use (U1-U0)*TexWidth instead of X1-X0 to account for oversampling. + float pad = ContainerAtlas->TexGlyphPadding + 0.99f; + DirtyLookupTables = true; + MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + pad) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + pad); +} + +void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) +{ + IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. + unsigned int index_size = (unsigned int)IndexLookup.Size; + + if (dst < index_size && IndexLookup.Data[dst] == (ImU16)-1 && !overwrite_dst) // 'dst' already exists + return; + if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op + return; + + GrowIndex(dst + 1); + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImU16)-1; + IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; +} + +// Find glyph, return fallback if missing +ImFontGlyph* ImFont::FindGlyph(ImWchar c) +{ + if (c >= (size_t)IndexLookup.Size) + return FallbackGlyph; + const ImU16 i = IndexLookup.Data[c]; + if (i == (ImU16)-1) + return FallbackGlyph; + return &Glyphs.Data[i]; +} + +ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) +{ + if (c >= (size_t)IndexLookup.Size) + return NULL; + const ImU16 i = IndexLookup.Data[c]; + if (i == (ImU16)-1) + return NULL; + return &Glyphs.Data[i]; +} + +// Trim trailing space and find beginning of next line +static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end) +{ + while (text < text_end && ImCharIsBlankA(*text)) + text++; + if (*text == '\n') + text++; + return text; +} + +#define ImFontGetCharAdvanceX(_FONT, _CH) ((int)(_CH) < (_FONT)->IndexAdvanceX.Size ? (_FONT)->IndexAdvanceX.Data[_CH] : (_FONT)->FallbackAdvanceX) + +// Simple word-wrapping for English, not full-featured. Please submit failing cases! +// This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. +// FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) +{ + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ + + // List of hardcoded separators: .,;!?'" + + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" --> "Hello" "World" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + IM_ASSERT(text_end != NULL); + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + const char* next_s; + if (c < 0x80) + next_s = s + 1; + else + next_s = s + ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + if (c == '\r') + { + s = next_s; + continue; + } + } + + const float char_width = ImFontGetCharAdvanceX(this, c); + if (ImCharIsBlankW(c)) + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + word_end = s; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width > wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + // +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol). + if (s == text && text < text_end) + return s + 1; + return s; +} + +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) +{ + if (!text_end) + text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this. + + const float line_height = size; + const float scale = size / FontSize; + + ImVec2 text_size = ImVec2(0, 0); + float line_width = 0.0f; + + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + const char* s = text_begin; + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + continue; + } + } + + // Decode and advance source + const char* prev_s = s; + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; + } + if (c == '\r') + continue; + } + + const float char_width = ImFontGetCharAdvanceX(this, c) * scale; + if (line_width + char_width >= max_width) + { + s = prev_s; + break; + } + + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (line_width > 0 || text_size.y == 0.0f) + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) +{ + const ImFontGlyph* glyph = FindGlyph(c); + if (!glyph || !glyph->Visible) + return; + if (glyph->Colored) + col |= ~IM_COL32_A_MASK; + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(x + glyph->X0 * scale, y + glyph->Y0 * scale), ImVec2(x + glyph->X1 * scale, y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); +} + +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) +{ + // Align to be pixel perfect + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); + if (y > clip_rect.w) + return; + + if (!text_end) + text_end = text_begin + ImStrlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. + + const float scale = size / FontSize; + const float line_height = FontSize * scale; + const float origin_x = x; + const bool word_wrap_enabled = (wrap_width > 0.0f); + + // Fast-forward to first visible line + const char* s = text_begin; + if (y + line_height < clip_rect.y) + while (y + line_height < clip_rect.y && s < text_end) + { + const char* line_end = (const char*)ImMemchr(s, '\n', text_end - s); + if (word_wrap_enabled) + { + // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPositionA(). + // If the specs for CalcWordWrapPositionA() were reworked to optionally return on \n we could combine both. + // However it is still better than nothing performing the fast-forward! + s = CalcWordWrapPositionA(scale, s, line_end ? line_end : text_end, wrap_width); + s = CalcWordWrapNextLineStartA(s, text_end); + } + else + { + s = line_end ? line_end + 1 : text_end; + } + y += line_height; + } + + // For large text, scan for the last visible line in order to avoid over-reserving in the call to PrimReserve() + // Note that very large horizontal line will still be affected by the issue (e.g. a one megabyte string buffer without a newline will likely crash atm) + if (text_end - s > 10000 && !word_wrap_enabled) + { + const char* s_end = s; + float y_end = y; + while (y_end < clip_rect.w && s_end < text_end) + { + s_end = (const char*)ImMemchr(s_end, '\n', text_end - s_end); + s_end = s_end ? s_end + 1 : text_end; + y_end += line_height; + } + text_end = s_end; + } + if (s == text_end) + return; + + // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) + const int vtx_count_max = (int)(text_end - s) * 4; + const int idx_count_max = (int)(text_end - s) * 6; + const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; + draw_list->PrimReserve(idx_count_max, vtx_count_max); + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_index = draw_list->_VtxCurrentIdx; + + const ImU32 col_untinted = col | ~IM_COL32_A_MASK; + const char* word_wrap_eol = NULL; + + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - origin_x)); + + if (s >= word_wrap_eol) + { + x = origin_x; + y += line_height; + if (y > clip_rect.w) + break; // break out of main loop + word_wrap_eol = NULL; + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks + continue; + } + } + + // Decode and advance source + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + + if (c < 32) + { + if (c == '\n') + { + x = origin_x; + y += line_height; + if (y > clip_rect.w) + break; // break out of main loop + continue; + } + if (c == '\r') + continue; + } + + const ImFontGlyph* glyph = FindGlyph((ImWchar)c); + if (glyph == NULL) + continue; + + float char_width = glyph->AdvanceX * scale; + if (glyph->Visible) + { + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) + { + if (x1 < clip_rect.x) + { + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; + } + if (y1 < clip_rect.y) + { + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; + } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // Support for untinted glyphs + ImU32 glyph_col = glyph->Colored ? col_untinted : col; + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = glyph_col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = glyph_col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = glyph_col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = glyph_col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + idx_write[0] = (ImDrawIdx)(vtx_index); idx_write[1] = (ImDrawIdx)(vtx_index + 1); idx_write[2] = (ImDrawIdx)(vtx_index + 2); + idx_write[3] = (ImDrawIdx)(vtx_index); idx_write[4] = (ImDrawIdx)(vtx_index + 2); idx_write[5] = (ImDrawIdx)(vtx_index + 3); + vtx_write += 4; + vtx_index += 4; + idx_write += 6; + } + } + } + x += char_width; + } + + // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. + draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data); // Same as calling shrink() + draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + draw_list->_VtxWritePtr = vtx_write; + draw_list->_IdxWritePtr = idx_write; + draw_list->_VtxCurrentIdx = vtx_index; +} + +//----------------------------------------------------------------------------- +// [SECTION] ImGui Internal Render Helpers +//----------------------------------------------------------------------------- +// Vaguely redesigned to stop accessing ImGui global state: +// - RenderArrow() +// - RenderBullet() +// - RenderCheckMark() +// - RenderArrowPointingAt() +// - RenderRectFilledRangeH() +// - RenderRectFilledWithHole() +//----------------------------------------------------------------------------- +// Function in need of a redesign (legacy mess) +// - RenderColorRectWithAlphaCheckerboard() +//----------------------------------------------------------------------------- + +// Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state +void ImGui::RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale) +{ + const float h = draw_list->_Data->FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = pos + ImVec2(h * 0.50f, h * 0.50f * scale); + + ImVec2 a, b, c; + switch (dir) + { + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + a = ImVec2(+0.000f, +0.750f) * r; + b = ImVec2(-0.866f, -0.750f) * r; + c = ImVec2(+0.866f, -0.750f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + a = ImVec2(+0.750f, +0.000f) * r; + b = ImVec2(-0.750f, +0.866f) * r; + c = ImVec2(-0.750f, -0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_COUNT: + IM_ASSERT(0); + break; + } + draw_list->AddTriangleFilled(center + a, center + b, center + c, col); +} + +void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) +{ + // FIXME-OPT: This should be baked in font. + draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8); +} + +void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) +{ + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness * 0.5f; + pos += ImVec2(thickness * 0.25f, thickness * 0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third * 0.5f; + draw_list->PathLineTo(ImVec2(bx - third, by - third)); + draw_list->PathLineTo(ImVec2(bx, by)); + draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f)); + draw_list->PathStroke(col, 0, thickness); +} + +// Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. +void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) +{ + switch (direction) + { + case ImGuiDir_Left: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Right: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_Up: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Down: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_None: case ImGuiDir_COUNT: break; // Fix warnings + } +} + +static inline float ImAcos01(float x) +{ + if (x <= 0.0f) return IM_PI * 0.5f; + if (x >= 1.0f) return 0.0f; + return ImAcos(x); + //return (-0.69813170079773212f * x * x - 0.87266462599716477f) * x + 1.5707963267948966f; // Cheap approximation, may be enough for what we do. +} + +// FIXME: Cleanup and move code to ImDrawList. +void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) +{ + if (x_end_norm == x_start_norm) + return; + if (x_start_norm > x_end_norm) + ImSwap(x_start_norm, x_end_norm); + + ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); + ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); + if (rounding == 0.0f) + { + draw_list->AddRectFilled(p0, p1, col, 0.0f); + return; + } + + rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); + const float inv_rounding = 1.0f / rounding; + const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); + const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); + const float half_pi = IM_PI * 0.5f; // We will == compare to this because we know this is the exact value ImAcos01 can return. + const float x0 = ImMax(p0.x, rect.Min.x + rounding); + if (arc0_b == arc0_e) + { + draw_list->PathLineTo(ImVec2(x0, p1.y)); + draw_list->PathLineTo(ImVec2(x0, p0.y)); + } + else if (arc0_b == 0.0f && arc0_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL + draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR + } + else + { + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e); // TR + } + if (p1.x > rect.Min.x + rounding) + { + const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); + const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); + const float x1 = ImMin(p1.x, rect.Max.x - rounding); + if (arc1_b == arc1_e) + { + draw_list->PathLineTo(ImVec2(x1, p0.y)); + draw_list->PathLineTo(ImVec2(x1, p1.y)); + } + else if (arc1_b == 0.0f && arc1_e == half_pi) + { + draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR + draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR + } + else + { + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e); // BR + } + } + draw_list->PathFillConvex(col); +} + +void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding) +{ + const bool fill_L = (inner.Min.x > outer.Min.x); + const bool fill_R = (inner.Max.x < outer.Max.x); + const bool fill_U = (inner.Min.y > outer.Min.y); + const bool fill_D = (inner.Max.y < outer.Max.y); + if (fill_L) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomLeft)); + if (fill_R) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopRight) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_U) draw_list->AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersTopRight)); + if (fill_D) draw_list->AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersBottomLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_L && fill_U) draw_list->AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopLeft); + if (fill_R && fill_U) draw_list->AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopRight); + if (fill_L && fill_D) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomLeft); + if (fill_R && fill_D) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomRight); +} + +// Helper for ColorPicker4() +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. +// FIXME: uses ImGui::GetColorU32 +void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, ImDrawFlags flags) +{ + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags = ImDrawFlags_RoundCornersDefault_; + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col)); + draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; + if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; } + + // Combine flags + cell_flags = (flags == ImDrawFlags_RoundCornersNone || cell_flags == ImDrawFlags_RoundCornersNone) ? ImDrawFlags_RoundCornersNone : (cell_flags & flags); + draw_list->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding, cell_flags); + } + } + } + else + { + draw_list->AddRectFilled(p_min, p_max, col, rounding, flags); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Decompression code +//----------------------------------------------------------------------------- +// Compressed with stb_compress() then converted to a C array and encoded as base85. +// Use the program in misc/fonts/binary_to_compressed_c.cpp to create the array from a TTF file. +// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. +// Decompression from stb.h (public domain) by Sean Barrett https://github.com/nothings/stb/blob/master/stb.h +//----------------------------------------------------------------------------- + +static unsigned int stb_decompress_length(const unsigned char *input) +{ + return (input[8] << 24) + (input[9] << 16) + (input[10] << 8) + input[11]; +} + +static unsigned char *stb__barrier_out_e, *stb__barrier_out_b; +static const unsigned char *stb__barrier_in_b; +static unsigned char *stb__dout; +static void stb__match(const unsigned char *data, unsigned int length) +{ + // INVERSE of memmove... write each byte before copying the next... + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_out_b) { stb__dout = stb__barrier_out_e+1; return; } + while (length--) *stb__dout++ = *data++; +} + +static void stb__lit(const unsigned char *data, unsigned int length) +{ + IM_ASSERT(stb__dout + length <= stb__barrier_out_e); + if (stb__dout + length > stb__barrier_out_e) { stb__dout += length; return; } + if (data < stb__barrier_in_b) { stb__dout = stb__barrier_out_e+1; return; } + memcpy(stb__dout, data, length); + stb__dout += length; +} + +#define stb__in2(x) ((i[x] << 8) + i[(x)+1]) +#define stb__in3(x) ((i[x] << 16) + stb__in2((x)+1)) +#define stb__in4(x) ((i[x] << 24) + stb__in3((x)+1)) + +static const unsigned char *stb_decompress_token(const unsigned char *i) +{ + if (*i >= 0x20) { // use fewer if's for cases that expand small + if (*i >= 0x80) stb__match(stb__dout-i[1]-1, i[0] - 0x80 + 1), i += 2; + else if (*i >= 0x40) stb__match(stb__dout-(stb__in2(0) - 0x4000 + 1), i[2]+1), i += 3; + else /* *i >= 0x20 */ stb__lit(i+1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); + } else { // more ifs for cases that expand large, since overhead is amortized + if (*i >= 0x18) stb__match(stb__dout-(stb__in3(0) - 0x180000 + 1), i[3]+1), i += 4; + else if (*i >= 0x10) stb__match(stb__dout-(stb__in3(0) - 0x100000 + 1), stb__in2(3)+1), i += 5; + else if (*i >= 0x08) stb__lit(i+2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); + else if (*i == 0x07) stb__lit(i+3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); + else if (*i == 0x06) stb__match(stb__dout-(stb__in3(1)+1), i[4]+1), i += 5; + else if (*i == 0x04) stb__match(stb__dout-(stb__in3(1)+1), stb__in2(4)+1), i += 6; + } + return i; +} + +static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, unsigned int buflen) +{ + const unsigned long ADLER_MOD = 65521; + unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; + unsigned long blocklen = buflen % 5552; + + unsigned long i; + while (buflen) { + for (i=0; i + 7 < blocklen; i += 8) { + s1 += buffer[0], s2 += s1; + s1 += buffer[1], s2 += s1; + s1 += buffer[2], s2 += s1; + s1 += buffer[3], s2 += s1; + s1 += buffer[4], s2 += s1; + s1 += buffer[5], s2 += s1; + s1 += buffer[6], s2 += s1; + s1 += buffer[7], s2 += s1; + + buffer += 8; + } + + for (; i < blocklen; ++i) + s1 += *buffer++, s2 += s1; + + s1 %= ADLER_MOD, s2 %= ADLER_MOD; + buflen -= blocklen; + blocklen = 5552; + } + return (unsigned int)(s2 << 16) + (unsigned int)s1; +} + +static unsigned int stb_decompress(unsigned char *output, const unsigned char *i, unsigned int /*length*/) +{ + if (stb__in4(0) != 0x57bC0000) return 0; + if (stb__in4(4) != 0) return 0; // error! stream is > 4GB + const unsigned int olen = stb_decompress_length(i); + stb__barrier_in_b = i; + stb__barrier_out_e = output + olen; + stb__barrier_out_b = output; + i += 16; + + stb__dout = output; + for (;;) { + const unsigned char *old_i = i; + i = stb_decompress_token(i); + if (i == old_i) { + if (*i == 0x05 && i[1] == 0xfa) { + IM_ASSERT(stb__dout == output + olen); + if (stb__dout != output + olen) return 0; + if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) + return 0; + return olen; + } else { + IM_ASSERT(0); /* NOTREACHED */ + return 0; + } + } + IM_ASSERT(stb__dout <= output + olen); + if (stb__dout > output + olen) + return 0; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Default font data (ProggyClean.ttf) +//----------------------------------------------------------------------------- +// ProggyClean.ttf +// Copyright (c) 2004, 2005 Tristan Grimmer +// MIT license (see License.txt in http://www.proggyfonts.net/index.php?menu=download) +// Download and more information at http://www.proggyfonts.net or http://upperboundsinteractive.com/fonts.php +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEFAULT_FONT + +// File: 'ProggyClean.ttf' (41208 bytes) +// Exported using binary_to_compressed_c.exe -u8 "ProggyClean.ttf" proggy_clean_ttf +static const unsigned int proggy_clean_ttf_compressed_size = 9583; +static const unsigned char proggy_clean_ttf_compressed_data[9583] = +{ + 87,188,0,0,0,0,0,0,0,0,160,248,0,4,0,0,55,0,1,0,0,0,12,0,128,0,3,0,64,79,83,47,50,136,235,116,144,0,0,1,72,130,21,44,78,99,109,97,112,2,18,35,117,0,0,3,160,130,19,36,82,99,118,116, + 32,130,23,130,2,33,4,252,130,4,56,2,103,108,121,102,18,175,137,86,0,0,7,4,0,0,146,128,104,101,97,100,215,145,102,211,130,27,32,204,130,3,33,54,104,130,16,39,8,66,1,195,0,0,1,4,130, + 15,59,36,104,109,116,120,138,0,126,128,0,0,1,152,0,0,2,6,108,111,99,97,140,115,176,216,0,0,5,130,30,41,2,4,109,97,120,112,1,174,0,218,130,31,32,40,130,16,44,32,110,97,109,101,37,89, + 187,150,0,0,153,132,130,19,44,158,112,111,115,116,166,172,131,239,0,0,155,36,130,51,44,210,112,114,101,112,105,2,1,18,0,0,4,244,130,47,32,8,132,203,46,1,0,0,60,85,233,213,95,15,60, + 245,0,3,8,0,131,0,34,183,103,119,130,63,43,0,0,189,146,166,215,0,0,254,128,3,128,131,111,130,241,33,2,0,133,0,32,1,130,65,38,192,254,64,0,0,3,128,131,16,130,5,32,1,131,7,138,3,33,2, + 0,130,17,36,1,1,0,144,0,130,121,130,23,38,2,0,8,0,64,0,10,130,9,32,118,130,9,130,6,32,0,130,59,33,1,144,131,200,35,2,188,2,138,130,16,32,143,133,7,37,1,197,0,50,2,0,131,0,33,4,9,131, + 5,145,3,43,65,108,116,115,0,64,0,0,32,172,8,0,131,0,35,5,0,1,128,131,77,131,3,33,3,128,191,1,33,1,128,130,184,35,0,0,128,0,130,3,131,11,32,1,130,7,33,0,128,131,1,32,1,136,9,32,0,132, + 15,135,5,32,1,131,13,135,27,144,35,32,1,149,25,131,21,32,0,130,0,32,128,132,103,130,35,132,39,32,0,136,45,136,97,133,17,130,5,33,0,0,136,19,34,0,128,1,133,13,133,5,32,128,130,15,132, + 131,32,3,130,5,32,3,132,27,144,71,32,0,133,27,130,29,130,31,136,29,131,63,131,3,65,63,5,132,5,132,205,130,9,33,0,0,131,9,137,119,32,3,132,19,138,243,130,55,32,1,132,35,135,19,131,201, + 136,11,132,143,137,13,130,41,32,0,131,3,144,35,33,128,0,135,1,131,223,131,3,141,17,134,13,136,63,134,15,136,53,143,15,130,96,33,0,3,131,4,130,3,34,28,0,1,130,5,34,0,0,76,130,17,131, + 9,36,28,0,4,0,48,130,17,46,8,0,8,0,2,0,0,0,127,0,255,32,172,255,255,130,9,34,0,0,129,132,9,130,102,33,223,213,134,53,132,22,33,1,6,132,6,64,4,215,32,129,165,216,39,177,0,1,141,184, + 1,255,133,134,45,33,198,0,193,1,8,190,244,1,28,1,158,2,20,2,136,2,252,3,20,3,88,3,156,3,222,4,20,4,50,4,80,4,98,4,162,5,22,5,102,5,188,6,18,6,116,6,214,7,56,7,126,7,236,8,78,8,108, + 8,150,8,208,9,16,9,74,9,136,10,22,10,128,11,4,11,86,11,200,12,46,12,130,12,234,13,94,13,164,13,234,14,80,14,150,15,40,15,176,16,18,16,116,16,224,17,82,17,182,18,4,18,110,18,196,19, + 76,19,172,19,246,20,88,20,174,20,234,21,64,21,128,21,166,21,184,22,18,22,126,22,198,23,52,23,142,23,224,24,86,24,186,24,238,25,54,25,150,25,212,26,72,26,156,26,240,27,92,27,200,28, + 4,28,76,28,150,28,234,29,42,29,146,29,210,30,64,30,142,30,224,31,36,31,118,31,166,31,166,32,16,130,1,52,46,32,138,32,178,32,200,33,20,33,116,33,152,33,238,34,98,34,134,35,12,130,1, + 33,128,35,131,1,60,152,35,176,35,216,36,0,36,74,36,104,36,144,36,174,37,6,37,96,37,130,37,248,37,248,38,88,38,170,130,1,8,190,216,39,64,39,154,40,10,40,104,40,168,41,14,41,32,41,184, + 41,248,42,54,42,96,42,96,43,2,43,42,43,94,43,172,43,230,44,32,44,52,44,154,45,40,45,92,45,120,45,170,45,232,46,38,46,166,47,38,47,182,47,244,48,94,48,200,49,62,49,180,50,30,50,158, + 51,30,51,130,51,238,52,92,52,206,53,58,53,134,53,212,54,38,54,114,54,230,55,118,55,216,56,58,56,166,57,18,57,116,57,174,58,46,58,154,59,6,59,124,59,232,60,58,60,150,61,34,61,134,61, + 236,62,86,62,198,63,42,63,154,64,18,64,106,64,208,65,54,65,162,66,8,66,64,66,122,66,184,66,240,67,98,67,204,68,42,68,138,68,238,69,88,69,182,69,226,70,84,70,180,71,20,71,122,71,218, + 72,84,72,198,73,64,0,36,70,21,8,8,77,3,0,7,0,11,0,15,0,19,0,23,0,27,0,31,0,35,0,39,0,43,0,47,0,51,0,55,0,59,0,63,0,67,0,71,0,75,0,79,0,83,0,87,0,91,0,95,0,99,0,103,0,107,0,111,0,115, + 0,119,0,123,0,127,0,131,0,135,0,139,0,143,0,0,17,53,51,21,49,150,3,32,5,130,23,32,33,130,3,211,7,151,115,32,128,133,0,37,252,128,128,2,128,128,190,5,133,74,32,4,133,6,206,5,42,0,7, + 1,128,0,0,2,0,4,0,0,65,139,13,37,0,1,53,51,21,7,146,3,32,3,130,19,32,1,141,133,32,3,141,14,131,13,38,255,0,128,128,0,6,1,130,84,35,2,128,4,128,140,91,132,89,32,51,65,143,6,139,7,33, + 1,0,130,57,32,254,130,3,32,128,132,4,32,4,131,14,138,89,35,0,0,24,0,130,0,33,3,128,144,171,66,55,33,148,115,65,187,19,32,5,130,151,143,155,163,39,32,1,136,182,32,253,134,178,132,7, + 132,200,145,17,32,3,65,48,17,165,17,39,0,0,21,0,128,255,128,3,65,175,17,65,3,27,132,253,131,217,139,201,155,233,155,27,131,67,131,31,130,241,33,255,0,131,181,137,232,132,15,132,4,138, + 247,34,255,0,128,179,238,32,0,130,0,32,20,65,239,48,33,0,19,67,235,10,32,51,65,203,14,65,215,11,32,7,154,27,135,39,32,33,130,35,33,128,128,130,231,32,253,132,231,32,128,132,232,34, + 128,128,254,133,13,136,8,32,253,65,186,5,130,36,130,42,176,234,133,231,34,128,0,0,66,215,44,33,0,1,68,235,6,68,211,19,32,49,68,239,14,139,207,139,47,66,13,7,32,51,130,47,33,1,0,130, + 207,35,128,128,1,0,131,222,131,5,130,212,130,6,131,212,32,0,130,10,133,220,130,233,130,226,32,254,133,255,178,233,39,3,1,128,3,0,2,0,4,68,15,7,68,99,12,130,89,130,104,33,128,4,133, + 93,130,10,38,0,0,11,1,0,255,0,68,63,16,70,39,9,66,215,8,32,7,68,77,6,68,175,14,32,29,68,195,6,132,7,35,2,0,128,255,131,91,132,4,65,178,5,141,111,67,129,23,165,135,140,107,142,135,33, + 21,5,69,71,6,131,7,33,1,0,140,104,132,142,130,4,137,247,140,30,68,255,12,39,11,0,128,0,128,3,0,3,69,171,15,67,251,7,65,15,8,66,249,11,65,229,7,67,211,7,66,13,7,35,1,128,128,254,133, + 93,32,254,131,145,132,4,132,18,32,2,151,128,130,23,34,0,0,9,154,131,65,207,8,68,107,15,68,51,7,32,7,70,59,7,135,121,130,82,32,128,151,111,41,0,0,4,0,128,255,0,1,128,1,137,239,33,0, + 37,70,145,10,65,77,10,65,212,14,37,0,0,0,5,0,128,66,109,5,70,123,10,33,0,19,72,33,18,133,237,70,209,11,33,0,2,130,113,137,119,136,115,33,1,0,133,43,130,5,34,0,0,10,69,135,6,70,219, + 13,66,155,7,65,9,12,66,157,11,66,9,11,32,7,130,141,132,252,66,151,9,137,9,66,15,30,36,0,20,0,128,0,130,218,71,11,42,68,51,8,65,141,7,73,19,15,69,47,23,143,39,66,81,7,32,1,66,55,6,34, + 1,128,128,68,25,5,69,32,6,137,6,136,25,32,254,131,42,32,3,66,88,26,148,26,32,0,130,0,32,14,164,231,70,225,12,66,233,7,67,133,19,71,203,15,130,161,32,255,130,155,32,254,139,127,134, + 12,164,174,33,0,15,164,159,33,59,0,65,125,20,66,25,7,32,5,68,191,6,66,29,7,144,165,65,105,9,35,128,128,255,0,137,2,133,182,164,169,33,128,128,197,171,130,155,68,235,7,32,21,70,77,19, + 66,21,10,68,97,8,66,30,5,66,4,43,34,0,17,0,71,19,41,65,253,20,71,25,23,65,91,15,65,115,7,34,2,128,128,66,9,8,130,169,33,1,0,66,212,13,132,28,72,201,43,35,0,0,0,18,66,27,38,76,231,5, + 68,157,20,135,157,32,7,68,185,13,65,129,28,66,20,5,32,253,66,210,11,65,128,49,133,61,32,0,65,135,6,74,111,37,72,149,12,66,203,19,65,147,19,68,93,7,68,85,8,76,4,5,33,255,0,133,129,34, + 254,0,128,68,69,8,181,197,34,0,0,12,65,135,32,65,123,20,69,183,27,133,156,66,50,5,72,87,10,67,137,32,33,0,19,160,139,78,251,13,68,55,20,67,119,19,65,91,36,69,177,15,32,254,143,16,65, + 98,53,32,128,130,0,32,0,66,43,54,70,141,23,66,23,15,131,39,69,47,11,131,15,70,129,19,74,161,9,36,128,255,0,128,254,130,153,65,148,32,67,41,9,34,0,0,4,79,15,5,73,99,10,71,203,8,32,3, + 72,123,6,72,43,8,32,2,133,56,131,99,130,9,34,0,0,6,72,175,5,73,159,14,144,63,135,197,132,189,133,66,33,255,0,73,6,7,70,137,12,35,0,0,0,10,130,3,73,243,25,67,113,12,65,73,7,69,161,7, + 138,7,37,21,2,0,128,128,254,134,3,73,116,27,33,128,128,130,111,39,12,0,128,1,0,3,128,2,72,219,21,35,43,0,47,0,67,47,20,130,111,33,21,1,68,167,13,81,147,8,133,230,32,128,77,73,6,32, + 128,131,142,134,18,130,6,32,255,75,18,12,131,243,37,128,0,128,3,128,3,74,231,21,135,123,32,29,134,107,135,7,32,21,74,117,7,135,7,134,96,135,246,74,103,23,132,242,33,0,10,67,151,28, + 67,133,20,66,141,11,131,11,32,3,77,71,6,32,128,130,113,32,1,81,4,6,134,218,66,130,24,131,31,34,0,26,0,130,0,77,255,44,83,15,11,148,155,68,13,7,32,49,78,231,18,79,7,11,73,243,11,32, + 33,65,187,10,130,63,65,87,8,73,239,19,35,0,128,1,0,131,226,32,252,65,100,6,32,128,139,8,33,1,0,130,21,32,253,72,155,44,73,255,20,32,128,71,67,8,81,243,39,67,15,20,74,191,23,68,121, + 27,32,1,66,150,6,32,254,79,19,11,131,214,32,128,130,215,37,2,0,128,253,0,128,136,5,65,220,24,147,212,130,210,33,0,24,72,219,42,84,255,13,67,119,16,69,245,19,72,225,19,65,3,15,69,93, + 19,131,55,132,178,71,115,14,81,228,6,142,245,33,253,0,132,43,172,252,65,16,11,75,219,8,65,219,31,66,223,24,75,223,10,33,29,1,80,243,10,66,175,8,131,110,134,203,133,172,130,16,70,30, + 7,164,183,130,163,32,20,65,171,48,65,163,36,65,143,23,65,151,19,65,147,13,65,134,17,133,17,130,216,67,114,5,164,217,65,137,12,72,147,48,79,71,19,74,169,22,80,251,8,65,173,7,66,157, + 15,74,173,15,32,254,65,170,8,71,186,45,72,131,6,77,143,40,187,195,152,179,65,123,38,68,215,57,68,179,15,65,85,7,69,187,14,32,21,66,95,15,67,19,25,32,1,83,223,6,32,2,76,240,7,77,166, + 43,65,8,5,130,206,32,0,67,39,54,143,167,66,255,19,82,193,11,151,47,85,171,5,67,27,17,132,160,69,172,11,69,184,56,66,95,6,33,12,1,130,237,32,2,68,179,27,68,175,16,80,135,15,72,55,7, + 71,87,12,73,3,12,132,12,66,75,32,76,215,5,169,139,147,135,148,139,81,12,12,81,185,36,75,251,7,65,23,27,76,215,9,87,165,12,65,209,15,72,157,7,65,245,31,32,128,71,128,6,32,1,82,125,5, + 34,0,128,254,131,169,32,254,131,187,71,180,9,132,27,32,2,88,129,44,32,0,78,47,40,65,79,23,79,171,14,32,21,71,87,8,72,15,14,65,224,33,130,139,74,27,62,93,23,7,68,31,7,75,27,7,139,15, + 74,3,7,74,23,27,65,165,11,65,177,15,67,123,5,32,1,130,221,32,252,71,96,5,74,12,12,133,244,130,25,34,1,0,128,130,2,139,8,93,26,8,65,9,32,65,57,14,140,14,32,0,73,79,67,68,119,11,135, + 11,32,51,90,75,14,139,247,65,43,7,131,19,139,11,69,159,11,65,247,6,36,1,128,128,253,0,90,71,9,33,1,0,132,14,32,128,89,93,14,69,133,6,130,44,131,30,131,6,65,20,56,33,0,16,72,179,40, + 75,47,12,65,215,19,74,95,19,65,43,11,131,168,67,110,5,75,23,17,69,106,6,75,65,5,71,204,43,32,0,80,75,47,71,203,15,159,181,68,91,11,67,197,7,73,101,13,68,85,6,33,128,128,130,214,130, + 25,32,254,74,236,48,130,194,37,0,18,0,128,255,128,77,215,40,65,139,64,32,51,80,159,10,65,147,39,130,219,84,212,43,130,46,75,19,97,74,33,11,65,201,23,65,173,31,33,1,0,79,133,6,66,150, + 5,67,75,48,85,187,6,70,207,37,32,71,87,221,13,73,163,14,80,167,15,132,15,83,193,19,82,209,8,78,99,9,72,190,11,77,110,49,89,63,5,80,91,35,99,63,32,70,235,23,81,99,10,69,148,10,65,110, + 36,32,0,65,99,47,95,219,11,68,171,51,66,87,7,72,57,7,74,45,17,143,17,65,114,50,33,14,0,65,111,40,159,195,98,135,15,35,7,53,51,21,100,78,9,95,146,16,32,254,82,114,6,32,128,67,208,37, + 130,166,99,79,58,32,17,96,99,14,72,31,19,72,87,31,82,155,7,67,47,14,32,21,131,75,134,231,72,51,17,72,78,8,133,8,80,133,6,33,253,128,88,37,9,66,124,36,72,65,12,134,12,71,55,43,66,139, + 27,85,135,10,91,33,12,65,35,11,66,131,11,71,32,8,90,127,6,130,244,71,76,11,168,207,33,0,12,66,123,32,32,0,65,183,15,68,135,11,66,111,7,67,235,11,66,111,15,32,254,97,66,12,160,154,67, + 227,52,80,33,15,87,249,15,93,45,31,75,111,12,93,45,11,77,99,9,160,184,81,31,12,32,15,98,135,30,104,175,7,77,249,36,69,73,15,78,5,12,32,254,66,151,19,34,128,128,4,87,32,12,149,35,133, + 21,96,151,31,32,19,72,35,5,98,173,15,143,15,32,21,143,99,158,129,33,0,0,65,35,52,65,11,15,147,15,98,75,11,33,1,0,143,151,132,15,32,254,99,200,37,132,43,130,4,39,0,10,0,128,1,128,3, + 0,104,151,14,97,187,20,69,131,15,67,195,11,87,227,7,33,128,128,132,128,33,254,0,68,131,9,65,46,26,42,0,0,0,7,0,0,255,128,3,128,0,88,223,15,33,0,21,89,61,22,66,209,12,65,2,12,37,0,2, + 1,0,3,128,101,83,8,36,0,1,53,51,29,130,3,34,21,1,0,66,53,8,32,0,68,215,6,100,55,25,107,111,9,66,193,11,72,167,8,73,143,31,139,31,33,1,0,131,158,32,254,132,5,33,253,128,65,16,9,133, + 17,89,130,25,141,212,33,0,0,93,39,8,90,131,25,93,39,14,66,217,6,106,179,8,159,181,71,125,15,139,47,138,141,87,11,14,76,23,14,65,231,26,140,209,66,122,8,81,179,5,101,195,26,32,47,74, + 75,13,69,159,11,83,235,11,67,21,16,136,167,131,106,130,165,130,15,32,128,101,90,24,134,142,32,0,65,103,51,108,23,11,101,231,15,75,173,23,74,237,23,66,15,6,66,46,17,66,58,17,65,105, + 49,66,247,55,71,179,12,70,139,15,86,229,7,84,167,15,32,1,95,72,12,89,49,6,33,128,128,65,136,38,66,30,9,32,0,100,239,7,66,247,29,70,105,20,65,141,19,69,81,15,130,144,32,128,83,41,5, + 32,255,131,177,68,185,5,133,126,65,97,37,32,0,130,0,33,21,0,130,55,66,195,28,67,155,13,34,79,0,83,66,213,13,73,241,19,66,59,19,65,125,11,135,201,66,249,16,32,128,66,44,11,66,56,17, + 68,143,8,68,124,38,67,183,12,96,211,9,65,143,29,112,171,5,32,0,68,131,63,34,33,53,51,71,121,11,32,254,98,251,16,32,253,74,231,10,65,175,37,133,206,37,0,0,8,1,0,0,107,123,11,113,115, + 9,33,0,1,130,117,131,3,73,103,7,66,51,18,66,44,5,133,75,70,88,5,32,254,65,39,12,68,80,9,34,12,0,128,107,179,28,68,223,6,155,111,86,147,15,32,2,131,82,141,110,33,254,0,130,15,32,4,103, + 184,15,141,35,87,176,5,83,11,5,71,235,23,114,107,11,65,189,16,70,33,15,86,153,31,135,126,86,145,30,65,183,41,32,0,130,0,32,10,65,183,24,34,35,0,39,67,85,9,65,179,15,143,15,33,1,0,65, + 28,17,157,136,130,123,32,20,130,3,32,0,97,135,24,115,167,19,80,71,12,32,51,110,163,14,78,35,19,131,19,155,23,77,229,8,78,9,17,151,17,67,231,46,94,135,8,73,31,31,93,215,56,82,171,25, + 72,77,8,162,179,169,167,99,131,11,69,85,19,66,215,15,76,129,13,68,115,22,72,79,35,67,113,5,34,0,0,19,70,31,46,65,89,52,73,223,15,85,199,33,95,33,8,132,203,73,29,32,67,48,16,177,215, + 101,13,15,65,141,43,69,141,15,75,89,5,70,0,11,70,235,21,178,215,36,10,0,128,0,0,71,207,24,33,0,19,100,67,6,80,215,11,66,67,7,80,43,12,71,106,7,80,192,5,65,63,5,66,217,26,33,0,13,156, + 119,68,95,5,72,233,12,134,129,85,81,11,76,165,20,65,43,8,73,136,8,75,10,31,38,128,128,0,0,0,13,1,130,4,32,3,106,235,29,114,179,12,66,131,23,32,7,77,133,6,67,89,12,131,139,116,60,9, + 89,15,37,32,0,74,15,7,103,11,22,65,35,5,33,55,0,93,81,28,67,239,23,78,85,5,107,93,14,66,84,17,65,193,26,74,183,10,66,67,34,143,135,79,91,15,32,7,117,111,8,75,56,9,84,212,9,154,134, + 32,0,130,0,32,18,130,3,70,171,41,83,7,16,70,131,19,84,191,15,84,175,19,84,167,30,84,158,12,154,193,68,107,15,33,0,0,65,79,42,65,71,7,73,55,7,118,191,16,83,180,9,32,255,76,166,9,154, + 141,32,0,130,0,69,195,52,65,225,15,151,15,75,215,31,80,56,10,68,240,17,100,32,9,70,147,39,65,93,12,71,71,41,92,85,15,84,135,23,78,35,15,110,27,10,84,125,8,107,115,29,136,160,38,0,0, + 14,0,128,255,0,82,155,24,67,239,8,119,255,11,69,131,11,77,29,6,112,31,8,134,27,105,203,8,32,2,75,51,11,75,195,12,74,13,29,136,161,37,128,0,0,0,11,1,130,163,82,115,8,125,191,17,69,35, + 12,74,137,15,143,15,32,1,65,157,12,136,12,161,142,65,43,40,65,199,6,65,19,24,102,185,11,76,123,11,99,6,12,135,12,32,254,130,8,161,155,101,23,9,39,8,0,0,1,128,3,128,2,78,63,17,72,245, + 12,67,41,11,90,167,9,32,128,97,49,9,32,128,109,51,14,132,97,81,191,8,130,97,125,99,12,121,35,9,127,75,15,71,79,12,81,151,23,87,97,7,70,223,15,80,245,16,105,97,15,32,254,113,17,6,32, + 128,130,8,105,105,8,76,122,18,65,243,21,74,63,7,38,4,1,0,255,0,2,0,119,247,28,133,65,32,255,141,91,35,0,0,0,16,67,63,36,34,59,0,63,77,59,9,119,147,11,143,241,66,173,15,66,31,11,67, + 75,8,81,74,16,32,128,131,255,87,181,42,127,43,5,34,255,128,2,120,235,11,37,19,0,23,0,0,37,109,191,14,118,219,7,127,43,14,65,79,14,35,0,0,0,3,73,91,5,130,5,38,3,0,7,0,11,0,0,70,205, + 11,88,221,12,32,0,73,135,7,87,15,22,73,135,10,79,153,15,97,71,19,65,49,11,32,1,131,104,121,235,11,80,65,11,142,179,144,14,81,123,46,32,1,88,217,5,112,5,8,65,201,15,83,29,15,122,147, + 11,135,179,142,175,143,185,67,247,39,66,199,7,35,5,0,128,3,69,203,15,123,163,12,67,127,7,130,119,71,153,10,141,102,70,175,8,32,128,121,235,30,136,89,100,191,11,116,195,11,111,235,15, + 72,39,7,32,2,97,43,5,132,5,94,67,8,131,8,125,253,10,32,3,65,158,16,146,16,130,170,40,0,21,0,128,0,0,3,128,5,88,219,15,24,64,159,32,135,141,65,167,15,68,163,10,97,73,49,32,255,82,58, + 7,93,80,8,97,81,16,24,67,87,52,34,0,0,5,130,231,33,128,2,80,51,13,65,129,8,113,61,6,132,175,65,219,5,130,136,77,152,17,32,0,95,131,61,70,215,6,33,21,51,90,53,10,78,97,23,105,77,31, + 65,117,7,139,75,24,68,195,9,24,64,22,9,33,0,128,130,11,33,128,128,66,25,5,121,38,5,134,5,134,45,66,40,36,66,59,18,34,128,0,0,66,59,81,135,245,123,103,19,120,159,19,77,175,12,33,255, + 0,87,29,10,94,70,21,66,59,54,39,3,1,128,3,0,2,128,4,24,65,7,15,66,47,7,72,98,12,37,0,0,0,3,1,0,24,65,55,21,131,195,32,1,67,178,6,33,4,0,77,141,8,32,6,131,47,74,67,16,24,69,3,20,24, + 65,251,7,133,234,130,229,94,108,17,35,0,0,6,0,141,175,86,59,5,162,79,85,166,8,70,112,13,32,13,24,64,67,26,24,71,255,7,123,211,12,80,121,11,69,215,15,66,217,11,69,71,10,131,113,132, + 126,119,90,9,66,117,19,132,19,32,0,130,0,24,64,47,59,33,7,0,73,227,5,68,243,15,85,13,12,76,37,22,74,254,15,130,138,33,0,4,65,111,6,137,79,65,107,16,32,1,77,200,6,34,128,128,3,75,154, + 12,37,0,16,0,0,2,0,104,115,36,140,157,68,67,19,68,51,15,106,243,15,134,120,70,37,10,68,27,10,140,152,65,121,24,32,128,94,155,7,67,11,8,24,74,11,25,65,3,12,83,89,18,82,21,37,67,200, + 5,130,144,24,64,172,12,33,4,0,134,162,74,80,14,145,184,32,0,130,0,69,251,20,32,19,81,243,5,82,143,8,33,5,53,89,203,5,133,112,79,109,15,33,0,21,130,71,80,175,41,36,75,0,79,0,83,121, + 117,9,87,89,27,66,103,11,70,13,15,75,191,11,135,67,87,97,20,109,203,5,69,246,8,108,171,5,78,195,38,65,51,13,107,203,11,77,3,17,24,75,239,17,65,229,28,79,129,39,130,175,32,128,123,253, + 7,132,142,24,65,51,15,65,239,41,36,128,128,0,0,13,65,171,5,66,163,28,136,183,118,137,11,80,255,15,67,65,7,74,111,8,32,0,130,157,32,253,24,76,35,10,103,212,5,81,175,9,69,141,7,66,150, + 29,131,158,24,75,199,28,124,185,7,76,205,15,68,124,14,32,3,123,139,16,130,16,33,128,128,108,199,6,33,0,3,65,191,35,107,11,6,73,197,11,24,70,121,15,83,247,15,24,70,173,23,69,205,14, + 32,253,131,140,32,254,136,4,94,198,9,32,3,78,4,13,66,127,13,143,13,32,0,130,0,33,16,0,24,69,59,39,109,147,12,76,253,19,24,69,207,15,69,229,15,130,195,71,90,10,139,10,130,152,73,43, + 40,91,139,10,65,131,37,35,75,0,79,0,84,227,12,143,151,68,25,15,80,9,23,95,169,11,34,128,2,128,112,186,5,130,6,83,161,19,76,50,6,130,37,65,145,44,110,83,5,32,16,67,99,6,71,67,15,76, + 55,17,140,215,67,97,23,76,69,15,77,237,11,104,211,23,77,238,11,65,154,43,33,0,10,83,15,28,83,13,20,67,145,19,67,141,14,97,149,21,68,9,15,86,251,5,66,207,5,66,27,37,82,1,23,127,71,12, + 94,235,10,110,175,24,98,243,15,132,154,132,4,24,66,69,10,32,4,67,156,43,130,198,35,2,1,0,4,75,27,9,69,85,9,95,240,7,32,128,130,35,32,28,66,43,40,24,82,63,23,83,123,12,72,231,15,127, + 59,23,116,23,19,117,71,7,24,77,99,15,67,111,15,71,101,8,36,2,128,128,252,128,127,60,11,32,1,132,16,130,18,141,24,67,107,9,32,3,68,194,15,175,15,38,0,11,0,128,1,128,2,80,63,25,32,0, + 24,65,73,11,69,185,15,83,243,16,32,0,24,81,165,8,130,86,77,35,6,155,163,88,203,5,24,66,195,30,70,19,19,24,80,133,15,32,1,75,211,8,32,254,108,133,8,79,87,20,65,32,9,41,0,0,7,0,128,0, + 0,2,128,2,68,87,15,66,1,16,92,201,16,24,76,24,17,133,17,34,128,0,30,66,127,64,34,115,0,119,73,205,9,66,43,11,109,143,15,24,79,203,11,90,143,15,131,15,155,31,65,185,15,86,87,11,35,128, + 128,253,0,69,7,6,130,213,33,1,0,119,178,15,142,17,66,141,74,83,28,6,36,7,0,0,4,128,82,39,18,76,149,12,67,69,21,32,128,79,118,15,32,0,130,0,32,8,131,206,32,2,79,83,9,100,223,14,102, + 113,23,115,115,7,24,65,231,12,130,162,32,4,68,182,19,130,102,93,143,8,69,107,29,24,77,255,12,143,197,72,51,7,76,195,15,132,139,85,49,15,130,152,131,18,71,81,23,70,14,11,36,0,10,0,128, + 2,69,59,9,89,151,15,66,241,11,76,165,12,71,43,15,75,49,13,65,12,23,132,37,32,0,179,115,130,231,95,181,16,132,77,32,254,67,224,8,65,126,20,79,171,8,32,2,89,81,5,75,143,6,80,41,8,34, + 2,0,128,24,81,72,9,32,0,130,0,35,17,0,0,255,77,99,39,95,65,36,67,109,15,24,69,93,11,77,239,5,95,77,23,35,128,1,0,128,24,86,7,8,132,167,32,2,69,198,41,130,202,33,0,26,120,75,44,24,89, + 51,15,71,243,12,70,239,11,24,84,3,11,66,7,11,71,255,10,32,21,69,155,35,88,151,12,32,128,74,38,10,65,210,8,74,251,5,65,226,5,75,201,13,32,3,65,9,41,146,41,40,0,0,0,9,1,0,1,0,2,91,99, + 19,32,35,106,119,13,70,219,15,83,239,12,137,154,32,2,67,252,19,36,128,0,0,4,1,130,196,32,2,130,8,91,107,8,32,0,135,81,24,73,211,8,132,161,73,164,13,36,0,8,0,128,2,105,123,26,139,67, + 76,99,15,34,1,0,128,135,76,83,156,20,92,104,8,67,251,30,24,86,47,27,123,207,12,24,86,7,15,71,227,8,32,4,65,20,20,131,127,32,0,130,123,32,0,71,223,26,32,19,90,195,22,71,223,15,84,200, + 6,32,128,133,241,24,84,149,9,67,41,25,36,0,0,0,22,0,88,111,49,32,87,66,21,5,77,3,27,123,75,7,71,143,19,135,183,71,183,19,130,171,74,252,5,131,5,89,87,17,32,1,132,18,130,232,68,11,10, + 33,1,128,70,208,16,66,230,18,147,18,130,254,223,255,75,27,23,65,59,15,135,39,155,255,34,128,128,254,104,92,8,33,0,128,65,32,11,65,1,58,33,26,0,130,0,72,71,18,78,55,17,76,11,19,86,101, + 12,75,223,11,89,15,11,24,76,87,15,75,235,15,131,15,72,95,7,85,71,11,72,115,11,73,64,6,34,1,128,128,66,215,9,34,128,254,128,134,14,33,128,255,67,102,5,32,0,130,16,70,38,11,66,26,57, + 88,11,8,24,76,215,34,78,139,7,95,245,7,32,7,24,73,75,23,32,128,131,167,130,170,101,158,9,82,49,22,118,139,6,32,18,67,155,44,116,187,9,108,55,14,80,155,23,66,131,15,93,77,10,131,168, + 32,128,73,211,12,24,75,187,22,32,4,96,71,20,67,108,19,132,19,120,207,8,32,5,76,79,15,66,111,21,66,95,8,32,3,190,211,111,3,8,211,212,32,20,65,167,44,34,75,0,79,97,59,13,32,33,112,63, + 10,65,147,19,69,39,19,143,39,24,66,71,9,130,224,65,185,43,94,176,12,65,183,24,71,38,8,24,72,167,7,65,191,38,136,235,24,96,167,12,65,203,62,115,131,13,65,208,42,175,235,67,127,6,32, + 4,76,171,29,114,187,5,32,71,65,211,5,65,203,68,72,51,8,164,219,32,0,172,214,71,239,58,78,3,27,66,143,15,77,19,15,147,31,35,33,53,51,21,66,183,10,173,245,66,170,30,150,30,34,0,0,23, + 80,123,54,76,1,16,73,125,15,82,245,11,167,253,24,76,85,12,70,184,5,32,254,131,185,37,254,0,128,1,0,128,133,16,117,158,18,92,27,38,65,3,17,130,251,35,17,0,128,254,24,69,83,39,140,243, + 121,73,19,109,167,7,81,41,15,24,95,175,12,102,227,15,121,96,11,24,95,189,7,32,3,145,171,154,17,24,77,47,9,33,0,5,70,71,37,68,135,7,32,29,117,171,11,69,87,15,24,79,97,19,24,79,149,23, + 131,59,32,1,75,235,5,72,115,11,72,143,7,132,188,71,27,46,131,51,32,0,69,95,6,175,215,32,21,131,167,81,15,19,151,191,151,23,131,215,71,43,5,32,254,24,79,164,24,74,109,8,77,166,13,65, + 176,26,88,162,5,98,159,6,171,219,120,247,6,79,29,8,99,169,10,103,59,19,65,209,35,131,35,91,25,19,112,94,15,83,36,8,173,229,33,20,0,88,75,43,71,31,12,65,191,71,33,1,0,130,203,32,254, + 131,4,68,66,7,67,130,6,104,61,13,173,215,38,13,1,0,0,0,2,128,67,111,28,74,129,16,104,35,19,79,161,16,87,14,7,138,143,132,10,67,62,36,114,115,5,162,151,67,33,16,108,181,15,143,151,67, + 5,5,24,100,242,15,170,153,34,0,0,14,65,51,34,32,55,79,75,9,32,51,74,7,10,65,57,38,132,142,32,254,72,0,14,139,163,32,128,80,254,8,67,158,21,65,63,7,32,4,72,227,27,95,155,12,67,119,19, + 124,91,24,149,154,72,177,34,97,223,8,155,151,24,108,227,15,88,147,16,72,117,19,68,35,11,92,253,15,70,199,15,24,87,209,17,32,2,87,233,7,32,1,24,88,195,10,119,24,8,32,3,81,227,24,65, + 125,21,35,128,128,0,25,76,59,48,24,90,187,9,97,235,12,66,61,11,91,105,19,24,79,141,11,24,79,117,15,24,79,129,27,90,53,13,130,13,32,253,131,228,24,79,133,40,69,70,8,66,137,31,65,33, + 19,96,107,8,68,119,29,66,7,5,68,125,16,65,253,19,65,241,27,24,90,179,13,24,79,143,18,33,128,128,130,246,32,254,130,168,68,154,36,77,51,9,97,47,5,167,195,32,21,131,183,78,239,27,155, + 195,78,231,14,201,196,77,11,6,32,5,73,111,37,97,247,12,77,19,31,155,207,78,215,19,162,212,69,17,14,66,91,19,80,143,57,78,203,39,159,215,32,128,93,134,8,24,80,109,24,66,113,15,169,215, + 66,115,6,32,4,69,63,33,32,0,101,113,7,86,227,35,143,211,36,49,53,51,21,1,77,185,14,65,159,28,69,251,34,67,56,8,33,9,0,24,107,175,25,90,111,12,110,251,11,119,189,24,119,187,34,87,15, + 9,32,4,66,231,37,90,39,7,66,239,8,84,219,15,69,105,23,24,85,27,27,87,31,11,33,1,128,76,94,6,32,1,85,241,7,33,128,128,106,48,10,33,128,128,69,136,11,133,13,24,79,116,49,84,236,8,24, + 91,87,9,32,5,165,255,69,115,12,66,27,15,159,15,24,72,247,12,74,178,5,24,80,64,15,33,0,128,143,17,77,89,51,130,214,24,81,43,7,170,215,74,49,8,159,199,143,31,139,215,69,143,5,32,254, + 24,81,50,35,181,217,84,123,70,143,195,159,15,65,187,16,66,123,7,65,175,15,65,193,29,68,207,39,79,27,5,70,131,6,32,4,68,211,33,33,67,0,83,143,14,159,207,143,31,140,223,33,0,128,24,80, + 82,14,24,93,16,23,32,253,65,195,5,68,227,40,133,214,107,31,7,32,5,67,115,27,87,9,8,107,31,43,66,125,6,32,0,103,177,23,131,127,72,203,36,32,0,110,103,8,155,163,73,135,6,32,19,24,112, + 99,10,65,71,11,73,143,19,143,31,126,195,5,24,85,21,9,24,76,47,14,32,254,24,93,77,36,68,207,11,39,25,0,0,255,128,3,128,4,66,51,37,95,247,13,82,255,24,76,39,19,147,221,66,85,27,24,118, + 7,8,24,74,249,12,76,74,8,91,234,8,67,80,17,131,222,33,253,0,121,30,44,73,0,16,69,15,6,32,0,65,23,38,69,231,12,65,179,6,98,131,16,86,31,27,24,108,157,14,80,160,8,24,65,46,17,33,4,0, + 96,2,18,144,191,65,226,8,68,19,5,171,199,80,9,15,180,199,67,89,5,32,255,24,79,173,28,174,201,24,79,179,50,32,1,24,122,5,10,82,61,10,180,209,83,19,8,32,128,24,80,129,27,111,248,43,131, + 71,24,115,103,8,67,127,41,78,213,24,100,247,19,66,115,39,75,107,5,32,254,165,219,78,170,40,24,112,163,49,32,1,97,203,6,65,173,64,32,0,83,54,7,133,217,88,37,12,32,254,131,28,33,128, + 3,67,71,44,84,183,6,32,5,69,223,33,96,7,7,123,137,16,192,211,24,112,14,9,32,255,67,88,29,68,14,10,84,197,38,33,0,22,116,47,50,32,87,106,99,9,116,49,15,89,225,15,97,231,23,70,41,19, + 82,85,8,93,167,6,32,253,132,236,108,190,7,89,251,5,116,49,58,33,128,128,131,234,32,15,24,74,67,38,70,227,24,24,83,45,23,89,219,12,70,187,12,89,216,19,32,2,69,185,24,141,24,70,143,66, + 24,82,119,56,78,24,10,32,253,133,149,132,6,24,106,233,7,69,198,48,178,203,81,243,12,68,211,15,106,255,23,66,91,15,69,193,7,100,39,10,24,83,72,16,176,204,33,19,0,88,207,45,68,21,12, + 68,17,10,65,157,53,68,17,6,32,254,92,67,10,65,161,25,69,182,43,24,118,91,47,69,183,18,181,209,111,253,12,89,159,8,66,112,12,69,184,45,35,0,0,0,9,24,80,227,26,73,185,16,118,195,15,131, + 15,33,1,0,65,59,15,66,39,27,160,111,66,205,12,148,111,143,110,33,128,128,156,112,24,81,199,8,75,199,23,66,117,20,155,121,32,254,68,126,12,72,213,29,134,239,149,123,89,27,16,148,117, + 65,245,8,24,71,159,14,141,134,134,28,73,51,55,109,77,15,105,131,11,68,67,11,76,169,27,107,209,12,102,174,8,32,128,72,100,18,116,163,56,79,203,11,75,183,44,85,119,19,71,119,23,151,227, + 32,1,93,27,8,65,122,5,77,102,8,110,120,20,66,23,8,66,175,17,66,63,12,133,12,79,35,8,74,235,33,67,149,16,69,243,15,78,57,15,69,235,16,67,177,7,151,192,130,23,67,84,29,141,192,174,187, + 77,67,15,69,11,12,159,187,77,59,10,199,189,24,70,235,50,96,83,19,66,53,23,105,65,19,77,47,12,163,199,66,67,37,78,207,50,67,23,23,174,205,67,228,6,71,107,13,67,22,14,66,85,11,83,187, + 38,124,47,49,95,7,19,66,83,23,67,23,19,24,96,78,17,80,101,16,71,98,40,33,0,7,88,131,22,24,89,245,12,84,45,12,102,213,5,123,12,9,32,2,126,21,14,43,255,0,128,128,0,0,20,0,128,255,128, + 3,126,19,39,32,75,106,51,7,113,129,15,24,110,135,19,126,47,15,115,117,11,69,47,11,32,2,109,76,9,102,109,9,32,128,75,2,10,130,21,32,254,69,47,6,32,3,94,217,47,32,0,65,247,10,69,15,46, + 65,235,31,65,243,15,101,139,10,66,174,14,65,247,16,72,102,28,69,17,14,84,243,9,165,191,88,47,48,66,53,12,32,128,71,108,6,203,193,32,17,75,187,42,73,65,16,65,133,52,114,123,9,167,199, + 69,21,37,86,127,44,75,171,11,180,197,78,213,12,148,200,81,97,46,24,95,243,9,32,4,66,75,33,113,103,9,87,243,36,143,225,24,84,27,31,90,145,8,148,216,67,49,5,24,84,34,14,75,155,27,67, + 52,13,140,13,36,0,20,0,128,255,24,135,99,46,88,59,43,155,249,80,165,7,136,144,71,161,23,32,253,132,33,32,254,88,87,44,136,84,35,128,0,0,21,81,103,5,94,47,44,76,51,12,143,197,151,15, + 65,215,31,24,64,77,13,65,220,20,65,214,14,71,4,40,65,213,13,32,0,130,0,35,21,1,2,0,135,0,34,36,0,72,134,10,36,1,0,26,0,130,134,11,36,2,0,14,0,108,134,11,32,3,138,23,32,4,138,11,34, + 5,0,20,134,33,34,0,0,6,132,23,32,1,134,15,32,18,130,25,133,11,37,1,0,13,0,49,0,133,11,36,2,0,7,0,38,134,11,36,3,0,17,0,45,134,11,32,4,138,35,36,5,0,10,0,62,134,23,32,6,132,23,36,3, + 0,1,4,9,130,87,131,167,133,11,133,167,133,11,133,167,133,11,37,3,0,34,0,122,0,133,11,133,167,133,11,133,167,133,11,133,167,34,50,0,48,130,1,34,52,0,47,134,5,8,49,49,0,53,98,121,32, + 84,114,105,115,116,97,110,32,71,114,105,109,109,101,114,82,101,103,117,108,97,114,84,84,88,32,80,114,111,103,103,121,67,108,101,97,110,84,84,50,48,48,52,47,130,2,53,49,53,0,98,0,121, + 0,32,0,84,0,114,0,105,0,115,0,116,0,97,0,110,130,15,32,71,132,15,36,109,0,109,0,101,130,9,32,82,130,5,36,103,0,117,0,108,130,29,32,114,130,43,34,84,0,88,130,35,32,80,130,25,34,111, + 0,103,130,1,34,121,0,67,130,27,32,101,132,59,32,84,130,31,33,0,0,65,155,9,34,20,0,0,65,11,6,130,8,135,2,33,1,1,130,9,8,120,1,1,2,1,3,1,4,1,5,1,6,1,7,1,8,1,9,1,10,1,11,1,12,1,13,1,14, + 1,15,1,16,1,17,1,18,1,19,1,20,1,21,1,22,1,23,1,24,1,25,1,26,1,27,1,28,1,29,1,30,1,31,1,32,0,3,0,4,0,5,0,6,0,7,0,8,0,9,0,10,0,11,0,12,0,13,0,14,0,15,0,16,0,17,0,18,0,19,0,20,0,21,0, + 22,0,23,0,24,0,25,0,26,0,27,0,28,0,29,0,30,0,31,130,187,8,66,33,0,34,0,35,0,36,0,37,0,38,0,39,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0, + 57,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,130,243,9,75,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0, + 92,0,93,0,94,0,95,0,96,0,97,1,33,1,34,1,35,1,36,1,37,1,38,1,39,1,40,1,41,1,42,1,43,1,44,1,45,1,46,1,47,1,48,1,49,1,50,1,51,1,52,1,53,1,54,1,55,1,56,1,57,1,58,1,59,1,60,1,61,1,62,1, + 63,1,64,1,65,0,172,0,163,0,132,0,133,0,189,0,150,0,232,0,134,0,142,0,139,0,157,0,169,0,164,0,239,0,138,0,218,0,131,0,147,0,242,0,243,0,141,0,151,0,136,0,195,0,222,0,241,0,158,0,170, + 0,245,0,244,0,246,0,162,0,173,0,201,0,199,0,174,0,98,0,99,0,144,0,100,0,203,0,101,0,200,0,202,0,207,0,204,0,205,0,206,0,233,0,102,0,211,0,208,0,209,0,175,0,103,0,240,0,145,0,214,0, + 212,0,213,0,104,0,235,0,237,0,137,0,106,0,105,0,107,0,109,0,108,0,110,0,160,0,111,0,113,0,112,0,114,0,115,0,117,0,116,0,118,0,119,0,234,0,120,0,122,0,121,0,123,0,125,0,124,0,184,0, + 161,0,127,0,126,0,128,0,129,0,236,0,238,0,186,14,117,110,105,99,111,100,101,35,48,120,48,48,48,49,141,14,32,50,141,14,32,51,141,14,32,52,141,14,32,53,141,14,32,54,141,14,32,55,141, + 14,32,56,141,14,32,57,141,14,32,97,141,14,32,98,141,14,32,99,141,14,32,100,141,14,32,101,141,14,32,102,140,14,33,49,48,141,14,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49, + 141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,45,49,102,6,100,101,108,101,116,101,4,69,117,114, + 111,140,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236, + 32,56,141,236,32,56,141,236,32,56,65,220,13,32,57,65,220,13,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141, + 239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,35,57,102,0,0,5,250,72,249,98,247, +}; + +static const char* GetDefaultCompressedFontDataTTF(int* out_size) +{ + *out_size = proggy_clean_ttf_compressed_size; + return (const char*)proggy_clean_ttf_compressed_data; +} +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imgui_internal.h b/thirdparty/imgui/imgui_internal.h new file mode 100644 index 00000000..ddf9ea3e --- /dev/null +++ b/thirdparty/imgui/imgui_internal.h @@ -0,0 +1,3636 @@ +// dear imgui, v1.91.9b +// (internal structures/api) + +// You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. + +/* + +Index of this file: + +// [SECTION] Header mess +// [SECTION] Forward declarations +// [SECTION] Context pointer +// [SECTION] STB libraries includes +// [SECTION] Macros +// [SECTION] Generic helpers +// [SECTION] ImDrawList support +// [SECTION] Style support +// [SECTION] Data types support +// [SECTION] Widgets support: flags, enums, data structures +// [SECTION] Popup support +// [SECTION] Inputs support +// [SECTION] Clipper support +// [SECTION] Navigation support +// [SECTION] Typing-select support +// [SECTION] Columns support +// [SECTION] Box-select support +// [SECTION] Multi-select support +// [SECTION] Docking support +// [SECTION] Viewport support +// [SECTION] Settings support +// [SECTION] Localization support +// [SECTION] Error handling, State recovery support +// [SECTION] Metrics, Debug tools +// [SECTION] Generic context hooks +// [SECTION] ImGuiContext (main imgui context) +// [SECTION] ImGuiWindowTempData, ImGuiWindow +// [SECTION] Tab bar, Tab item support +// [SECTION] Table support +// [SECTION] ImGui internal API +// [SECTION] ImFontAtlas internal API +// [SECTION] Test Engine specific hooks (imgui_test_engine) + +*/ + +#pragma once +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +#ifndef IMGUI_VERSION +#include "imgui.h" +#endif + +#include // FILE*, sscanf +#include // NULL, malloc, free, qsort, atoi, atof +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // INT_MIN, INT_MAX + +// Enable SSE intrinsics if available +#if (defined __SSE__ || defined __x86_64__ || defined _M_X64 || (defined(_M_IX86_FP) && (_M_IX86_FP >= 1))) && !defined(IMGUI_DISABLE_SSE) +#define IMGUI_ENABLE_SSE +#include +#if (defined __AVX__ || defined __SSE4_2__) +#define IMGUI_ENABLE_SSE4_2 +#include +#endif +#endif +// Emscripten has partial SSE 4.2 support where _mm_crc32_u32 is not available. See https://emscripten.org/docs/porting/simd.html#id11 and #8213 +#if defined(IMGUI_ENABLE_SSE4_2) && !defined(IMGUI_USE_LEGACY_CRC32_ADLER) && !defined(__EMSCRIPTEN__) +#define IMGUI_ENABLE_SSE4_2_CRC +#endif + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (disable: 26812) // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloor() +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#endif + +// In 1.89.4, we moved the implementation of "courtesy maths operators" from imgui_internal.h in imgui.h +// As they are frequently requested, we do not want to encourage to many people using imgui_internal.h +#if defined(IMGUI_DEFINE_MATH_OPERATORS) && !defined(IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED) +#error Please '#define IMGUI_DEFINE_MATH_OPERATORS' _BEFORE_ including imgui.h! +#endif + +// Legacy defines +#ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#endif +#ifdef IMGUI_DISABLE_MATH_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#endif + +// Enable stb_truetype by default unless FreeType is enabled. +// You can compile with both by defining both IMGUI_ENABLE_FREETYPE and IMGUI_ENABLE_STB_TRUETYPE together. +#ifndef IMGUI_ENABLE_FREETYPE +#define IMGUI_ENABLE_STB_TRUETYPE +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward declarations +//----------------------------------------------------------------------------- + +// Utilities +// (other types which are not forwarded declared are: ImBitArray<>, ImSpan<>, ImSpanAllocator<>, ImPool<>, ImChunkStream<>) +struct ImBitVector; // Store 1-bit per value +struct ImRect; // An axis-aligned rectangle (2 points) +struct ImGuiTextIndex; // Maintain a line index for a text buffer. + +// ImDrawList/ImFontAtlas +struct ImDrawDataBuilder; // Helper to build a ImDrawData instance +struct ImDrawListSharedData; // Data shared between all ImDrawList instances + +// ImGui +struct ImGuiBoxSelectState; // Box-selection state (currently used by multi-selection, could potentially be used by others) +struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it +struct ImGuiContext; // Main Dear ImGui context +struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine +struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +struct ImGuiDeactivatedItemData; // Data for IsItemDeactivated()/IsItemDeactivatedAfterEdit() function. +struct ImGuiErrorRecoveryState; // Storage of stack sizes for error handling and recovery +struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() +struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box +struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id +struct ImGuiLastItemData; // Status storage for last submitted items +struct ImGuiLocEntry; // A localization entry. +struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only +struct ImGuiMultiSelectState; // Multi-selection persistent state (for focused selection). +struct ImGuiMultiSelectTempData; // Multi-selection temporary state (while traversing). +struct ImGuiNavItemData; // Result of a keyboard/gamepad directional navigation move query result +struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions +struct ImGuiNextWindowData; // Storage for SetNextWindow** functions +struct ImGuiNextItemData; // Storage for SetNextItem** functions +struct ImGuiOldColumnData; // Storage data for a single column for legacy Columns() api +struct ImGuiOldColumns; // Storage data for a columns set for legacy Columns() api +struct ImGuiPopupData; // Storage for current popup stack +struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file +struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it +struct ImGuiStyleVarInfo; // Style variable information (e.g. to access style variables from an enum) +struct ImGuiTabBar; // Storage for a tab bar +struct ImGuiTabItem; // Storage for a tab item (within a tab bar) +struct ImGuiTable; // Storage for a table +struct ImGuiTableHeaderData; // Storage for TableAngledHeadersRow() +struct ImGuiTableColumn; // Storage for one column of a table +struct ImGuiTableInstanceData; // Storage for one instance of a same table +struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. +struct ImGuiTableSettings; // Storage for a table .ini settings +struct ImGuiTableColumnsSettings; // Storage for a column .ini settings +struct ImGuiTreeNodeStackData; // Temporary storage for TreeNode(). +struct ImGuiTypingSelectState; // Storage for GetTypingSelectRequest() +struct ImGuiTypingSelectRequest; // Storage for GetTypingSelectRequest() (aimed to be public) +struct ImGuiWindow; // Storage for one window +struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) +struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) + +// Enumerations +// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. +enum ImGuiLocKey : int; // -> enum ImGuiLocKey // Enum: a localization entry for translation. +typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical + +// Flags +typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) +typedef int ImGuiDebugLogFlags; // -> enum ImGuiDebugLogFlags_ // Flags: for ShowDebugLogWindow(), g.DebugLogFlags +typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow() +typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for g.LastItemData.StatusFlags +typedef int ImGuiOldColumnFlags; // -> enum ImGuiOldColumnFlags_ // Flags: for BeginColumns() +typedef int ImGuiLogFlags; // -> enum ImGuiLogFlags_ // Flags: for LogBegin() text capturing function +typedef int ImGuiNavRenderCursorFlags; // -> enum ImGuiNavRenderCursorFlags_//Flags: for RenderNavCursor() +typedef int ImGuiNavMoveFlags; // -> enum ImGuiNavMoveFlags_ // Flags: for navigation requests +typedef int ImGuiNextItemDataFlags; // -> enum ImGuiNextItemDataFlags_ // Flags: for SetNextItemXXX() functions +typedef int ImGuiNextWindowDataFlags; // -> enum ImGuiNextWindowDataFlags_// Flags: for SetNextWindowXXX() functions +typedef int ImGuiScrollFlags; // -> enum ImGuiScrollFlags_ // Flags: for ScrollToItem() and navigation requests +typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // Flags: for SeparatorEx() +typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() +typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() +typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() +typedef int ImGuiWindowRefreshFlags; // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy() + +//----------------------------------------------------------------------------- +// [SECTION] Context pointer +// See implementation of this variable in imgui.cpp for comments and details. +//----------------------------------------------------------------------------- + +#ifndef GImGui +extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Macros +//----------------------------------------------------------------------------- + +// Debug Printing Into TTY +// (since IMGUI_VERSION_NUM >= 18729: IMGUI_DEBUG_LOG was reworked into IMGUI_DEBUG_PRINTF (and removed framecount from it). If you were using a #define IMGUI_DEBUG_LOG please rename) +#ifndef IMGUI_DEBUG_PRINTF +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#define IMGUI_DEBUG_PRINTF(_FMT,...) printf(_FMT, __VA_ARGS__) +#else +#define IMGUI_DEBUG_PRINTF(_FMT,...) ((void)0) +#endif +#endif + +// Debug Logging for ShowDebugLogWindow(). This is designed for relatively rare events so please don't spam. +#define IMGUI_DEBUG_LOG_ERROR(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventError) IMGUI_DEBUG_LOG(__VA_ARGS__); else g.DebugLogSkippedErrors++; } while (0) +#define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_NAV(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventNav) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_SELECTION(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_CLIPPER(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_IO(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FONT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFont) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_INPUTROUTING(...) do{if (g.DebugLogFlags & ImGuiDebugLogFlags_EventInputRouting)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) + +// Static Asserts +#define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") + +// "Paranoid" Debug Asserts are meant to only be enabled during specific debugging/work, otherwise would slow down the code too much. +// We currently don't have many of those so the effect is currently negligible, but onward intent to add more aggressive ones in the code. +//#define IMGUI_DEBUG_PARANOID +#ifdef IMGUI_DEBUG_PARANOID +#define IM_ASSERT_PARANOID(_EXPR) IM_ASSERT(_EXPR) +#else +#define IM_ASSERT_PARANOID(_EXPR) +#endif + +// Misc Macros +#define IM_PI 3.14159265358979323846f +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" // Play it nice with Windows users (Update: since 2018-05, Notepad finally appears to support Unix-style carriage returns!) +#else +#define IM_NEWLINE "\n" +#endif +#ifndef IM_TABSIZE // Until we move this to runtime and/or add proper tab support, at least allow users to compile-time override +#define IM_TABSIZE (4) +#endif +#define IM_MEMALIGN(_OFF,_ALIGN) (((_OFF) + ((_ALIGN) - 1)) & ~((_ALIGN) - 1)) // Memory align e.g. IM_ALIGN(0,4)=0, IM_ALIGN(1,4)=4, IM_ALIGN(4,4)=4, IM_ALIGN(5,4)=8 +#define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose +#define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 +#define IM_TRUNC(_VAL) ((float)(int)(_VAL)) // ImTrunc() is not inlined in MSVC debug builds +#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // +#define IM_STRINGIFY_HELPER(_X) #_X +#define IM_STRINGIFY(_X) IM_STRINGIFY_HELPER(_X) // Preprocessor idiom to stringify e.g. an integer. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +#define IM_FLOOR IM_TRUNC +#endif + +// Hint for branch prediction +#if (defined(__cplusplus) && (__cplusplus >= 202002L)) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 202002L)) +#define IM_LIKELY [[likely]] +#define IM_UNLIKELY [[unlikely]] +#else +#define IM_LIKELY +#define IM_UNLIKELY +#endif + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif + +// Warnings +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_MSVC_WARNING_SUPPRESS(XXXX) __pragma(warning(suppress: XXXX)) +#else +#define IM_MSVC_WARNING_SUPPRESS(XXXX) +#endif + +// Debug Tools +// Use 'Metrics/Debugger->Tools->Item Picker' to break into the call-stack of a specific item. +// This will call IM_DEBUG_BREAK() which you may redefine yourself. See https://github.com/scottt/debugbreak for more reference. +#ifndef IM_DEBUG_BREAK +#if defined (_MSC_VER) +#define IM_DEBUG_BREAK() __debugbreak() +#elif defined(__clang__) +#define IM_DEBUG_BREAK() __builtin_debugtrap() +#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define IM_DEBUG_BREAK() __asm__ volatile("int3;nop") +#elif defined(__GNUC__) && defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") +#elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0") +#else +#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +#endif +#endif // #ifndef IM_DEBUG_BREAK + +// Format specifiers, printing 64-bit hasn't been decently standardized... +// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_PRId64 "I64d" +#define IM_PRIu64 "I64u" +#define IM_PRIX64 "I64X" +#else +#define IM_PRId64 "lld" +#define IM_PRIu64 "llu" +#define IM_PRIX64 "llX" +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Generic helpers +// Note that the ImXXX helpers functions are lower-level than ImGui functions. +// ImGui functions or the ImGui context are never called/used from other ImXXX functions. +//----------------------------------------------------------------------------- +// - Helpers: Hashing +// - Helpers: Sorting +// - Helpers: Bit manipulation +// - Helpers: String +// - Helpers: Formatting +// - Helpers: UTF-8 <> wchar conversions +// - Helpers: ImVec2/ImVec4 operators +// - Helpers: Maths +// - Helpers: Geometry +// - Helper: ImVec1 +// - Helper: ImVec2ih +// - Helper: ImRect +// - Helper: ImBitArray +// - Helper: ImBitVector +// - Helper: ImSpan<>, ImSpanAllocator<> +// - Helper: ImPool<> +// - Helper: ImChunkStream<> +// - Helper: ImGuiTextIndex +// - Helper: ImGuiStorage +//----------------------------------------------------------------------------- + +// Helpers: Hashing +IMGUI_API ImGuiID ImHashData(const void* data, size_t data_size, ImGuiID seed = 0); +IMGUI_API ImGuiID ImHashStr(const char* data, size_t data_size = 0, ImGuiID seed = 0); + +// Helpers: Sorting +#ifndef ImQsort +static inline void ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); } +#endif + +// Helpers: Color Blending +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b); + +// Helpers: Bit manipulation +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } +static inline unsigned int ImCountSetBits(unsigned int v) { unsigned int count = 0; while (v > 0) { v = v & (v - 1); count++; } return count; } + +// Helpers: String +#define ImStrlen strlen +#define ImMemchr memchr +IMGUI_API int ImStricmp(const char* str1, const char* str2); // Case insensitive compare. +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); // Case insensitive compare to a certain count. +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); // Copy to a certain count and always zero terminate (strncpy doesn't). +IMGUI_API char* ImStrdup(const char* str); // Duplicate a string. +IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); // Copy in provided buffer, recreate buffer if needed. +IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); // Find first occurrence of 'c' in string range. +IMGUI_API const char* ImStreolRange(const char* str, const char* str_end); // End end-of-line +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); // Find a substring in a string range. +IMGUI_API void ImStrTrimBlanks(char* str); // Remove leading and trailing blanks from a buffer. +IMGUI_API const char* ImStrSkipBlank(const char* str); // Find first non-blank character. +IMGUI_API int ImStrlenW(const ImWchar* str); // Computer string length (ImWchar string) +IMGUI_API const char* ImStrbol(const char* buf_mid_line, const char* buf_begin); // Find beginning-of-line +IM_MSVC_RUNTIME_CHECKS_OFF +static inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } +static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } +static inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } +static inline bool ImCharIsXdigitA(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Formatting +IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API const char* ImParseFormatFindStart(const char* format); +IMGUI_API const char* ImParseFormatFindEnd(const char* format); +IMGUI_API const char* ImParseFormatTrimDecorations(const char* format, char* buf, size_t buf_size); +IMGUI_API void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size); +IMGUI_API const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size); +IMGUI_API int ImParseFormatPrecision(const char* format, int default_value); + +// Helpers: UTF-8 <> wchar conversions +IMGUI_API const char* ImTextCharToUtf8(char out_buf[5], unsigned int c); // return out_buf +IMGUI_API int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count +IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // read one character. return input UTF-8 bytes count +IMGUI_API int ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count +IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) +IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 +IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 +IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. +IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. + +// Helpers: File System +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef void* ImFileHandle; +static inline ImFileHandle ImFileOpen(const char*, const char*) { return NULL; } +static inline bool ImFileClose(ImFileHandle) { return false; } +static inline ImU64 ImFileGetSize(ImFileHandle) { return (ImU64)-1; } +static inline ImU64 ImFileRead(void*, ImU64, ImU64, ImFileHandle) { return 0; } +static inline ImU64 ImFileWrite(const void*, ImU64, ImU64, ImFileHandle) { return 0; } +#endif +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef FILE* ImFileHandle; +IMGUI_API ImFileHandle ImFileOpen(const char* filename, const char* mode); +IMGUI_API bool ImFileClose(ImFileHandle file); +IMGUI_API ImU64 ImFileGetSize(ImFileHandle file); +IMGUI_API ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file); +IMGUI_API ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file); +#else +#define IMGUI_DISABLE_TTY_FUNCTIONS // Can't use stdout, fflush if we are not using default file functions +#endif +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size = NULL, int padding_bytes = 0); + +// Helpers: Maths +IM_MSVC_RUNTIME_CHECKS_OFF +// - Wrapper for standard libs functions. (Note that imgui_demo.cpp does _not_ use them to keep the code easy to copy) +#ifndef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#define ImFabs(X) fabsf(X) +#define ImSqrt(X) sqrtf(X) +#define ImFmod(X, Y) fmodf((X), (Y)) +#define ImCos(X) cosf(X) +#define ImSin(X) sinf(X) +#define ImAcos(X) acosf(X) +#define ImAtan2(Y, X) atan2f((Y), (X)) +#define ImAtof(STR) atof(STR) +#define ImCeil(X) ceilf(X) +static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision +static inline double ImPow(double x, double y) { return pow(x, y); } +static inline float ImLog(float x) { return logf(x); } // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision +static inline double ImLog(double x) { return log(x); } +static inline int ImAbs(int x) { return x < 0 ? -x : x; } +static inline float ImAbs(float x) { return fabsf(x); } +static inline double ImAbs(double x) { return fabs(x); } +static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument +static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; } +#ifdef IMGUI_ENABLE_SSE +static inline float ImRsqrt(float x) { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); } +#else +static inline float ImRsqrt(float x) { return 1.0f / sqrtf(x); } +#endif +static inline double ImRsqrt(double x) { return 1.0 / sqrt(x); } +#endif +// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double +// (Exceptionally using templates here but we could also redefine them for those types) +template static inline T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; } +template static inline T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; } +template static inline T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } +template static inline T ImLerp(T a, T b, float t) { return (T)(a + (b - a) * t); } +template static inline void ImSwap(T& a, T& b) { T tmp = a; a = b; b = tmp; } +template static inline T ImAddClampOverflow(T a, T b, T mn, T mx) { if (b < 0 && (a < mn - b)) return mn; if (b > 0 && (a > mx - b)) return mx; return a + b; } +template static inline T ImSubClampOverflow(T a, T b, T mn, T mx) { if (b > 0 && (a < mn + b)) return mn; if (b < 0 && (a > mx + b)) return mx; return a - b; } +// - Misc maths helpers +static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); } +static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2&mn, const ImVec2&mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } +static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); } +static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); } +static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } +static inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } +static inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } +static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } +static inline float ImTrunc(float f) { return (float)(int)(f); } +static inline ImVec2 ImTrunc(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } +static inline float ImFloor(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); } +static inline int ImModPositive(int a, int b) { return (a + b) % b; } +static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } +static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } +static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline float ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; } +static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } +static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Geometry +IMGUI_API ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t); +IMGUI_API ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments); // For curves with explicit number of segments +IMGUI_API ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol);// For auto-tessellated curves you can use tess_tol = style.CurveTessellationTol +IMGUI_API ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t); +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); +inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +inline bool ImTriangleIsClockwise(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ((b.x - a.x) * (c.y - b.y)) - ((c.x - b.x) * (b.y - a.y)) > 0.0f; } + +// Helper: ImVec1 (1D vector) +// (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec1 +{ + float x; + constexpr ImVec1() : x(0.0f) { } + constexpr ImVec1(float _x) : x(_x) { } +}; + +// Helper: ImVec2ih (2D vector, half-size integer, for long-term packed storage) +struct ImVec2ih +{ + short x, y; + constexpr ImVec2ih() : x(0), y(0) {} + constexpr ImVec2ih(short _x, short _y) : x(_x), y(_y) {} + constexpr explicit ImVec2ih(const ImVec2& rhs) : x((short)rhs.x), y((short)rhs.y) {} +}; + +// Helper: ImRect (2D axis aligned bounding-box) +// NB: we can't rely on ImVec2 math operators being available here! +struct IMGUI_API ImRect +{ + ImVec2 Min; // Upper-left + ImVec2 Max; // Lower-right + + constexpr ImRect() : Min(0.0f, 0.0f), Max(0.0f, 0.0f) {} + constexpr ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} + constexpr ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} + constexpr ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} + + ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } + ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } + float GetWidth() const { return Max.x - Min.x; } + float GetHeight() const { return Max.y - Min.y; } + float GetArea() const { return (Max.x - Min.x) * (Max.y - Min.y); } + ImVec2 GetTL() const { return Min; } // Top-left + ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right + ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left + ImVec2 GetBR() const { return Max; } // Bottom-right + bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } + bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool ContainsWithPad(const ImVec2& p, const ImVec2& pad) const { return p.x >= Min.x - pad.x && p.y >= Min.y - pad.y && p.x < Max.x + pad.x && p.y < Max.y + pad.y; } + bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } + void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } + void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } + void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } + void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } + void TranslateX(float dx) { Min.x += dx; Max.x += dx; } + void TranslateY(float dy) { Min.y += dy; Max.y += dy; } + void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. + void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. + void Floor() { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); } + bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } + ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } +}; + +// Helper: ImBitArray +#define IM_BITARRAY_TESTBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] & ((ImU32)1 << ((_N) & 31))) != 0) // Macro version of ImBitArrayTestBit(): ensure args have side-effect or are costly! +#define IM_BITARRAY_CLEARBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] &= ~((ImU32)1 << ((_N) & 31)))) // Macro version of ImBitArrayClearBit(): ensure args have side-effect or are costly! +inline size_t ImBitArrayGetStorageSizeInBytes(int bitcount) { return (size_t)((bitcount + 31) >> 5) << 2; } +inline void ImBitArrayClearAllBits(ImU32* arr, int bitcount){ memset(arr, 0, ImBitArrayGetStorageSizeInBytes(bitcount)); } +inline bool ImBitArrayTestBit(const ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); return (arr[n >> 5] & mask) != 0; } +inline void ImBitArrayClearBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] &= ~mask; } +inline void ImBitArraySetBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] |= mask; } +inline void ImBitArraySetBitRange(ImU32* arr, int n, int n2) // Works on range [n..n2) +{ + n2--; + while (n <= n2) + { + int a_mod = (n & 31); + int b_mod = (n2 > (n | 31) ? 31 : (n2 & 31)) + 1; + ImU32 mask = (ImU32)(((ImU64)1 << b_mod) - 1) & ~(ImU32)(((ImU64)1 << a_mod) - 1); + arr[n >> 5] |= mask; + n = (n + 32) & ~31; + } +} + +typedef ImU32* ImBitArrayPtr; // Name for use in structs + +// Helper: ImBitArray class (wrapper over ImBitArray functions) +// Store 1-bit per value. +template +struct ImBitArray +{ + ImU32 Storage[(BITCOUNT + 31) >> 5]; + ImBitArray() { ClearAllBits(); } + void ClearAllBits() { memset(Storage, 0, sizeof(Storage)); } + void SetAllBits() { memset(Storage, 255, sizeof(Storage)); } + bool TestBit(int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } + void SetBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArraySetBit(Storage, n); } + void ClearBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArrayClearBit(Storage, n); } + void SetBitRange(int n, int n2) { n += OFFSET; n2 += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT && n2 > n && n2 <= BITCOUNT); ImBitArraySetBitRange(Storage, n, n2); } // Works on range [n..n2) + bool operator[](int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } +}; + +// Helper: ImBitVector +// Store 1-bit per value. +struct IMGUI_API ImBitVector +{ + ImVector Storage; + void Create(int sz) { Storage.resize((sz + 31) >> 5); memset(Storage.Data, 0, (size_t)Storage.Size * sizeof(Storage.Data[0])); } + void Clear() { Storage.clear(); } + bool TestBit(int n) const { IM_ASSERT(n < (Storage.Size << 5)); return IM_BITARRAY_TESTBIT(Storage.Data, n); } + void SetBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArraySetBit(Storage.Data, n); } + void ClearBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArrayClearBit(Storage.Data, n); } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helper: ImSpan<> +// Pointing to a span of data we don't own. +template +struct ImSpan +{ + T* Data; + T* DataEnd; + + // Constructors, destructor + inline ImSpan() { Data = DataEnd = NULL; } + inline ImSpan(T* data, int size) { Data = data; DataEnd = data + size; } + inline ImSpan(T* data, T* data_end) { Data = data; DataEnd = data_end; } + + inline void set(T* data, int size) { Data = data; DataEnd = data + size; } + inline void set(T* data, T* data_end) { Data = data; DataEnd = data_end; } + inline int size() const { return (int)(ptrdiff_t)(DataEnd - Data); } + inline int size_in_bytes() const { return (int)(ptrdiff_t)(DataEnd - Data) * (int)sizeof(T); } + inline T& operator[](int i) { T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + inline const T& operator[](int i) const { const T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return DataEnd; } + inline const T* end() const { return DataEnd; } + + // Utilities + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < DataEnd); const ptrdiff_t off = it - Data; return (int)off; } +}; + +// Helper: ImSpanAllocator<> +// Facilitate storing multiple chunks into a single large block (the "arena") +// - Usage: call Reserve() N times, allocate GetArenaSizeInBytes() worth, pass it to SetArenaBasePtr(), call GetSpan() N times to retrieve the aligned ranges. +template +struct ImSpanAllocator +{ + char* BasePtr; + int CurrOff; + int CurrIdx; + int Offsets[CHUNKS]; + int Sizes[CHUNKS]; + + ImSpanAllocator() { memset(this, 0, sizeof(*this)); } + inline void Reserve(int n, size_t sz, int a=4) { IM_ASSERT(n == CurrIdx && n < CHUNKS); CurrOff = IM_MEMALIGN(CurrOff, a); Offsets[n] = CurrOff; Sizes[n] = (int)sz; CurrIdx++; CurrOff += (int)sz; } + inline int GetArenaSizeInBytes() { return CurrOff; } + inline void SetArenaBasePtr(void* base_ptr) { BasePtr = (char*)base_ptr; } + inline void* GetSpanPtrBegin(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n]); } + inline void* GetSpanPtrEnd(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n] + Sizes[n]); } + template + inline void GetSpan(int n, ImSpan* span) { span->set((T*)GetSpanPtrBegin(n), (T*)GetSpanPtrEnd(n)); } +}; + +// Helper: ImPool<> +// Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer, +// Honor constructor/destructor. Add/remove invalidate all pointers. Indexes have the same lifetime as the associated object. +typedef int ImPoolIdx; +template +struct ImPool +{ + ImVector Buf; // Contiguous data + ImGuiStorage Map; // ID->Index + ImPoolIdx FreeIdx; // Next free idx to use + ImPoolIdx AliveCount; // Number of active/alive items (for display purpose) + + ImPool() { FreeIdx = AliveCount = 0; } + ~ImPool() { Clear(); } + T* GetByKey(ImGuiID key) { int idx = Map.GetInt(key, -1); return (idx != -1) ? &Buf[idx] : NULL; } + T* GetByIndex(ImPoolIdx n) { return &Buf[n]; } + ImPoolIdx GetIndex(const T* p) const { IM_ASSERT(p >= Buf.Data && p < Buf.Data + Buf.Size); return (ImPoolIdx)(p - Buf.Data); } + T* GetOrAddByKey(ImGuiID key) { int* p_idx = Map.GetIntRef(key, -1); if (*p_idx != -1) return &Buf[*p_idx]; *p_idx = FreeIdx; return Add(); } + bool Contains(const T* p) const { return (p >= Buf.Data && p < Buf.Data + Buf.Size); } + void Clear() { for (int n = 0; n < Map.Data.Size; n++) { int idx = Map.Data[n].val_i; if (idx != -1) Buf[idx].~T(); } Map.Clear(); Buf.clear(); FreeIdx = AliveCount = 0; } + T* Add() { int idx = FreeIdx; if (idx == Buf.Size) { Buf.resize(Buf.Size + 1); FreeIdx++; } else { FreeIdx = *(int*)&Buf[idx]; } IM_PLACEMENT_NEW(&Buf[idx]) T(); AliveCount++; return &Buf[idx]; } + void Remove(ImGuiID key, const T* p) { Remove(key, GetIndex(p)); } + void Remove(ImGuiID key, ImPoolIdx idx) { Buf[idx].~T(); *(int*)&Buf[idx] = FreeIdx; FreeIdx = idx; Map.SetInt(key, -1); AliveCount--; } + void Reserve(int capacity) { Buf.reserve(capacity); Map.Data.reserve(capacity); } + + // To iterate a ImPool: for (int n = 0; n < pool.GetMapSize(); n++) if (T* t = pool.TryGetMapData(n)) { ... } + // Can be avoided if you know .Remove() has never been called on the pool, or AliveCount == GetMapSize() + int GetAliveCount() const { return AliveCount; } // Number of active/alive items in the pool (for display purpose) + int GetBufSize() const { return Buf.Size; } + int GetMapSize() const { return Map.Data.Size; } // It is the map we need iterate to find valid items, since we don't have "alive" storage anywhere + T* TryGetMapData(ImPoolIdx n) { int idx = Map.Data[n].val_i; if (idx == -1) return NULL; return GetByIndex(idx); } +}; + +// Helper: ImChunkStream<> +// Build and iterate a contiguous stream of variable-sized structures. +// This is used by Settings to store persistent data while reducing allocation count. +// We store the chunk size first, and align the final size on 4 bytes boundaries. +// The tedious/zealous amount of casting is to avoid -Wcast-align warnings. +template +struct ImChunkStream +{ + ImVector Buf; + + void clear() { Buf.clear(); } + bool empty() const { return Buf.Size == 0; } + int size() const { return Buf.Size; } + T* alloc_chunk(size_t sz) { size_t HDR_SZ = 4; sz = IM_MEMALIGN(HDR_SZ + sz, 4u); int off = Buf.Size; Buf.resize(off + (int)sz); ((int*)(void*)(Buf.Data + off))[0] = (int)sz; return (T*)(void*)(Buf.Data + off + (int)HDR_SZ); } + T* begin() { size_t HDR_SZ = 4; if (!Buf.Data) return NULL; return (T*)(void*)(Buf.Data + HDR_SZ); } + T* next_chunk(T* p) { size_t HDR_SZ = 4; IM_ASSERT(p >= begin() && p < end()); p = (T*)(void*)((char*)(void*)p + chunk_size(p)); if (p == (T*)(void*)((char*)end() + HDR_SZ)) return (T*)0; IM_ASSERT(p < end()); return p; } + int chunk_size(const T* p) { return ((const int*)p)[-1]; } + T* end() { return (T*)(void*)(Buf.Data + Buf.Size); } + int offset_from_ptr(const T* p) { IM_ASSERT(p >= begin() && p < end()); const ptrdiff_t off = (const char*)p - Buf.Data; return (int)off; } + T* ptr_from_offset(int off) { IM_ASSERT(off >= 4 && off < Buf.Size); return (T*)(void*)(Buf.Data + off); } + void swap(ImChunkStream& rhs) { rhs.Buf.swap(Buf); } +}; + +// Helper: ImGuiTextIndex +// Maintain a line index for a text buffer. This is a strong candidate to be moved into the public API. +struct ImGuiTextIndex +{ + ImVector LineOffsets; + int EndOffset = 0; // Because we don't own text buffer we need to maintain EndOffset (may bake in LineOffsets?) + + void clear() { LineOffsets.clear(); EndOffset = 0; } + int size() { return LineOffsets.Size; } + const char* get_line_begin(const char* base, int n) { return base + LineOffsets[n]; } + const char* get_line_end(const char* base, int n) { return base + (n + 1 < LineOffsets.Size ? (LineOffsets[n + 1] - 1) : EndOffset); } + void append(const char* base, int old_size, int new_size); +}; + +// Helper: ImGuiStorage +IMGUI_API ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key); + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList support +//----------------------------------------------------------------------------- + +// ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value. +// Estimation of number of circle segment based on error is derived using method described in https://stackoverflow.com/a/2244088/15194693 +// Number of segments (N) is calculated using equation: +// N = ceil ( pi / acos(1 - error / r) ) where r > 0, error <= r +// Our equation is significantly simpler that one in the post thanks for choosing segment that is +// perpendicular to X axis. Follow steps in the article from this starting condition and you will +// will get this result. +// +// Rendering circles with an odd number of segments, while mathematically correct will produce +// asymmetrical results on the raster grid. Therefore we're rounding N to next even number (7->8, 8->8, 9->10 etc.) +#define IM_ROUNDUP_TO_EVEN(_V) ((((_V) + 1) / 2) * 2) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 4 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp(IM_ROUNDUP_TO_EVEN((int)ImCeil(IM_PI / ImAcos(1 - ImMin((_MAXERROR), (_RAD)) / (_RAD)))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) + +// Raw equation from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC rewritten for 'r' and 'error'. +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(_N,_MAXERROR) ((_MAXERROR) / (1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI)))) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_ERROR(_N,_RAD) ((1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI))) / (_RAD)) + +// ImDrawList: Lookup table size for adaptive arc drawing, cover full circle. +#ifndef IM_DRAWLIST_ARCFAST_TABLE_SIZE +#define IM_DRAWLIST_ARCFAST_TABLE_SIZE 48 // Number of samples in lookup table. +#endif +#define IM_DRAWLIST_ARCFAST_SAMPLE_MAX IM_DRAWLIST_ARCFAST_TABLE_SIZE // Sample index _PathArcToFastEx() for 360 angle. + +// Data shared between all ImDrawList instances +// Conceptually this could have been called e.g. ImDrawListSharedContext +// Typically one ImGui context would create and maintain one of this. +// You may want to create your own instance of you try to ImDrawList completely without ImGui. In that case, watch out for future changes to this structure. +struct IMGUI_API ImDrawListSharedData +{ + ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas + const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas + ImFont* Font; // Current/default font (optional, for simplified AddText overload) + float FontSize; // Current/default font size (optional, for simplified AddText overload) + float FontScale; // Current/default font scale (== FontSize / Font->FontSize) + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() + float CircleSegmentMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc + float InitialFringeScale; // Initial scale to apply to AA fringe + ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) + ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + ImVector TempBuffer; // Temporary write buffer + + // Lookup tables + ImVec2 ArcFastVtx[IM_DRAWLIST_ARCFAST_TABLE_SIZE]; // Sample points on the quarter of the circle. + float ArcFastRadiusCutoff; // Cutoff radius after which arc drawing will fallback to slower PathArcTo() + ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + + ImDrawListSharedData(); + void SetCircleTessellationMaxError(float max_error); +}; + +struct ImDrawDataBuilder +{ + ImVector* Layers[2]; // Pointers to global layers for: regular, tooltip. LayersP[0] is owned by DrawData. + ImVector LayerData1; + + ImDrawDataBuilder() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Style support +//----------------------------------------------------------------------------- + +struct ImGuiStyleVarInfo +{ + ImU32 Count : 8; // 1+ + ImGuiDataType DataType : 8; + ImU32 Offset : 16; // Offset in parent structure + void* GetVarPtr(void* parent) const { return (void*)((unsigned char*)parent + Offset); } +}; + +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColorMod +{ + ImGuiCol Col; + ImVec4 BackupValue; +}; + +// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. +struct ImGuiStyleMod +{ + ImGuiStyleVar VarIdx; + union { int BackupInt[2]; float BackupFloat[2]; }; + ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Data types support +//----------------------------------------------------------------------------- + +struct ImGuiDataTypeStorage +{ + ImU8 Data[8]; // Opaque storage to fit any data up to ImGuiDataType_COUNT +}; + +// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). +struct ImGuiDataTypeInfo +{ + size_t Size; // Size in bytes + const char* Name; // Short descriptive name for the type, for debugging + const char* PrintFmt; // Default printf format for the type + const char* ScanFmt; // Default scanf format for the type +}; + +// Extend ImGuiDataType_ +enum ImGuiDataTypePrivate_ +{ + ImGuiDataType_Pointer = ImGuiDataType_COUNT, + ImGuiDataType_ID, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Widgets support: flags, enums, data structures +//----------------------------------------------------------------------------- + +// Extend ImGuiItemFlags +// - input: PushItemFlag() manipulates g.CurrentItemFlags, g.NextItemData.ItemFlags, ItemAdd() calls may add extra flags too. +// - output: stored in g.LastItemData.ItemFlags +enum ImGuiItemFlagsPrivate_ +{ + // Controlled by user + ImGuiItemFlags_Disabled = 1 << 10, // false // Disable interactions (DOES NOT affect visuals. DO NOT mix direct use of this with BeginDisabled(). See BeginDisabled()/EndDisabled() for full disable feature, and github #211). + ImGuiItemFlags_ReadOnly = 1 << 11, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. + ImGuiItemFlags_MixedValue = 1 << 12, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) + ImGuiItemFlags_NoWindowHoverableCheck = 1 << 13, // false // Disable hoverable check in ItemHoverable() + ImGuiItemFlags_AllowOverlap = 1 << 14, // false // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame. + ImGuiItemFlags_NoNavDisableMouseHover = 1 << 15, // false // Nav keyboard/gamepad mode doesn't disable hover highlight (behave as if NavHighlightItemUnderNav==false). + ImGuiItemFlags_NoMarkEdited = 1 << 16, // false // Skip calling MarkItemEdited() + + // Controlled by widget code + ImGuiItemFlags_Inputable = 1 << 20, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. + ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData() + ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData() + + ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead. + + // Obsolete + //ImGuiItemFlags_SelectableDontClosePopup = !ImGuiItemFlags_AutoClosePopups, // Can't have a redirect as we inverted the behavior +}; + +// Status flags for an already submitted item +// - output: stored in g.LastItemData.StatusFlags +enum ImGuiItemStatusFlags_ +{ + ImGuiItemStatusFlags_None = 0, + ImGuiItemStatusFlags_HoveredRect = 1 << 0, // Mouse position is within item rectangle (does NOT mean that the window is in correct z-order and can be hovered!, this is only one part of the most-common IsItemHovered test) + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, // g.LastItemData.DisplayRect is valid + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3, // Set when Selectable(), TreeNode() reports toggling a selection. We can't report "Selected", only state changes, in order to easily handle clipping with less issues. + ImGuiItemStatusFlags_ToggledOpen = 1 << 4, // Set when TreeNode() reports toggling their open state. + ImGuiItemStatusFlags_HasDeactivated = 1 << 5, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. + ImGuiItemStatusFlags_Deactivated = 1 << 6, // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. + ImGuiItemStatusFlags_HoveredWindow = 1 << 7, // Override the HoveredWindow test to allow cross-window hover testing. + ImGuiItemStatusFlags_Visible = 1 << 8, // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()). + ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid. + ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd(). + + // Additional status + semantic for ImGuiTestEngine +#ifdef IMGUI_ENABLE_TEST_ENGINE + ImGuiItemStatusFlags_Openable = 1 << 20, // Item is an openable (e.g. TreeNode) + ImGuiItemStatusFlags_Opened = 1 << 21, // Opened status + ImGuiItemStatusFlags_Checkable = 1 << 22, // Item is a checkable (e.g. CheckBox, MenuItem) + ImGuiItemStatusFlags_Checked = 1 << 23, // Checked status + ImGuiItemStatusFlags_Inputable = 1 << 24, // Item is a text-inputable (e.g. InputText, SliderXXX, DragXXX) +#endif +}; + +// Extend ImGuiHoveredFlags_ +enum ImGuiHoveredFlagsPrivate_ +{ + ImGuiHoveredFlags_DelayMask_ = ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, + ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, + ImGuiHoveredFlags_AllowedMaskForIsItemHovered = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayMask_, +}; + +// Extend ImGuiInputTextFlags_ +enum ImGuiInputTextFlagsPrivate_ +{ + // [Internal] + ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() + ImGuiInputTextFlags_MergedItem = 1 << 27, // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. + ImGuiInputTextFlags_LocalizeDecimalPoint= 1 << 28, // For internal use by InputScalar() and TempInputScalar() +}; + +// Extend ImGuiButtonFlags_ +enum ImGuiButtonFlagsPrivate_ +{ + ImGuiButtonFlags_PressedOnClick = 1 << 4, // return true on click (mouse down event) + ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, // [Default] return true on click + release on same item <-- this is what the majority of Button are using + ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, // return true on click + release even if the release event is not done while hovering the item + ImGuiButtonFlags_PressedOnRelease = 1 << 7, // return true on release (default requires click+release) + ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, // return true on double-click (default requires click+release) + ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, // return true when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) + //ImGuiButtonFlags_Repeat = 1 << 10, // hold to repeat -> use ImGuiItemFlags_ButtonRepeat instead. + ImGuiButtonFlags_FlattenChildren = 1 << 11, // allow interactions even if a child window is overlapping + ImGuiButtonFlags_AllowOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable. + //ImGuiButtonFlags_DontClosePopups = 1 << 13, // disable automatically closing parent popup on press + //ImGuiButtonFlags_Disabled = 1 << 14, // disable interactions -> use BeginDisabled() or ImGuiItemFlags_Disabled + ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine + ImGuiButtonFlags_NoKeyModsAllowed = 1 << 16, // disable mouse interaction if a key modifier is held + ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) + ImGuiButtonFlags_NoNavFocus = 1 << 18, // don't override navigation focus when activated (FIXME: this is essentially used every time an item uses ImGuiItemFlags_NoNav, but because legacy specs don't requires LastItemData to be set ButtonBehavior(), we can't poll g.LastItemData.ItemFlags) + ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, // don't report as hovered when nav focus is on this item + ImGuiButtonFlags_NoSetKeyOwner = 1 << 20, // don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_NoTestKeyOwner = 1 << 21, // don't test key/input owner when polling the key (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_PressedOnMask_ = ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_PressedOnDragDropHold, + ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease, +}; + +// Extend ImGuiComboFlags_ +enum ImGuiComboFlagsPrivate_ +{ + ImGuiComboFlags_CustomPreview = 1 << 20, // enable BeginComboPreview() +}; + +// Extend ImGuiSliderFlags_ +enum ImGuiSliderFlagsPrivate_ +{ + ImGuiSliderFlags_Vertical = 1 << 20, // Should this slider be orientated vertically? + ImGuiSliderFlags_ReadOnly = 1 << 21, // Consider using g.NextItemData.ItemFlags |= ImGuiItemFlags_ReadOnly instead. +}; + +// Extend ImGuiSelectableFlags_ +enum ImGuiSelectableFlagsPrivate_ +{ + // NB: need to be in sync with last value of ImGuiSelectableFlags_ + ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, + ImGuiSelectableFlags_SelectOnNav = 1 << 21, // (WIP) Auto-select when moved into. This is not exposed in public API as to handle multi-select and modifiers we will need user to explicitly control focus scope. May be replaced with a BeginSelection() API. + ImGuiSelectableFlags_SelectOnClick = 1 << 22, // Override button behavior to react on Click (default is Click+Release) + ImGuiSelectableFlags_SelectOnRelease = 1 << 23, // Override button behavior to react on Release (default is Click+Release) + ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, // Span all avail width even if we declared less for layout purpose. FIXME: We may be able to remove this (added in 6251d379, 2bcafc86 for menus) + ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25, // Set Nav/Focus ID on mouse hover (used by MenuItem) + ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 26, // Disable padding each side with ItemSpacing * 0.5f + ImGuiSelectableFlags_NoSetKeyOwner = 1 << 27, // Don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) +}; + +// Extend ImGuiTreeNodeFlags_ +enum ImGuiTreeNodeFlagsPrivate_ +{ + ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 28,// FIXME-WIP: Hard-coded for CollapsingHeader() + ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 29,// FIXME-WIP: Turn Down arrow into an Up arrow, for reversed trees (#6517) + ImGuiTreeNodeFlags_OpenOnMask_ = ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow, +}; + +enum ImGuiSeparatorFlags_ +{ + ImGuiSeparatorFlags_None = 0, + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1, + ImGuiSeparatorFlags_SpanAllColumns = 1 << 2, // Make separator cover all columns of a legacy Columns() set. +}; + +// Flags for FocusWindow(). This is not called ImGuiFocusFlags to avoid confusion with public-facing ImGuiFocusedFlags. +// FIXME: Once we finishing replacing more uses of GetTopMostPopupModal()+IsWindowWithinBeginStackOf() +// and FindBlockingModal() with this, we may want to change the flag to be opt-out instead of opt-in. +enum ImGuiFocusRequestFlags_ +{ + ImGuiFocusRequestFlags_None = 0, + ImGuiFocusRequestFlags_RestoreFocusedChild = 1 << 0, // Find last focused child (if any) and focus it instead. + ImGuiFocusRequestFlags_UnlessBelowModal = 1 << 1, // Do not set focus if the window is below a modal. +}; + +enum ImGuiTextFlags_ +{ + ImGuiTextFlags_None = 0, + ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0, +}; + +enum ImGuiTooltipFlags_ +{ + ImGuiTooltipFlags_None = 0, + ImGuiTooltipFlags_OverridePrevious = 1 << 1, // Clear/ignore previously submitted tooltip (defaults to append) +}; + +// FIXME: this is in development, not exposed/functional as a generic feature yet. +// Horizontal/Vertical enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiLayoutType_ +{ + ImGuiLayoutType_Horizontal = 0, + ImGuiLayoutType_Vertical = 1 +}; + +// Flags for LogBegin() text capturing function +enum ImGuiLogFlags_ +{ + ImGuiLogFlags_None = 0, + + ImGuiLogFlags_OutputTTY = 1 << 0, + ImGuiLogFlags_OutputFile = 1 << 1, + ImGuiLogFlags_OutputBuffer = 1 << 2, + ImGuiLogFlags_OutputClipboard = 1 << 3, + ImGuiLogFlags_OutputMask_ = ImGuiLogFlags_OutputTTY | ImGuiLogFlags_OutputFile | ImGuiLogFlags_OutputBuffer | ImGuiLogFlags_OutputClipboard, +}; + +// X/Y enums are fixed to 0/1 so they may be used to index ImVec2 +enum ImGuiAxis +{ + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 +}; + +enum ImGuiPlotType +{ + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram, +}; + +// Storage data for BeginComboPreview()/EndComboPreview() +struct IMGUI_API ImGuiComboPreviewData +{ + ImRect PreviewRect; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + float BackupPrevLineTextBaseOffset; + ImGuiLayoutType BackupLayout; + + ImGuiComboPreviewData() { memset(this, 0, sizeof(*this)); } +}; + +// Stacked storage data for BeginGroup()/EndGroup() +struct IMGUI_API ImGuiGroupData +{ + ImGuiID WindowID; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + ImVec1 BackupIndent; + ImVec1 BackupGroupOffset; + ImVec2 BackupCurrLineSize; + float BackupCurrLineTextBaseOffset; + ImGuiID BackupActiveIdIsAlive; + bool BackupDeactivatedIdIsAlive; + bool BackupHoveredIdIsAlive; + bool BackupIsSameLine; + bool EmitItem; +}; + +// Simple column measurement, currently used for MenuItem() only.. This is very short-sighted/throw-away code and NOT a generic helper. +struct IMGUI_API ImGuiMenuColumns +{ + ImU32 TotalWidth; + ImU32 NextTotalWidth; + ImU16 Spacing; + ImU16 OffsetIcon; // Always zero for now + ImU16 OffsetLabel; // Offsets are locked in Update() + ImU16 OffsetShortcut; + ImU16 OffsetMark; + ImU16 Widths[4]; // Width of: Icon, Label, Shortcut, Mark (accumulators for current frame) + + ImGuiMenuColumns() { memset(this, 0, sizeof(*this)); } + void Update(float spacing, bool window_reappearing); + float DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark); + void CalcNextTotalWidth(bool update_offsets); +}; + +// Internal temporary state for deactivating InputText() instances. +struct IMGUI_API ImGuiInputTextDeactivatedState +{ + ImGuiID ID; // widget id owning the text state (which just got deactivated) + ImVector TextA; // text buffer + + ImGuiInputTextDeactivatedState() { memset(this, 0, sizeof(*this)); } + void ClearFreeMemory() { ID = 0; TextA.clear(); } +}; + +// Forward declare imstb_textedit.h structure + make its main configuration define accessible +#undef IMSTB_TEXTEDIT_STRING +#undef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_STRING ImGuiInputTextState +#define IMSTB_TEXTEDIT_CHARTYPE char +#define IMSTB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 +namespace ImStb { struct STB_TexteditState; } +typedef ImStb::STB_TexteditState ImStbTexteditState; + +// Internal state of the currently focused/edited text input box +// For a given item ID, access with ImGui::GetInputTextState() +struct IMGUI_API ImGuiInputTextState +{ + ImGuiContext* Ctx; // parent UI context (needs to be set explicitly by parent). + ImStbTexteditState* Stb; // State for stb_textedit.h + ImGuiInputTextFlags Flags; // copy of InputText() flags. may be used to check if e.g. ImGuiInputTextFlags_Password is set. + ImGuiID ID; // widget id owning the text state + int TextLen; // UTF-8 length of the string in TextA (in bytes) + const char* TextSrc; // == TextA.Data unless read-only, in which case == buf passed to InputText(). Field only set and valid _inside_ the call InputText() call. + ImVector TextA; // main UTF8 buffer. TextA.Size is a buffer size! Should always be >= buf_size passed by user (and of course >= CurLenA + 1). + ImVector TextToRevertTo; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) + ImVector CallbackTextBackup; // temporary storage for callback to support automatic reconcile of undo-stack + int BufCapacity; // end-user buffer capacity (include zero terminator) + ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) + float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately + bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) + bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection + bool Edited; // edited this frame + bool WantReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. + int ReloadSelectionStart; + int ReloadSelectionEnd; + + ImGuiInputTextState(); + ~ImGuiInputTextState(); + void ClearText() { TextLen = 0; TextA[0] = 0; CursorClamp(); } + void ClearFreeMemory() { TextA.clear(); TextToRevertTo.clear(); } + void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation + void OnCharPressed(unsigned int c); + + // Cursor & Selection + void CursorAnimReset(); + void CursorClamp(); + bool HasSelection() const; + void ClearSelection(); + int GetCursorPos() const; + int GetSelectionStart() const; + int GetSelectionEnd() const; + void SelectAll(); + + // Reload user buf (WIP #2890) + // If you modify underlying user-passed const char* while active you need to call this (InputText V2 may lift this) + // strcpy(my_buf, "hello"); + // if (ImGuiInputTextState* state = ImGui::GetInputTextState(id)) // id may be ImGui::GetItemID() is last item + // state->ReloadUserBufAndSelectAll(); + void ReloadUserBufAndSelectAll(); + void ReloadUserBufAndKeepSelection(); + void ReloadUserBufAndMoveToEnd(); +}; + +enum ImGuiWindowRefreshFlags_ +{ + ImGuiWindowRefreshFlags_None = 0, + ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, // [EXPERIMENTAL] Try to keep existing contents, USER MUST NOT HONOR BEGIN() RETURNING FALSE AND NOT APPEND. + ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, // [EXPERIMENTAL] Always refresh on hover + ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, // [EXPERIMENTAL] Always refresh on focus + // Refresh policy/frequency, Load Balancing etc. +}; + +enum ImGuiNextWindowDataFlags_ +{ + ImGuiNextWindowDataFlags_None = 0, + ImGuiNextWindowDataFlags_HasPos = 1 << 0, + ImGuiNextWindowDataFlags_HasSize = 1 << 1, + ImGuiNextWindowDataFlags_HasContentSize = 1 << 2, + ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, + ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, + ImGuiNextWindowDataFlags_HasFocus = 1 << 5, + ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, + ImGuiNextWindowDataFlags_HasScroll = 1 << 7, + ImGuiNextWindowDataFlags_HasWindowFlags = 1 << 8, + ImGuiNextWindowDataFlags_HasChildFlags = 1 << 9, + ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 10, +}; + +// Storage for SetNexWindow** functions +struct ImGuiNextWindowData +{ + ImGuiNextWindowDataFlags HasFlags; + + // Members below are NOT cleared. Always rely on HasFlags. + ImGuiCond PosCond; + ImGuiCond SizeCond; + ImGuiCond CollapsedCond; + ImVec2 PosVal; + ImVec2 PosPivotVal; + ImVec2 SizeVal; + ImVec2 ContentSizeVal; + ImVec2 ScrollVal; + ImGuiWindowFlags WindowFlags; // Only honored by BeginTable() + ImGuiChildFlags ChildFlags; + bool CollapsedVal; + ImRect SizeConstraintRect; + ImGuiSizeCallback SizeCallback; + void* SizeCallbackUserData; + float BgAlphaVal; // Override background alpha + ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) + ImGuiWindowRefreshFlags RefreshFlagsVal; + + ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } + inline void ClearFlags() { HasFlags = ImGuiNextWindowDataFlags_None; } +}; + +enum ImGuiNextItemDataFlags_ +{ + ImGuiNextItemDataFlags_None = 0, + ImGuiNextItemDataFlags_HasWidth = 1 << 0, + ImGuiNextItemDataFlags_HasOpen = 1 << 1, + ImGuiNextItemDataFlags_HasShortcut = 1 << 2, + ImGuiNextItemDataFlags_HasRefVal = 1 << 3, + ImGuiNextItemDataFlags_HasStorageID = 1 << 4, +}; + +struct ImGuiNextItemData +{ + ImGuiNextItemDataFlags HasFlags; // Called HasFlags instead of Flags to avoid mistaking this + ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap and ImGuiItemFlags_HasSelectionUserData. + + // Members below are NOT cleared by ItemAdd() meaning they are still valid during e.g. NavProcessItem(). Always rely on HasFlags. + ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() + ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) + float Width; // Set by SetNextItemWidth() + ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() + ImGuiInputFlags ShortcutFlags; // Set by SetNextItemShortcut() + bool OpenVal; // Set by SetNextItemOpen() + ImU8 OpenCond; // Set by SetNextItemOpen() + ImGuiDataTypeStorage RefVal; // Not exposed yet, for ImGuiInputTextFlags_ParseEmptyAsRefVal + ImGuiID StorageId; // Set by SetNextItemStorageID() + + ImGuiNextItemData() { memset(this, 0, sizeof(*this)); SelectionUserData = -1; } + inline void ClearFlags() { HasFlags = ImGuiNextItemDataFlags_None; ItemFlags = ImGuiItemFlags_None; } // Also cleared manually by ItemAdd()! +}; + +// Status storage for the last submitted item +struct ImGuiLastItemData +{ + ImGuiID ID; + ImGuiItemFlags ItemFlags; // See ImGuiItemFlags_ (called 'InFlags' before v1.91.4). + ImGuiItemStatusFlags StatusFlags; // See ImGuiItemStatusFlags_ + ImRect Rect; // Full rectangle + ImRect NavRect; // Navigation scoring rectangle (not displayed) + // Rarely used fields are not explicitly cleared, only valid when the corresponding ImGuiItemStatusFlags are set. + ImRect DisplayRect; // Display rectangle. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) is set. + ImRect ClipRect; // Clip rectangle at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasClipRect) is set.. + ImGuiKeyChord Shortcut; // Shortcut at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasShortcut) is set.. + + ImGuiLastItemData() { memset(this, 0, sizeof(*this)); } +}; + +// Store data emitted by TreeNode() for usage by TreePop() +// - To implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere: store the minimum amount of data +// which we can't infer in TreePop(), to perform the equivalent of NavApplyItemToResult(). +// Only stored when the node is a potential candidate for landing on a Left arrow jump. +struct ImGuiTreeNodeStackData +{ + ImGuiID ID; + ImGuiTreeNodeFlags TreeFlags; + ImGuiItemFlags ItemFlags; // Used for nav landing + ImRect NavRect; // Used for nav landing +}; + +// sizeof() = 20 +struct IMGUI_API ImGuiErrorRecoveryState +{ + short SizeOfWindowStack; + short SizeOfIDStack; + short SizeOfTreeStack; + short SizeOfColorStack; + short SizeOfStyleVarStack; + short SizeOfFontStack; + short SizeOfFocusScopeStack; + short SizeOfGroupStack; + short SizeOfItemFlagsStack; + short SizeOfBeginPopupStack; + short SizeOfDisabledStack; + + ImGuiErrorRecoveryState() { memset(this, 0, sizeof(*this)); } +}; + +// Data saved for each window pushed into the stack +struct ImGuiWindowStackData +{ + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiErrorRecoveryState StackSizesInBegin; // Store size of various stacks for asserting + bool DisabledOverrideReenable; // Non-child window override disabled flag + float DisabledOverrideReenableAlphaBackup; +}; + +struct ImGuiShrinkWidthItem +{ + int Index; + float Width; + float InitialWidth; +}; + +struct ImGuiPtrOrIndex +{ + void* Ptr; // Either field can be set, not both. e.g. Dock node tab bars are loose while BeginTabBar() ones are in a pool. + int Index; // Usually index in a main pool. + + ImGuiPtrOrIndex(void* ptr) { Ptr = ptr; Index = -1; } + ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } +}; + +// Data used by IsItemDeactivated()/IsItemDeactivatedAfterEdit() functions +struct ImGuiDeactivatedItemData +{ + ImGuiID ID; + int ElapseFrame; + bool HasBeenEditedBefore; + bool IsAlive; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Popup support +//----------------------------------------------------------------------------- + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox, + ImGuiPopupPositionPolicy_Tooltip, +}; + +// Storage for popup stacks (g.OpenPopupStack and g.BeginPopupStack) +struct ImGuiPopupData +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* RestoreNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close + int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + + ImGuiPopupData() { memset(this, 0, sizeof(*this)); ParentNavLayer = OpenFrameCount = -1; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Inputs support +//----------------------------------------------------------------------------- + +// Bit array for named keys +typedef ImBitArray ImBitArrayForNamedKeys; + +// [Internal] Key ranges +#define ImGuiKey_LegacyNativeKey_BEGIN 0 +#define ImGuiKey_LegacyNativeKey_END 512 +#define ImGuiKey_Keyboard_BEGIN (ImGuiKey_NamedKey_BEGIN) +#define ImGuiKey_Keyboard_END (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_BEGIN (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_END (ImGuiKey_GamepadRStickDown + 1) +#define ImGuiKey_Mouse_BEGIN (ImGuiKey_MouseLeft) +#define ImGuiKey_Mouse_END (ImGuiKey_MouseWheelY + 1) +#define ImGuiKey_Aliases_BEGIN (ImGuiKey_Mouse_BEGIN) +#define ImGuiKey_Aliases_END (ImGuiKey_Mouse_END) + +// [Internal] Named shortcuts for Navigation +#define ImGuiKey_NavKeyboardTweakSlow ImGuiMod_Ctrl +#define ImGuiKey_NavKeyboardTweakFast ImGuiMod_Shift +#define ImGuiKey_NavGamepadTweakSlow ImGuiKey_GamepadL1 +#define ImGuiKey_NavGamepadTweakFast ImGuiKey_GamepadR1 +#define ImGuiKey_NavGamepadActivate (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceRight : ImGuiKey_GamepadFaceDown) +#define ImGuiKey_NavGamepadCancel (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceDown : ImGuiKey_GamepadFaceRight) +#define ImGuiKey_NavGamepadMenu ImGuiKey_GamepadFaceLeft +#define ImGuiKey_NavGamepadInput ImGuiKey_GamepadFaceUp + +enum ImGuiInputEventType +{ + ImGuiInputEventType_None = 0, + ImGuiInputEventType_MousePos, + ImGuiInputEventType_MouseWheel, + ImGuiInputEventType_MouseButton, + ImGuiInputEventType_Key, + ImGuiInputEventType_Text, + ImGuiInputEventType_Focus, + ImGuiInputEventType_COUNT +}; + +enum ImGuiInputSource +{ + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, // Note: may be Mouse or TouchScreen or Pen. See io.MouseSource to distinguish them. + ImGuiInputSource_Keyboard, + ImGuiInputSource_Gamepad, + ImGuiInputSource_COUNT +}; + +// FIXME: Structures in the union below need to be declared as anonymous unions appears to be an extension? +// Using ImVec2() would fail on Clang 'union member 'MousePos' has a non-trivial default constructor' +struct ImGuiInputEventMousePos { float PosX, PosY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseWheel { float WheelX, WheelY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseButton { int Button; bool Down; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventKey { ImGuiKey Key; bool Down; float AnalogValue; }; +struct ImGuiInputEventText { unsigned int Char; }; +struct ImGuiInputEventAppFocused { bool Focused; }; + +struct ImGuiInputEvent +{ + ImGuiInputEventType Type; + ImGuiInputSource Source; + ImU32 EventId; // Unique, sequential increasing integer to identify an event (if you need to correlate them to other data). + union + { + ImGuiInputEventMousePos MousePos; // if Type == ImGuiInputEventType_MousePos + ImGuiInputEventMouseWheel MouseWheel; // if Type == ImGuiInputEventType_MouseWheel + ImGuiInputEventMouseButton MouseButton; // if Type == ImGuiInputEventType_MouseButton + ImGuiInputEventKey Key; // if Type == ImGuiInputEventType_Key + ImGuiInputEventText Text; // if Type == ImGuiInputEventType_Text + ImGuiInputEventAppFocused AppFocused; // if Type == ImGuiInputEventType_Focus + }; + bool AddedByTestEngine; + + ImGuiInputEvent() { memset(this, 0, sizeof(*this)); } +}; + +// Input function taking an 'ImGuiID owner_id' argument defaults to (ImGuiKeyOwner_Any == 0) aka don't test ownership, which matches legacy behavior. +#define ImGuiKeyOwner_Any ((ImGuiID)0) // Accept key that have an owner, UNLESS a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. +#define ImGuiKeyOwner_NoOwner ((ImGuiID)-1) // Require key to have no owner. +//#define ImGuiKeyOwner_None ImGuiKeyOwner_NoOwner // We previously called this 'ImGuiKeyOwner_None' but it was inconsistent with our pattern that _None values == 0 and quite dangerous. Also using _NoOwner makes the IsKeyPressed() calls more explicit. + +typedef ImS16 ImGuiKeyRoutingIndex; + +// Routing table entry (sizeof() == 16 bytes) +struct ImGuiKeyRoutingData +{ + ImGuiKeyRoutingIndex NextEntryIndex; + ImU16 Mods; // Technically we'd only need 4-bits but for simplify we store ImGuiMod_ values which need 16-bits. + ImU8 RoutingCurrScore; // [DEBUG] For debug display + ImU8 RoutingNextScore; // Lower is better (0: perfect score) + ImGuiID RoutingCurr; + ImGuiID RoutingNext; + + ImGuiKeyRoutingData() { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_NoOwner; } +}; + +// Routing table: maintain a desired owner for each possible key-chord (key + mods), and setup owner in NewFrame() when mods are matching. +// Stored in main context (1 instance) +struct ImGuiKeyRoutingTable +{ + ImGuiKeyRoutingIndex Index[ImGuiKey_NamedKey_COUNT]; // Index of first entry in Entries[] + ImVector Entries; + ImVector EntriesNext; // Double-buffer to avoid reallocation (could use a shared buffer) + + ImGuiKeyRoutingTable() { Clear(); } + void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Index); n++) Index[n] = -1; Entries.clear(); EntriesNext.clear(); } +}; + +// This extends ImGuiKeyData but only for named keys (legacy keys don't support the new features) +// Stored in main context (1 per named key). In the future it might be merged into ImGuiKeyData. +struct ImGuiKeyOwnerData +{ + ImGuiID OwnerCurr; + ImGuiID OwnerNext; + bool LockThisFrame; // Reading this key requires explicit owner id (until end of frame). Set by ImGuiInputFlags_LockThisFrame. + bool LockUntilRelease; // Reading this key requires explicit owner id (until key is released). Set by ImGuiInputFlags_LockUntilRelease. When this is true LockThisFrame is always true as well. + + ImGuiKeyOwnerData() { OwnerCurr = OwnerNext = ImGuiKeyOwner_NoOwner; LockThisFrame = LockUntilRelease = false; } +}; + +// Extend ImGuiInputFlags_ +// Flags for extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlagsPrivate_ +{ + // Flags for IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(), Shortcut() + // - Repeat mode: Repeat rate selection + ImGuiInputFlags_RepeatRateDefault = 1 << 1, // Repeat rate: Regular (default) + ImGuiInputFlags_RepeatRateNavMove = 1 << 2, // Repeat rate: Fast + ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, // Repeat rate: Faster + // - Repeat mode: Specify when repeating key pressed can be interrupted. + // - In theory ImGuiInputFlags_RepeatUntilOtherKeyPress may be a desirable default, but it would break too many behavior so everything is opt-in. + ImGuiInputFlags_RepeatUntilRelease = 1 << 4, // Stop repeating when released (default for all functions except Shortcut). This only exists to allow overriding Shortcut() default behavior. + ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, // Stop repeating when released OR if keyboard mods are changed (default for Shortcut) + ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, // Stop repeating when released OR if keyboard mods are leaving the None state. Allows going from Mod+Key to Key by releasing Mod. + ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, // Stop repeating when released OR if any other keyboard key is pressed during the repeat + + // Flags for SetKeyOwner(), SetItemKeyOwner() + // - Locking key away from non-input aware code. Locking is useful to make input-owner-aware code steal keys from non-input-owner-aware code. If all code is input-owner-aware locking would never be necessary. + ImGuiInputFlags_LockThisFrame = 1 << 20, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared at end of frame. + ImGuiInputFlags_LockUntilRelease = 1 << 21, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared when the key is released or at end of each frame if key is released. + + // - Condition for SetItemKeyOwner() + ImGuiInputFlags_CondHovered = 1 << 22, // Only set if item is hovered (default to both) + ImGuiInputFlags_CondActive = 1 << 23, // Only set if item is active (default to both) + ImGuiInputFlags_CondDefault_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + + // [Internal] Mask of which function support which flags + ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | ImGuiInputFlags_RepeatRateNavMove | ImGuiInputFlags_RepeatRateNavTweak, + ImGuiInputFlags_RepeatUntilMask_ = ImGuiInputFlags_RepeatUntilRelease | ImGuiInputFlags_RepeatUntilKeyModsChange | ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | ImGuiInputFlags_RepeatUntilOtherKeyPress, + ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_, + ImGuiInputFlags_CondMask_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + ImGuiInputFlags_RouteTypeMask_ = ImGuiInputFlags_RouteActive | ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteAlways, + ImGuiInputFlags_RouteOptionsMask_ = ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused | ImGuiInputFlags_RouteFromRootWindow, + ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, + ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, + ImGuiInputFlags_SupportedByShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_, + ImGuiInputFlags_SupportedBySetNextItemShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_ | ImGuiInputFlags_Tooltip, + ImGuiInputFlags_SupportedBySetKeyOwner = ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, + ImGuiInputFlags_SupportedBySetItemKeyOwner = ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Clipper support +//----------------------------------------------------------------------------- + +// Note that Max is exclusive, so perhaps should be using a Begin/End convention. +struct ImGuiListClipperRange +{ + int Min; + int Max; + bool PosToIndexConvert; // Begin/End are absolute position (will be converted to indices later) + ImS8 PosToIndexOffsetMin; // Add to Min after converting to indices + ImS8 PosToIndexOffsetMax; // Add to Min after converting to indices + + static ImGuiListClipperRange FromIndices(int min, int max) { ImGuiListClipperRange r = { min, max, false, 0, 0 }; return r; } + static ImGuiListClipperRange FromPositions(float y1, float y2, int off_min, int off_max) { ImGuiListClipperRange r = { (int)y1, (int)y2, true, (ImS8)off_min, (ImS8)off_max }; return r; } +}; + +// Temporary clipper data, buffers shared/reused between instances +struct ImGuiListClipperData +{ + ImGuiListClipper* ListClipper; + float LossynessOffset; + int StepNo; + int ItemsFrozen; + ImVector Ranges; + + ImGuiListClipperData() { memset(this, 0, sizeof(*this)); } + void Reset(ImGuiListClipper* clipper) { ListClipper = clipper; StepNo = ItemsFrozen = 0; Ranges.resize(0); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Navigation support +//----------------------------------------------------------------------------- + +enum ImGuiActivateFlags_ +{ + ImGuiActivateFlags_None = 0, + ImGuiActivateFlags_PreferInput = 1 << 0, // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default for Enter key. + ImGuiActivateFlags_PreferTweak = 1 << 1, // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default for Space key and if keyboard is not used. + ImGuiActivateFlags_TryToPreserveState = 1 << 2, // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection) + ImGuiActivateFlags_FromTabbing = 1 << 3, // Activation requested by a tabbing request + ImGuiActivateFlags_FromShortcut = 1 << 4, // Activation requested by an item shortcut via SetNextItemShortcut() function. +}; + +// Early work-in-progress API for ScrollToItem() +enum ImGuiScrollFlags_ +{ + ImGuiScrollFlags_None = 0, + ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, // If item is not visible: scroll as little as possible on X axis to bring item back into view [default for X axis] + ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, // If item is not visible: scroll as little as possible on Y axis to bring item back into view [default for Y axis for windows that are already visible] + ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, // If item is not visible: scroll to make the item centered on X axis [rarely used] + ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, // If item is not visible: scroll to make the item centered on Y axis + ImGuiScrollFlags_AlwaysCenterX = 1 << 4, // Always center the result item on X axis [rarely used] + ImGuiScrollFlags_AlwaysCenterY = 1 << 5, // Always center the result item on Y axis [default for Y axis for appearing window) + ImGuiScrollFlags_NoScrollParent = 1 << 6, // Disable forwarding scrolling to parent window if required to keep item/rect visible (only scroll window the function was applied to). + ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleCenterX | ImGuiScrollFlags_AlwaysCenterX, + ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | ImGuiScrollFlags_KeepVisibleCenterY | ImGuiScrollFlags_AlwaysCenterY, +}; + +enum ImGuiNavRenderCursorFlags_ +{ + ImGuiNavRenderCursorFlags_None = 0, + ImGuiNavRenderCursorFlags_Compact = 1 << 1, // Compact highlight, no padding/distance from focused item + ImGuiNavRenderCursorFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) even when g.NavCursorVisible == false, aka even when using the mouse. + ImGuiNavRenderCursorFlags_NoRounding = 1 << 3, +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiNavHighlightFlags_None = ImGuiNavRenderCursorFlags_None, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_Compact = ImGuiNavRenderCursorFlags_Compact, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_AlwaysDraw = ImGuiNavRenderCursorFlags_AlwaysDraw, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_NoRounding = ImGuiNavRenderCursorFlags_NoRounding, // Renamed in 1.91.4 +#endif +}; + +enum ImGuiNavMoveFlags_ +{ + ImGuiNavMoveFlags_None = 0, + ImGuiNavMoveFlags_LoopX = 1 << 0, // On failed request, restart from opposite side + ImGuiNavMoveFlags_LoopY = 1 << 1, + ImGuiNavMoveFlags_WrapX = 1 << 2, // On failed request, request from opposite side one line down (when NavDir==right) or one line up (when NavDir==left) + ImGuiNavMoveFlags_WrapY = 1 << 3, // This is not super useful but provided for completeness + ImGuiNavMoveFlags_WrapMask_ = ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_WrapY, + ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, // Allow scoring and considering the current NavId as a move target candidate. This is used when the move source is offset (e.g. pressing PageDown actually needs to send a Up move request, if we are pressing PageDown from the bottom-most item we need to stay in place) + ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, // Store alternate result in NavMoveResultLocalVisible that only comprise elements that are already fully visible (used by PageUp/PageDown) + ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, // Force scrolling to min/max (used by Home/End) // FIXME-NAV: Aim to remove or reword, probably unnecessary + ImGuiNavMoveFlags_Forwarded = 1 << 7, + ImGuiNavMoveFlags_DebugNoResult = 1 << 8, // Dummy scoring for debug purpose, don't apply result + ImGuiNavMoveFlags_FocusApi = 1 << 9, // Requests from focus API can land/focus/activate items even if they are marked with _NoTabStop (see NavProcessItemForTabbingRequest() for details) + ImGuiNavMoveFlags_IsTabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight + ImGuiNavMoveFlags_IsPageMove = 1 << 11, // Identify a PageDown/PageUp request. + ImGuiNavMoveFlags_Activate = 1 << 12, // Activate/select target item. + ImGuiNavMoveFlags_NoSelect = 1 << 13, // Don't trigger selection by not setting g.NavJustMovedTo + ImGuiNavMoveFlags_NoSetNavCursorVisible = 1 << 14, // Do not alter the nav cursor visible state + ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, // (Experimental) Do not clear active id when applying move result +}; + +enum ImGuiNavLayer +{ + ImGuiNavLayer_Main = 0, // Main scrolling layer + ImGuiNavLayer_Menu = 1, // Menu layer (access with Alt) + ImGuiNavLayer_COUNT +}; + +// Storage for navigation query/results +struct ImGuiNavItemData +{ + ImGuiWindow* Window; // Init,Move // Best candidate window (result->ItemWindow->RootWindowForNav == request->Window) + ImGuiID ID; // Init,Move // Best candidate item ID + ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID + ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space + ImGuiItemFlags ItemFlags; // ????,Move // Best candidate item flags + float DistBox; // Move // Best candidate box distance to current NavId + float DistCenter; // Move // Best candidate center distance to current NavId + float DistAxial; // Move // Best candidate axial distance to current NavId + ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionUserData() value. Valid if (ItemFlags & ImGuiItemFlags_HasSelectionUserData) + + ImGuiNavItemData() { Clear(); } + void Clear() { Window = NULL; ID = FocusScopeId = 0; ItemFlags = 0; SelectionUserData = -1; DistBox = DistCenter = DistAxial = FLT_MAX; } +}; + +// Storage for PushFocusScope(), g.FocusScopeStack[], g.NavFocusRoute[] +struct ImGuiFocusScopeData +{ + ImGuiID ID; + ImGuiID WindowID; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Typing-select support +//----------------------------------------------------------------------------- + +// Flags for GetTypingSelectRequest() +enum ImGuiTypingSelectFlags_ +{ + ImGuiTypingSelectFlags_None = 0, + ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, // Backspace to delete character inputs. If using: ensure GetTypingSelectRequest() is not called more than once per frame (filter by e.g. focus state) + ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, // Allow "single char" search mode which is activated when pressing the same character multiple times. +}; + +// Returned by GetTypingSelectRequest(), designed to eventually be public. +struct IMGUI_API ImGuiTypingSelectRequest +{ + ImGuiTypingSelectFlags Flags; // Flags passed to GetTypingSelectRequest() + int SearchBufferLen; + const char* SearchBuffer; // Search buffer contents (use full string. unless SingleCharMode is set, in which case use SingleCharSize). + bool SelectRequest; // Set when buffer was modified this frame, requesting a selection. + bool SingleCharMode; // Notify when buffer contains same character repeated, to implement special mode. In this situation it preferred to not display any on-screen search indication. + ImS8 SingleCharSize; // Length in bytes of first letter codepoint (1 for ascii, 2-4 for UTF-8). If (SearchBufferLen==RepeatCharSize) only 1 letter has been input. +}; + +// Storage for GetTypingSelectRequest() +struct IMGUI_API ImGuiTypingSelectState +{ + ImGuiTypingSelectRequest Request; // User-facing data + char SearchBuffer[64]; // Search buffer: no need to make dynamic as this search is very transient. + ImGuiID FocusScope; + int LastRequestFrame = 0; + float LastRequestTime = 0.0f; + bool SingleCharModeLock = false; // After a certain single char repeat count we lock into SingleCharMode. Two benefits: 1) buffer never fill, 2) we can provide an immediate SingleChar mode without timer elapsing. + + ImGuiTypingSelectState() { memset(this, 0, sizeof(*this)); } + void Clear() { SearchBuffer[0] = 0; SingleCharModeLock = false; } // We preserve remaining data for easier debugging +}; + +//----------------------------------------------------------------------------- +// [SECTION] Columns support +//----------------------------------------------------------------------------- + +// Flags for internal's BeginColumns(). This is an obsolete API. Prefer using BeginTable() nowadays! +enum ImGuiOldColumnFlags_ +{ + ImGuiOldColumnFlags_None = 0, + ImGuiOldColumnFlags_NoBorder = 1 << 0, // Disable column dividers + ImGuiOldColumnFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers + ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns + ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window + ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, // Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, + //ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, + //ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, + //ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, + //ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, + //ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize, +#endif +}; + +struct ImGuiOldColumnData +{ + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiOldColumnFlags Flags; // Not exposed + ImRect ClipRect; + + ImGuiOldColumnData() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiOldColumns +{ + ImGuiID ID; + ImGuiOldColumnFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float OffMinX, OffMaxX; // Offsets from HostWorkRect.Min.x + float LineMinY, LineMaxY; + float HostCursorPosY; // Backup of CursorPos at the time of BeginColumns() + float HostCursorMaxPosX; // Backup of CursorMaxPos at the time of BeginColumns() + ImRect HostInitialClipRect; // Backup of ClipRect at the time of BeginColumns() + ImRect HostBackupClipRect; // Backup of ClipRect during PushColumnsBackground()/PopColumnsBackground() + ImRect HostBackupParentWorkRect;//Backup of WorkRect at the time of BeginColumns() + ImVector Columns; + ImDrawListSplitter Splitter; + + ImGuiOldColumns() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Box-select support +//----------------------------------------------------------------------------- + +struct ImGuiBoxSelectState +{ + // Active box-selection data (persistent, 1 active at a time) + ImGuiID ID; + bool IsActive; + bool IsStarting; + bool IsStartedFromVoid; // Starting click was not from an item. + bool IsStartedSetNavIdOnce; + bool RequestClear; + ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) + ImVec2 EndPosRel; // End position in window-contents relative space + ImVec2 ScrollAccum; // Scrolling accumulator (to behave at high-frame spaces) + ImGuiWindow* Window; + + // Temporary/Transient data + bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. + ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) + ImRect BoxSelectRectCurr; + + ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Multi-select support +//----------------------------------------------------------------------------- + +// We always assume that -1 is an invalid value (which works for indices and pointers) +#define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) + +// Temporary storage for multi-select +struct IMGUI_API ImGuiMultiSelectTempData +{ + ImGuiMultiSelectIO IO; // MUST BE FIRST FIELD. Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. + ImGuiMultiSelectState* Storage; + ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) + ImGuiMultiSelectFlags Flags; + ImVec2 ScopeRectMin; + ImVec2 BackupCursorMaxPos; + ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. + ImGuiID BoxSelectId; + ImGuiKeyChord KeyMods; + ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. + bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. + bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. + bool IsKeyboardSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + bool NavIdPassedBy; + bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. + bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. + + ImGuiMultiSelectTempData() { Clear(); } + void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. + void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = ImGuiSelectionUserData_Invalid; IO.NavIdSelected = IO.RangeSrcReset = false; } +}; + +// Persistent storage for multi-select (as long as selection is alive) +struct IMGUI_API ImGuiMultiSelectState +{ + ImGuiWindow* Window; + ImGuiID ID; + int LastFrameActive; // Last used frame-count, for GC. + int LastSelectionSize; // Set by BeginMultiSelect() based on optional info provided by user. May be -1 if unknown. + ImS8 RangeSelected; // -1 (don't have) or true/false + ImS8 NavIdSelected; // -1 (don't have) or true/false + ImGuiSelectionUserData RangeSrcItem; // + ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) + + ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = LastSelectionSize = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Docking support +//----------------------------------------------------------------------------- + +#ifdef IMGUI_HAS_DOCK +// +#endif // #ifdef IMGUI_HAS_DOCK + +//----------------------------------------------------------------------------- +// [SECTION] Viewport support +//----------------------------------------------------------------------------- + +// ImGuiViewport Private/Internals fields (cardinal sin: we are using inheritance!) +// Every instance of ImGuiViewport is in fact a ImGuiViewportP. +struct ImGuiViewportP : public ImGuiViewport +{ + int BgFgDrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used + ImDrawList* BgFgDrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. + ImDrawData DrawDataP; + ImDrawDataBuilder DrawDataBuilder; // Temporary data while building final ImDrawData + + // Per-viewport work area + // - Insets are >= 0.0f values, distance from viewport corners to work area. + // - BeginMainMenuBar() and DockspaceOverViewport() tend to use work area to avoid stepping over existing contents. + // - Generally 'safeAreaInsets' in iOS land, 'DisplayCutout' in Android land. + ImVec2 WorkInsetMin; // Work Area inset locked for the frame. GetWorkRect() always fits within GetMainRect(). + ImVec2 WorkInsetMax; // " + ImVec2 BuildWorkInsetMin; // Work Area inset accumulator for current frame, to become next frame's WorkInset + ImVec2 BuildWorkInsetMax; // " + + ImGuiViewportP() { BgFgDrawListsLastFrame[0] = BgFgDrawListsLastFrame[1] = -1; BgFgDrawLists[0] = BgFgDrawLists[1] = NULL; } + ~ImGuiViewportP() { if (BgFgDrawLists[0]) IM_DELETE(BgFgDrawLists[0]); if (BgFgDrawLists[1]) IM_DELETE(BgFgDrawLists[1]); } + + // Calculate work rect pos/size given a set of offset (we have 1 pair of offset for rect locked from last frame data, and 1 pair for currently building rect) + ImVec2 CalcWorkRectPos(const ImVec2& inset_min) const { return ImVec2(Pos.x + inset_min.x, Pos.y + inset_min.y); } + ImVec2 CalcWorkRectSize(const ImVec2& inset_min, const ImVec2& inset_max) const { return ImVec2(ImMax(0.0f, Size.x - inset_min.x - inset_max.x), ImMax(0.0f, Size.y - inset_min.y - inset_max.y)); } + void UpdateWorkRect() { WorkPos = CalcWorkRectPos(WorkInsetMin); WorkSize = CalcWorkRectSize(WorkInsetMin, WorkInsetMax); } // Update public fields + + // Helpers to retrieve ImRect (we don't need to store BuildWorkRect as every access tend to change it, hence the code asymmetry) + ImRect GetMainRect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + ImRect GetWorkRect() const { return ImRect(WorkPos.x, WorkPos.y, WorkPos.x + WorkSize.x, WorkPos.y + WorkSize.y); } + ImRect GetBuildWorkRect() const { ImVec2 pos = CalcWorkRectPos(BuildWorkInsetMin); ImVec2 size = CalcWorkRectSize(BuildWorkInsetMin, BuildWorkInsetMax); return ImRect(pos.x, pos.y, pos.x + size.x, pos.y + size.y); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Settings support +//----------------------------------------------------------------------------- + +// Windows data saved in imgui.ini file +// Because we never destroy or rename ImGuiWindowSettings, we can store the names in a separate buffer easily. +// (this is designed to be stored in a ImChunkStream buffer, with the variable-length Name following our structure) +struct ImGuiWindowSettings +{ + ImGuiID ID; + ImVec2ih Pos; + ImVec2ih Size; + bool Collapsed; + bool IsChild; + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + bool WantDelete; // Set to invalidate/delete the settings entry + + ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); } + char* GetName() { return (char*)(this + 1); } +}; + +struct ImGuiSettingsHandler +{ + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHashStr(TypeName) + void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Clear all settings data + void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called before reading (in registration order) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); // Read: Called when entering into a new ini entry e.g. "[Window][Name]" + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry + void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called after reading (in registration order) + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Localization support +//----------------------------------------------------------------------------- + +// This is experimental and not officially supported, it'll probably fall short of features, if/when it does we may backtrack. +enum ImGuiLocKey : int +{ + ImGuiLocKey_VersionStr, + ImGuiLocKey_TableSizeOne, + ImGuiLocKey_TableSizeAllFit, + ImGuiLocKey_TableSizeAllDefault, + ImGuiLocKey_TableResetOrder, + ImGuiLocKey_WindowingMainMenuBar, + ImGuiLocKey_WindowingPopup, + ImGuiLocKey_WindowingUntitled, + ImGuiLocKey_OpenLink_s, + ImGuiLocKey_CopyLink, + ImGuiLocKey_COUNT +}; + +struct ImGuiLocEntry +{ + ImGuiLocKey Key; + const char* Text; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Error handling, State recovery support +//----------------------------------------------------------------------------- + +// Macros used by Recoverable Error handling +// - Only dispatch error if _EXPR: evaluate as assert (similar to an assert macro). +// - The message will always be a string literal, in order to increase likelihood of being display by an assert handler. +// - See 'Demo->Configuration->Error Handling' and ImGuiIO definitions for details on error handling. +// - Read https://github.com/ocornut/imgui/wiki/Error-Handling for details on error handling. +#ifndef IM_ASSERT_USER_ERROR +#define IM_ASSERT_USER_ERROR(_EXPR,_MSG) do { if (!(_EXPR) && ImGui::ErrorLog(_MSG)) { IM_ASSERT((_EXPR) && _MSG); } } while (0) // Recoverable User Error +#endif + +// The error callback is currently not public, as it is expected that only advanced users will rely on it. +typedef void (*ImGuiErrorCallback)(ImGuiContext* ctx, void* user_data, const char* msg); // Function signature for g.ErrorCallback + +//----------------------------------------------------------------------------- +// [SECTION] Metrics, Debug Tools +//----------------------------------------------------------------------------- + +// See IMGUI_DEBUG_LOG() and IMGUI_DEBUG_LOG_XXX() macros. +enum ImGuiDebugLogFlags_ +{ + // Event types + ImGuiDebugLogFlags_None = 0, + ImGuiDebugLogFlags_EventError = 1 << 0, // Error submitted by IM_ASSERT_USER_ERROR() + ImGuiDebugLogFlags_EventActiveId = 1 << 1, + ImGuiDebugLogFlags_EventFocus = 1 << 2, + ImGuiDebugLogFlags_EventPopup = 1 << 3, + ImGuiDebugLogFlags_EventNav = 1 << 4, + ImGuiDebugLogFlags_EventClipper = 1 << 5, + ImGuiDebugLogFlags_EventSelection = 1 << 6, + ImGuiDebugLogFlags_EventIO = 1 << 7, + ImGuiDebugLogFlags_EventFont = 1 << 8, + ImGuiDebugLogFlags_EventInputRouting = 1 << 9, + ImGuiDebugLogFlags_EventDocking = 1 << 10, // Unused in this branch + ImGuiDebugLogFlags_EventViewport = 1 << 11, // Unused in this branch + + ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventFont | ImGuiDebugLogFlags_EventInputRouting | ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, + ImGuiDebugLogFlags_OutputToTTY = 1 << 20, // Also send output to TTY + ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, // Also send output to Test Engine +}; + +struct ImGuiDebugAllocEntry +{ + int FrameCount; + ImS16 AllocCount; + ImS16 FreeCount; +}; + +struct ImGuiDebugAllocInfo +{ + int TotalAllocCount; // Number of call to MemAlloc(). + int TotalFreeCount; + ImS16 LastEntriesIdx; // Current index in buffer + ImGuiDebugAllocEntry LastEntriesBuf[6]; // Track last 6 frames that had allocations + + ImGuiDebugAllocInfo() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiMetricsConfig +{ + bool ShowDebugLog = false; + bool ShowIDStackTool = false; + bool ShowWindowsRects = false; + bool ShowWindowsBeginOrder = false; + bool ShowTablesRects = false; + bool ShowDrawCmdMesh = true; + bool ShowDrawCmdBoundingBoxes = true; + bool ShowTextEncodingViewer = false; + int ShowWindowsRectsType = -1; + int ShowTablesRectsType = -1; + int HighlightMonitorIdx = -1; + ImGuiID HighlightViewportID = 0; +}; + +struct ImGuiStackLevelInfo +{ + ImGuiID ID; + ImS8 QueryFrameCount; // >= 1: Query in progress + bool QuerySuccess; // Obtained result from DebugHookIdInfo() + ImGuiDataType DataType : 8; + char Desc[57]; // Arbitrarily sized buffer to hold a result (FIXME: could replace Results[] with a chunk stream?) FIXME: Now that we added CTRL+C this should be fixed. + + ImGuiStackLevelInfo() { memset(this, 0, sizeof(*this)); } +}; + +// State for ID Stack tool queries +struct ImGuiIDStackTool +{ + int LastActiveFrame; + int StackLevel; // -1: query stack and resize Results, >= 0: individual stack level + ImGuiID QueryId; // ID to query details for + ImVector Results; + bool CopyToClipboardOnCtrlC; + float CopyToClipboardLastTime; + ImGuiTextBuffer ResultPathBuf; + + ImGuiIDStackTool() { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Generic context hooks +//----------------------------------------------------------------------------- + +typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, ImGuiContextHook* hook); +enum ImGuiContextHookType { ImGuiContextHookType_NewFramePre, ImGuiContextHookType_NewFramePost, ImGuiContextHookType_EndFramePre, ImGuiContextHookType_EndFramePost, ImGuiContextHookType_RenderPre, ImGuiContextHookType_RenderPost, ImGuiContextHookType_Shutdown, ImGuiContextHookType_PendingRemoval_ }; + +struct ImGuiContextHook +{ + ImGuiID HookId; // A unique ID assigned by AddContextHook() + ImGuiContextHookType Type; + ImGuiID Owner; + ImGuiContextHookCallback Callback; + void* UserData; + + ImGuiContextHook() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiContext (main Dear ImGui context) +//----------------------------------------------------------------------------- + +struct ImGuiContext +{ + bool Initialized; + bool FontAtlasOwnedByContext; // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it. + ImGuiIO IO; + ImGuiPlatformIO PlatformIO; + ImGuiStyle Style; + ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() + float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. + float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. + float FontScale; // == FontSize / Font->FontSize + float CurrentDpiScale; // Current window/viewport DpiScale + ImDrawListSharedData DrawListSharedData; + double Time; + int FrameCount; + int FrameCountEnded; + int FrameCountRendered; + ImGuiID WithinEndChildID; // Set within EndChild() + bool WithinFrameScope; // Set by NewFrame(), cleared by EndFrame() + bool WithinFrameScopeWithImplicitWindow; // Set by NewFrame(), cleared by EndFrame() when the implicit debug window has been pushed + bool GcCompactAll; // Request full GC + bool TestEngineHookItems; // Will call test engine hooks: ImGuiTestEngineHook_ItemAdd(), ImGuiTestEngineHook_ItemInfo(), ImGuiTestEngineHook_Log() + void* TestEngine; // Test engine user data + char ContextName[16]; // Storage for a context name (to facilitate debugging multi-context setups) + + // Inputs + ImVector InputEventsQueue; // Input events which will be trickled/written into IO structure. + ImVector InputEventsTrail; // Past input events processed in NewFrame(). This is to allow domain-specific application to access e.g mouse/pen trail. + ImGuiMouseSource InputEventsNextMouseSource; + ImU32 InputEventsNextEventId; + + // Windows state + ImVector Windows; // Windows, sorted in display order, back to front + ImVector WindowsFocusOrder; // Root windows, sorted in focus order, back to front. + ImVector WindowsTempSortBuffer; // Temporary buffer used in EndFrame() to reorder windows so parents are kept before their child + ImVector CurrentWindowStack; + ImGuiStorage WindowsById; // Map window's ImGuiID to ImGuiWindow* + int WindowsActiveCount; // Number of unique windows submitted by frame + float WindowsBorderHoverPadding; // Padding around resizable windows for which hovering on counts as hovering the window == ImMax(style.TouchExtraPadding, style.WindowBorderHoverPadding). This isn't so multi-dpi friendly. + ImGuiID DebugBreakInWindow; // Set to break in Begin() call. + ImGuiWindow* CurrentWindow; // Window being drawn into + ImGuiWindow* HoveredWindow; // Window the mouse is hovering. Will typically catch mouse inputs. + ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. + ImGuiWindow* HoveredWindowBeforeClear; // Window the mouse is hovering. Filled even with _NoMouse. This is currently useful for multi-context compositors. + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindow. + ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. + ImVec2 WheelingWindowRefMousePos; + int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL + int WheelingWindowScrolledFrame; + float WheelingWindowReleaseTimer; + ImVec2 WheelingWindowWheelRemainder; + ImVec2 WheelingAxisAvg; + + // Item/widgets state and tracking information + ImGuiID DebugDrawIdConflicts; // Set when we detect multiple items with the same identifier + ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] + ImGuiID HoveredId; // Hovered widget, filled during the frame + ImGuiID HoveredIdPreviousFrame; + int HoveredIdPreviousFrameItemCount; // Count numbers of items using the same ID as last frame's hovered id + float HoveredIdTimer; // Measure contiguous hovering time + float HoveredIdNotActiveTimer; // Measure contiguous hovering time where the item has not been active + bool HoveredIdAllowOverlap; + bool HoveredIdIsDisabled; // At least one widget passed the rect test, but has been discarded by disabled flag or popup inhibit. May be true even if HoveredId == 0. + bool ItemUnclipByLog; // Disable ItemAdd() clipping, essentially a memory-locality friendly copy of LogEnabled + ImGuiID ActiveId; // Active widget + ImGuiID ActiveIdIsAlive; // Active widget has been seen this frame (we can't use a bool as the ActiveId may change within the frame) + float ActiveIdTimer; + bool ActiveIdIsJustActivated; // Set at the time of activation for one frame + bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + bool ActiveIdNoClearOnFocusLoss; // Disable losing active id if the active id window gets unfocused. + bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. + bool ActiveIdHasBeenEditedBefore; // Was the value associated to the widget Edited over the course of the Active state. + bool ActiveIdHasBeenEditedThisFrame; + bool ActiveIdFromShortcut; + int ActiveIdMouseButton : 8; + ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) + ImGuiWindow* ActiveIdWindow; + ImGuiInputSource ActiveIdSource; // Activating source: ImGuiInputSource_Mouse OR ImGuiInputSource_Keyboard OR ImGuiInputSource_Gamepad + ImGuiID ActiveIdPreviousFrame; + ImGuiDeactivatedItemData DeactivatedItemData; + ImGuiDataTypeStorage ActiveIdValueOnActivation; // Backup of initial value at the time of activation. ONLY SET BY SPECIFIC WIDGETS: DragXXX and SliderXXX. + ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. + float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + + // Key/Input Ownership + Shortcut Routing system + // - The idea is that instead of "eating" a given key, we can link to an owner. + // - Input query can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Routing is requested ahead of time for a given chord (Key + Mods) and granted in NewFrame(). + double LastKeyModsChangeTime; // Record the last time key mods changed (affect repeat delay when using shortcut logic) + double LastKeyModsChangeFromNoneTime; // Record the last time key mods changed away from being 0 (affect repeat delay when using shortcut logic) + double LastKeyboardKeyPressTime; // Record the last time a keyboard key (ignore mouse/gamepad ones) was pressed. + ImBitArrayForNamedKeys KeysMayBeCharInput; // Lookup to tell if a key can emit char input, see IsKeyChordPotentiallyCharInput(). sizeof() = 20 bytes + ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; + ImGuiKeyRoutingTable KeysRoutingTable; + ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) + bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (this is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations) + ImGuiKeyChord DebugBreakInShortcutRouting; // Set to break in SetShortcutRouting()/Shortcut() calls. + //ImU32 ActiveIdUsingNavInputMask; // [OBSOLETE] Since (IMGUI_VERSION_NUM >= 18804) : 'g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);' becomes --> 'SetKeyOwner(ImGuiKey_Escape, g.ActiveId) and/or SetKeyOwner(ImGuiKey_NavGamepadCancel, g.ActiveId);' + + // Next window/item data + ImGuiID CurrentFocusScopeId; // Value for currently appending items == g.FocusScopeStack.back(). Not to be mistaken with g.NavFocusScopeId. + ImGuiItemFlags CurrentItemFlags; // Value for currently appending items == g.ItemFlagsStack.back() + ImGuiID DebugLocateId; // Storage for DebugLocateItemOnHover() feature: this is read by ItemAdd() so we keep it in a hot/cached location + ImGuiNextItemData NextItemData; // Storage for SetNextItem** functions + ImGuiLastItemData LastItemData; // Storage for last submitted item (setup by ItemAdd) + ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + bool DebugShowGroupRects; + + // Shared stacks + ImGuiCol DebugFlashStyleColorIdx; // (Keep close to ColorStack to share cache line) + ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() + ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() + ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() + ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin() + ImVector ItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() + ImVector GroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() + ImVector OpenPopupStack; // Which popups are open (persistent) + ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImVectorTreeNodeStack; // Stack for TreeNode() + + // Viewports + ImVector Viewports; // Active viewports (Size==1 in 'master' branch). Each viewports hold their copy of ImDrawData. + + // Keyboard/Gamepad Navigation + bool NavCursorVisible; // Nav focus cursor/rectangle is visible? We hide it after a mouse click. We show it after a nav move. + bool NavHighlightItemUnderNav; // Disable mouse hovering highlight. Highlight navigation focused item instead of mouse hovered item. + //bool NavDisableHighlight; // Old name for !g.NavCursorVisible before 1.91.4 (2024/10/18). OPPOSITE VALUE (g.NavDisableHighlight == !g.NavCursorVisible) + //bool NavDisableMouseHover; // Old name for g.NavHighlightItemUnderNav before 1.91.1 (2024/10/18) this was called When user starts using keyboard/gamepad, we hide mouse hovering highlight until mouse is touched again. + bool NavMousePosDirty; // When set we will update mouse position if io.ConfigNavMoveSetMousePos is set (not enabled by default) + bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRectRel is valid + ImGuiID NavId; // Focused item for navigation + ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusedWindow' + ImGuiID NavFocusScopeId; // Focused focus scope (e.g. selection code often wants to "clear other items" when landing on an item of the same scope) + ImGuiNavLayer NavLayer; // Focused layer (main scrolling layer, or menu/title bar layer) + ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItem() + ImGuiID NavActivateDownId; // ~~ IsKeyDown(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyDown(ImGuiKey_NavGamepadActivate) ? NavId : 0 + ImGuiID NavActivatePressedId; // ~~ IsKeyPressed(ImGuiKey_Space) || IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate) ? NavId : 0 (no repeat) + ImGuiActivateFlags NavActivateFlags; + ImVector NavFocusRoute; // Reversed copy focus scope stack for NavId (should contains NavFocusScopeId). This essentially follow the window->ParentWindowForFocusRoute chain. + ImGuiID NavHighlightActivatedId; + float NavHighlightActivatedTimer; + ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame. + ImGuiActivateFlags NavNextActivateFlags; + ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Mouse + ImGuiSelectionUserData NavLastValidSelectionUserData; // Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data. + ImS8 NavCursorHideFrames; + + // Navigation: Init & Move Requests + bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest this is to perform early out in ItemAdd() + bool NavInitRequest; // Init request for appearing window to select first item + bool NavInitRequestFromMove; + ImGuiNavItemData NavInitResult; // Init request result (first item of the window, or one for which SetItemDefaultFocus() was called) + bool NavMoveSubmitted; // Move request submitted, will process result on next NewFrame() + bool NavMoveScoringItems; // Move request submitted, still scoring incoming items + bool NavMoveForwardToNextFrame; + ImGuiNavMoveFlags NavMoveFlags; + ImGuiScrollFlags NavMoveScrollFlags; + ImGuiKeyChord NavMoveKeyMods; + ImGuiDir NavMoveDir; // Direction of the move request (left/right/up/down) + ImGuiDir NavMoveDirForDebug; + ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? + ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring. + ImRect NavScoringNoClipRect; // Some nav operations (such as PageUp/PageDown) enforce a region which clipper will attempt to always keep submitted + int NavScoringDebugCount; // Metrics for debugging + int NavTabbingDir; // Generally -1 or +1, 0 when tabbing without a nav id + int NavTabbingCounter; // >0 when counting items for tabbing + ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow + ImGuiNavItemData NavMoveResultLocalVisible; // Best move request candidate within NavWindow that are mostly visible (when using ImGuiNavMoveFlags_AlsoScoreVisibleSet flag) + ImGuiNavItemData NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using ImGuiWindowFlags_NavFlattened flag) + ImGuiNavItemData NavTabbingResultFirst; // First tabbing request candidate within NavWindow and flattened hierarchy + + // Navigation: record of last move request + ImGuiID NavJustMovedFromFocusScopeId; // Just navigated from this focus scope id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToFocusScopeId; // Just navigated to this focus scope id (result of a successfully MoveRequest). + ImGuiKeyChord NavJustMovedToKeyMods; + bool NavJustMovedToIsTabbing; // Copy of ImGuiNavMoveFlags_IsTabbing. Maybe we should store whole flags. + bool NavJustMovedToHasSelectionData; // Copy of move result's ItemFlags & ImGuiItemFlags_HasSelectionUserData). Maybe we should just store ImGuiNavItemData. + + // Navigation: Windowing (CTRL+TAB for list, or Menu button + keys or directional pads to move/resize) + ImGuiKeyChord ConfigNavWindowingKeyNext; // = ImGuiMod_Ctrl | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiKey_Tab on OS X). For reconfiguration (see #4828) + ImGuiKeyChord ConfigNavWindowingKeyPrev; // = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab on OS X) + ImGuiWindow* NavWindowingTarget; // Target window when doing CTRL+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most! + ImGuiWindow* NavWindowingTargetAnim; // Record of last valid NavWindowingTarget until DimBgRatio and NavWindowingHighlightAlpha becomes 0.0f, so the fade-out can stay on it. + ImGuiWindow* NavWindowingListWindow; // Internal window actually listing the CTRL+Tab contents + float NavWindowingTimer; + float NavWindowingHighlightAlpha; + bool NavWindowingToggleLayer; + ImGuiKey NavWindowingToggleKey; + ImVec2 NavWindowingAccumDeltaPos; + ImVec2 NavWindowingAccumDeltaSize; + + // Render + float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) + + // Drag and Drop + bool DragDropActive; + bool DragDropWithinSource; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag source. + bool DragDropWithinTarget; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag target. + ImGuiDragDropFlags DragDropSourceFlags; + int DragDropSourceFrameCount; + int DragDropMouseButton; + ImGuiPayload DragDropPayload; + ImRect DragDropTargetRect; // Store rectangle of current target candidate (we favor small targets when overlapping) + ImRect DragDropTargetClipRect; // Store ClipRect at the time of item's drawing + ImGuiID DragDropTargetId; + ImGuiDragDropFlags DragDropAcceptFlags; + float DragDropAcceptIdCurrRectSurface; // Target item surface (we resolve overlapping targets by prioritizing the smaller surface) + ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) + ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) + int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source + ImGuiID DragDropHoldJustPressedId; // Set when holding a payload just made ButtonBehavior() return a press. + ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size + unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads + + // Clipper + int ClipperTempDataStacked; + ImVector ClipperTempData; + + // Tables + ImGuiTable* CurrentTable; + ImGuiID DebugBreakInTable; // Set to break in BeginTable() call. + int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) + ImVector TablesTempData; // Temporary table data (buffers reused/shared across instances, support nesting) + ImPool Tables; // Persistent table data + ImVector TablesLastTimeActive; // Last used timestamp of each tables (SOA, for efficient GC) + ImVector DrawChannelsTempMergeBuffer; + + // Tab bars + ImGuiTabBar* CurrentTabBar; + ImPool TabBars; + ImVector CurrentTabBarStack; + ImVector ShrinkWidthBuffer; + + // Multi-Select state + ImGuiBoxSelectState BoxSelectState; + ImGuiMultiSelectTempData* CurrentMultiSelect; + int MultiSelectTempDataStacked; // Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size) + ImVector MultiSelectTempData; + ImPool MultiSelectStorage; + + // Hover Delay system + ImGuiID HoverItemDelayId; + ImGuiID HoverItemDelayIdPreviousFrame; + float HoverItemDelayTimer; // Currently used by IsItemHovered() + float HoverItemDelayClearTimer; // Currently used by IsItemHovered(): grace time before g.TooltipHoverTimer gets cleared. + ImGuiID HoverItemUnlockedStationaryId; // Mouse has once been stationary on this item. Only reset after departing the item. + ImGuiID HoverWindowUnlockedStationaryId; // Mouse has once been stationary on this window. Only reset after departing the window. + + // Mouse state + ImGuiMouseCursor MouseCursor; + float MouseStationaryTimer; // Time the mouse has been stationary (with some loose heuristic) + ImVec2 MouseLastValidPos; + + // Widget state + ImGuiInputTextState InputTextState; + ImGuiInputTextDeactivatedState InputTextDeactivatedState; + ImFont InputTextPasswordFont; + ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiDataTypeStorage DataTypeZeroValue; // 0 for all data types + int BeginMenuDepth; + int BeginComboDepth; + ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets + ImGuiID ColorEditCurrentID; // Set temporarily while inside of the parent-most ColorEdit4/ColorPicker4 (because they call each others). + ImGuiID ColorEditSavedID; // ID we are saving/restoring HS for + float ColorEditSavedHue; // Backup of last Hue associated to LastColor, so we can restore Hue in lossy RGB<>HSV round trips + float ColorEditSavedSat; // Backup of last Saturation associated to LastColor, so we can restore Saturation in lossy RGB<>HSV round trips + ImU32 ColorEditSavedColor; // RGB value with alpha set to 0. + ImVec4 ColorPickerRef; // Initial/reference color at the time of opening the color picker. + ImGuiComboPreviewData ComboPreviewData; + ImRect WindowResizeBorderExpectedRect; // Expected border rect, switch to relative edit if moving + bool WindowResizeRelativeMode; + short ScrollbarSeekMode; // 0: scroll to clicked location, -1/+1: prev/next page. + float ScrollbarClickDeltaToGrabCenter; // When scrolling to mouse location: distance between mouse and center of grab box, normalized in parent space. + float SliderGrabClickOffset; + float SliderCurrentAccum; // Accumulated slider delta when using navigation controls. + bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it? + bool DragCurrentAccumDirty; + float DragCurrentAccum; // Accumulator for dragging modification. Always high-precision, not rounded by end-user precision settings + float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio + float DisabledAlphaBackup; // Backup for style.Alpha for BeginDisabled() + short DisabledStackSize; + short TooltipOverrideCount; + ImGuiWindow* TooltipPreviousWindow; // Window of last tooltip submitted during the frame + ImVector ClipboardHandlerData; // If no custom clipboard handler is defined + ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once + ImGuiTypingSelectState TypingSelectState; // State for GetTypingSelectRequest() + + // Platform support + ImGuiPlatformImeData PlatformImeData; // Data updated by current frame + ImGuiPlatformImeData PlatformImeDataPrev; // Previous frame data. When changed we call the platform_io.Platform_SetImeDataFn() handler. + + // Settings + bool SettingsLoaded; + float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero + ImGuiTextBuffer SettingsIniData; // In memory .ini settings + ImVector SettingsHandlers; // List of .ini settings handlers + ImChunkStream SettingsWindows; // ImGuiWindow .ini settings entries + ImChunkStream SettingsTables; // ImGuiTable .ini settings entries + ImVector Hooks; // Hooks for extensions (e.g. test engine) + ImGuiID HookIdNext; // Next available HookId + + // Localization + const char* LocalizationTable[ImGuiLocKey_COUNT]; + + // Capture/Logging + bool LogEnabled; // Currently capturing + ImGuiLogFlags LogFlags; // Capture flags/type + ImGuiWindow* LogWindow; + ImFileHandle LogFile; // If != NULL log to stdout/ file + ImGuiTextBuffer LogBuffer; // Accumulation buffer when log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. + const char* LogNextPrefix; + const char* LogNextSuffix; + float LogLinePosY; + bool LogLineFirstItem; + int LogDepthRef; + int LogDepthToExpand; + int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. + + // Error Handling + ImGuiErrorCallback ErrorCallback; // = NULL. May be exposed in public API eventually. + void* ErrorCallbackUserData; // = NULL + ImVec2 ErrorTooltipLockedPos; + bool ErrorFirst; + int ErrorCountCurrentFrame; // [Internal] Number of errors submitted this frame. + ImGuiErrorRecoveryState StackSizesInNewFrame; // [Internal] + ImGuiErrorRecoveryState*StackSizesInBeginForCurrentWindow; // [Internal] + + // Debug Tools + // (some of the highly frequently used data are interleaved in other structures above: DebugBreakXXX fields, DebugHookIdInfo, DebugLocateId etc.) + int DebugDrawIdConflictsCount; // Locked count (preserved when holding CTRL) + ImGuiDebugLogFlags DebugLogFlags; + ImGuiTextBuffer DebugLogBuf; + ImGuiTextIndex DebugLogIndex; + int DebugLogSkippedErrors; + ImGuiDebugLogFlags DebugLogAutoDisableFlags; + ImU8 DebugLogAutoDisableFrames; + ImU8 DebugLocateFrames; // For DebugLocateItemOnHover(). This is used together with DebugLocateId which is in a hot/cached spot above. + bool DebugBreakInLocateId; // Debug break in ItemAdd() call for g.DebugLocateId. + ImGuiKeyChord DebugBreakKeyChord; // = ImGuiKey_Pause + ImS8 DebugBeginReturnValueCullDepth; // Cycle between 0..9 then wrap around. + bool DebugItemPickerActive; // Item picker is active (started with DebugStartItemPicker()) + ImU8 DebugItemPickerMouseButton; + ImGuiID DebugItemPickerBreakId; // Will call IM_DEBUG_BREAK() when encountering this ID + float DebugFlashStyleColorTime; + ImVec4 DebugFlashStyleColorBackup; + ImGuiMetricsConfig DebugMetricsConfig; + ImGuiIDStackTool DebugIDStackTool; + ImGuiDebugAllocInfo DebugAllocInfo; + + // Misc + float FramerateSecPerFrame[60]; // Calculate estimate of framerate for user over the last 60 frames.. + int FramerateSecPerFrameIdx; + int FramerateSecPerFrameCount; + float FramerateSecPerFrameAccum; + int WantCaptureMouseNextFrame; // Explicit capture override via SetNextFrameWantCaptureMouse()/SetNextFrameWantCaptureKeyboard(). Default to -1. + int WantCaptureKeyboardNextFrame; // " + int WantTextInputNextFrame; + ImVector TempBuffer; // Temporary text buffer + char TempKeychordName[64]; + + ImGuiContext(ImFontAtlas* shared_font_atlas); +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiWindowTempData, ImGuiWindow +//----------------------------------------------------------------------------- + +// Transient per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the DC variable name in ImGuiWindow. +// (That's theory, in practice the delimitation between ImGuiWindow and ImGuiWindowTempData is quite tenuous and could be reconsidered..) +// (This doesn't need a constructor because we zero-clear it as part of ImGuiWindow and all frame-temporary data are setup on Begin) +struct IMGUI_API ImGuiWindowTempData +{ + // Layout + ImVec2 CursorPos; // Current emitting position, in absolute coordinates. + ImVec2 CursorPosPrevLine; + ImVec2 CursorStartPos; // Initial position after Begin(), generally ~ window position + WindowPadding. + ImVec2 CursorMaxPos; // Used to implicitly calculate ContentSize at the beginning of next frame, for scrolling range and auto-resize. Always growing during the frame. + ImVec2 IdealMaxPos; // Used to implicitly calculate ContentSizeIdeal at the beginning of next frame, for auto-resize only. Always growing during the frame. + ImVec2 CurrLineSize; + ImVec2 PrevLineSize; + float CurrLineTextBaseOffset; // Baseline offset (0.0f by default on a new line, generally == style.FramePadding.y when a framed item has been added). + float PrevLineTextBaseOffset; + bool IsSameLine; + bool IsSetPos; + ImVec1 Indent; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) + ImVec1 ColumnsOffset; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. + ImVec1 GroupOffset; + ImVec2 CursorStartPosLossyness;// Record the loss of precision of CursorStartPos due to really large scrolling amount. This is used by clipper to compensate and fix the most common use case of large scroll area. + + // Keyboard/Gamepad navigation + ImGuiNavLayer NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) + short NavLayersActiveMask; // Which layers have been written to (result from previous frame) + short NavLayersActiveMaskNext;// Which layers have been written to (accumulator for current frame) + bool NavIsScrollPushableX; // Set when current work location may be scrolled horizontally when moving left / right. This is generally always true UNLESS within a column. + bool NavHideHighlightOneFrame; + bool NavWindowHasScrollY; // Set per window when scrolling can be used (== ScrollMax.y > 0.0f) + + // Miscellaneous + bool MenuBarAppending; // FIXME: Remove this + ImVec2 MenuBarOffset; // MenuBarOffset.x is sort of equivalent of a per-layer CursorPos.x, saved/restored as we switch to the menu bar. The only situation when MenuBarOffset.y is > 0 if when (SafeAreaPadding.y > FramePadding.y), often used on TVs. + ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items measurement + int TreeDepth; // Current tree depth. + ImU32 TreeHasStackDataDepthMask; // Store whether given depth has ImGuiTreeNodeStackData data. Could be turned into a ImU64 if necessary. + ImVector ChildWindows; + ImGuiStorage* StateStorage; // Current persistent per-window storage (store e.g. tree node open/close state) + ImGuiOldColumns* CurrentColumns; // Current columns set + int CurrentTableIdx; // Current table index (into g.Tables) + ImGuiLayoutType LayoutType; + ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + ImU32 ModalDimBgColor; + ImGuiItemStatusFlags WindowItemStatusFlags; + ImGuiItemStatusFlags ChildItemStatusFlags; + + // Local parameters stacks + // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. + float ItemWidth; // Current item width (>0.0: width in pixels, <0.0: align xx pixels to the right of window). + float TextWrapPos; // Current text wrap pos. + ImVector ItemWidthStack; // Store item widths to restore (attention: .back() is not == ItemWidth) + ImVector TextWrapPosStack; // Store text wrap pos to restore (attention: .back() is not == TextWrapPos) +}; + +// Storage for one window +struct IMGUI_API ImGuiWindow +{ + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + char* Name; // Window name, owned by the window. + ImGuiID ID; // == ImHashStr(Name) + ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImGuiChildFlags ChildFlags; // Set when window is a child window. See enum ImGuiChildFlags_ + ImGuiViewportP* Viewport; // Always set in Begin(). Inactive windows may have a NULL value here if their viewport was discarded. + ImVec2 Pos; // Position (always rounded-up to nearest pixel) + ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) + ImVec2 SizeFull; // Size when non collapsed + ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding. + ImVec2 ContentSizeIdeal; + ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize(). + ImVec2 WindowPadding; // Window padding at the time of Begin(). + float WindowRounding; // Window rounding at the time of Begin(). May be clamped lower to avoid rendering artifacts with title bar, menu bar etc. + float WindowBorderSize; // Window border size at the time of Begin(). + float TitleBarHeight, MenuBarHeight; // Note that those used to be function before 2024/05/28. If you have old code calling TitleBarHeight() you can change it to TitleBarHeight. + float DecoOuterSizeX1, DecoOuterSizeY1; // Left/Up offsets. Sum of non-scrolling outer decorations (X1 generally == 0.0f. Y1 generally = TitleBarHeight + MenuBarHeight). Locked during Begin(). + float DecoOuterSizeX2, DecoOuterSizeY2; // Right/Down offsets (X2 generally == ScrollbarSize.x, Y2 == ScrollbarSizes.y). + float DecoInnerSizeX1, DecoInnerSizeY1; // Applied AFTER/OVER InnerRect. Specialized for Tables as they use specialized form of clipping and frozen rows/columns are inside InnerRect (and not part of regular decoration sizes). + int NameBufLen; // Size of buffer storing Name. May be larger than strlen(Name)! + ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID ChildId; // ID of corresponding item in parent window (for navigation to return from child window to parent window) + ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) + ImVec2 Scroll; + ImVec2 ScrollMax; + ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) + ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered + ImVec2 ScrollTargetEdgeSnapDist; // 0.0f = no snapping, >0.0f snapping threshold + ImVec2 ScrollbarSizes; // Size taken by each scrollbars on their smaller axis. Pay attention! ScrollbarSizes.x == width of the vertical scrollbar, ScrollbarSizes.y = height of the horizontal scrollbar. + bool ScrollbarX, ScrollbarY; // Are scrollbars visible? + bool ScrollbarXStabilizeEnabled; // Was ScrollbarX previously auto-stabilized? + ImU8 ScrollbarXStabilizeToggledHistory; // Used to stabilize scrollbar visibility in case of feedback loops + bool Active; // Set to true on Begin(), unless Collapsed + bool WasActive; + bool WriteAccessed; // Set to true when any widget access the current window + bool Collapsed; // Set when collapsing window to become only title-bar + bool WantCollapseToggle; + bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool SkipRefresh; // [EXPERIMENTAL] Reuse previous frame drawn contents, Begin() returns false. + bool Appearing; // Set during the frame where the window is appearing (or re-appearing) + bool Hidden; // Do not display (== HiddenFrames*** > 0) + bool IsFallbackWindow; // Set on the "Debug##Default" window. + bool IsExplicitChild; // Set when passed _ChildWindow, left to false by BeginDocked() + bool HasCloseButton; // Set when the window has a close button (p_open != NULL) + signed char ResizeBorderHovered; // Current border being hovered for resize (-1: none, otherwise 0-3) + signed char ResizeBorderHeld; // Current border being held for resize (-1: none, otherwise 0-3) + short BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) + short BeginCountPreviousFrame; // Number of Begin() during the previous frame + short BeginOrderWithinParent; // Begin() order within immediate parent window, if we are a child window. Otherwise 0. + short BeginOrderWithinContext; // Begin() order within entire imgui context. This is mostly used for debugging submission order related issues. + short FocusOrder; // Order within WindowsFocusOrder[], altered when windows are focused. + ImS8 AutoFitFramesX, AutoFitFramesY; + bool AutoFitOnlyGrows; + ImGuiDir AutoPosLastDirection; + ImS8 HiddenFramesCanSkipItems; // Hide the window for N frames + ImS8 HiddenFramesCannotSkipItems; // Hide the window for N frames while allowing items to be submitted so we can measure their size + ImS8 HiddenFramesForRenderOnly; // Hide the window until frame N at Render() time only + ImS8 DisableInputsFrames; // Disable window interactions for N frames + ImGuiCond SetWindowPosAllowFlags : 8; // store acceptable condition flags for SetNextWindowPos() use. + ImGuiCond SetWindowSizeAllowFlags : 8; // store acceptable condition flags for SetNextWindowSize() use. + ImGuiCond SetWindowCollapsedAllowFlags : 8; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) + ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0, 0) when positioning from top-left corner; ImVec2(0.5f, 0.5f) for centering; ImVec2(1, 1) for bottom right. + + ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack. (In theory this should be in the TempData structure) + ImGuiWindowTempData DC; // Temporary per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the "DC" variable name. + + // The best way to understand what those rectangles are is to use the 'Metrics->Tools->Show Windows Rectangles' viewer. + // The main 'OuterRect', omitted as a field, is window->Rect(). + ImRect OuterRectClipped; // == Window->Rect() just after setup in Begin(). == window->Rect() for root window. + ImRect InnerRect; // Inner rectangle (omit title bar, menu bar, scroll bar) + ImRect InnerClipRect; // == InnerRect shrunk by WindowPadding*0.5f on each side, clipped within viewport or parent clip rect. + ImRect WorkRect; // Initially covers the whole scrolling region. Reduced by containers e.g columns/tables when active. Shrunk by WindowPadding*1.0f on each side. This is meant to replace ContentRegionRect over time (from 1.71+ onward). + ImRect ParentWorkRect; // Backup of WorkRect before entering a container such as columns/tables. Used by e.g. SpanAllColumns functions to easily access. Stacked containers are responsible for maintaining this. // FIXME-WORKRECT: Could be a stack? + ImRect ClipRect; // Current clipping/scissoring rectangle, evolve as we are using PushClipRect(), etc. == DrawList->clip_rect_stack.back(). + ImRect ContentRegionRect; // FIXME: This is currently confusing/misleading. It is essentially WorkRect but not handling of scrolling. We currently rely on it as right/bottom aligned sizing operation need some size to rely on. + ImVec2ih HitTestHoleSize; // Define an optional rectangular hole where mouse will pass-through the window. + ImVec2ih HitTestHoleOffset; + + int LastFrameActive; // Last frame number the window was Active. + float LastTimeActive; // Last timestamp the window was Active (using float as we don't need high precision there) + float ItemWidthDefault; + ImGuiStorage StateStorage; + ImVector ColumnsStorage; + float FontWindowScale; // User scale multiplier per-window, via SetWindowFontScale() + float FontWindowScaleParents; + float FontRefSize; // This is a copy of window->CalcFontSize() at the time of Begin(), trying to phase out CalcFontSize() especially as it may be called on non-current window. + int SettingsOffset; // Offset into SettingsWindows[] (offsets are always valid as we only grow the array from the back) + + ImDrawList* DrawList; // == &DrawListInst (for backward compatibility reason with code using imgui_internal.h we keep this a pointer) + ImDrawList DrawListInst; + ImGuiWindow* ParentWindow; // If we are a child _or_ popup _or_ docked window, this is pointing to our parent. Otherwise NULL. + ImGuiWindow* ParentWindowInBeginStack; + ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. Doesn't cross through popups/dock nodes. + ImGuiWindow* RootWindowPopupTree; // Point to ourself or first ancestor that is not a child window. Cross through popups parent<>child. + ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. + ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + ImGuiWindow* ParentWindowForFocusRoute; // Set to manual link a window to its logical parent so that Shortcut() chain are honoerd (e.g. Tool linked to Document) + + ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) + ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1) + ImRect NavRectRel[ImGuiNavLayer_COUNT]; // Reference rectangle, in window relative space + ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; // Preferred X/Y position updated when moving on a given axis, reset to FLT_MAX. + ImGuiID NavRootFocusScopeId; // Focus Scope ID at the time of Begin() + + int MemoryDrawListIdxCapacity; // Backup of last idx/vtx count, so when waking up the window we can preallocate and avoid iterative alloc/copy + int MemoryDrawListVtxCapacity; + bool MemoryCompacted; // Set when window extraneous data have been garbage collected + +public: + ImGuiWindow(ImGuiContext* context, const char* name); + ~ImGuiWindow(); + + ImGuiID GetID(const char* str, const char* str_end = NULL); + ImGuiID GetID(const void* ptr); + ImGuiID GetID(int n); + ImGuiID GetIDFromPos(const ImVec2& p_abs); + ImGuiID GetIDFromRectangle(const ImRect& r_abs); + + // We don't use g.FontSize because the window may be != g.CurrentWindow. + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + float CalcFontSize() const { ImGuiContext& g = *Ctx; return g.FontBaseSize * FontWindowScale * FontWindowScaleParents; } + ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight)); } + ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight; return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Tab bar, Tab item support +//----------------------------------------------------------------------------- + +// Extend ImGuiTabBarFlags_ +enum ImGuiTabBarFlagsPrivate_ +{ + ImGuiTabBarFlags_DockNode = 1 << 20, // Part of a dock node [we don't use this in the master branch but it facilitate branch syncing to keep this around] + ImGuiTabBarFlags_IsFocused = 1 << 21, + ImGuiTabBarFlags_SaveSettings = 1 << 22, // FIXME: Settings are handled by the docking system, this only request the tab bar to mark settings dirty when reordering tabs +}; + +// Extend ImGuiTabItemFlags_ +enum ImGuiTabItemFlagsPrivate_ +{ + ImGuiTabItemFlags_SectionMask_ = ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, + ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_Button = 1 << 21, // Used by TabItemButton, change the tab item behavior to mimic a button + ImGuiTabItemFlags_Invisible = 1 << 22, // To reserve space e.g. with ImGuiTabItemFlags_Leading + //ImGuiTabItemFlags_Unsorted = 1 << 23, // [Docking] Trailing tabs with the _Unsorted flag will be sorted based on the DockOrder of their Window. +}; + +// Storage for one active tab item (sizeof() 40 bytes) +struct ImGuiTabItem +{ + ImGuiID ID; + ImGuiTabItemFlags Flags; + int LastFrameVisible; + int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance + float Offset; // Position relative to beginning of tab + float Width; // Width currently displayed + float ContentWidth; // Width of label, stored during BeginTabItem() call + float RequestedWidth; // Width optionally requested by caller, -1.0f is unused + ImS32 NameOffset; // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames + ImS16 BeginOrder; // BeginTabItem() order, used to re-order tabs after toggling ImGuiTabBarFlags_Reorderable + ImS16 IndexDuringLayout; // Index only used during TabBarLayout(). Tabs gets reordered so 'Tabs[n].IndexDuringLayout == n' but may mismatch during additions. + bool WantClose; // Marked as closed by SetTabItemClosed() + + ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; } +}; + +// Storage for a tab bar (sizeof() 160 bytes) +struct IMGUI_API ImGuiTabBar +{ + ImGuiWindow* Window; + ImVector Tabs; + ImGuiTabBarFlags Flags; + ImGuiID ID; // Zero for tab-bars used by docking + ImGuiID SelectedTabId; // Selected tab/window + ImGuiID NextSelectedTabId; // Next selected tab/window. Will also trigger a scrolling animation + ImGuiID VisibleTabId; // Can occasionally be != SelectedTabId (e.g. when previewing contents for CTRL+TAB preview) + int CurrFrameVisible; + int PrevFrameVisible; + ImRect BarRect; + float CurrTabsContentsHeight; + float PrevTabsContentsHeight; // Record the height of contents submitted below the tab bar + float WidthAllTabs; // Actual width of all tabs (locked during layout) + float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped + float ScrollingAnim; + float ScrollingTarget; + float ScrollingTargetDistToVisibility; + float ScrollingSpeed; + float ScrollingRectMinX; + float ScrollingRectMaxX; + float SeparatorMinX; + float SeparatorMaxX; + ImGuiID ReorderRequestTabId; + ImS16 ReorderRequestOffset; + ImS8 BeginCount; + bool WantLayout; + bool VisibleTabWasSubmitted; + bool TabsAddedNew; // Set to true when a new tab item or button has been added to the tab bar during last frame + ImS16 TabsActiveCount; // Number of tabs submitted this frame. + ImS16 LastTabItemIdx; // Index of last BeginTabItem() tab for use by EndTabItem() + float ItemSpacingY; + ImVec2 FramePadding; // style.FramePadding locked at the time of BeginTabBar() + ImVec2 BackupCursorPos; + ImGuiTextBuffer TabsNames; // For non-docking tab bar we re-append names in a contiguous buffer. + + ImGuiTabBar(); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Table support +//----------------------------------------------------------------------------- + +#define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code which cannot be used as a regular color. +#define IMGUI_TABLE_MAX_COLUMNS 512 // May be further lifted + +// Our current column maximum is 64 but we may raise that in the future. +typedef ImS16 ImGuiTableColumnIdx; +typedef ImU16 ImGuiTableDrawChannelIdx; + +// [Internal] sizeof() ~ 112 +// We use the terminology "Enabled" to refer to a column that is not Hidden by user/api. +// We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping. +// This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped". +struct ImGuiTableColumn +{ + ImGuiTableColumnFlags Flags; // Flags after some patching (not directly same as provided by user). See ImGuiTableColumnFlags_ + float WidthGiven; // Final/actual width visible == (MaxX - MinX), locked in TableUpdateLayout(). May be > WidthRequest to honor minimum width, may be < WidthRequest to honor shrinking columns down in tight space. + float MinX; // Absolute positions + float MaxX; + float WidthRequest; // Master width absolute value when !(Flags & _WidthStretch). When Stretch this is derived every frame from StretchWeight in TableUpdateLayout() + float WidthAuto; // Automatic width + float WidthMax; // Maximum width (FIXME: overwritten by each instance) + float StretchWeight; // Master width weight when (Flags & _WidthStretch). Often around ~1.0f initially. + float InitStretchWeightOrWidth; // Value passed to TableSetupColumn(). For Width it is a content width (_without padding_). + ImRect ClipRect; // Clipping rectangle for the column + ImGuiID UserID; // Optional, value passed to TableSetupColumn() + float WorkMinX; // Contents region min ~(MinX + CellPaddingX + CellSpacingX1) == cursor start position when entering column + float WorkMaxX; // Contents region max ~(MaxX - CellPaddingX - CellSpacingX2) + float ItemWidth; // Current item width for the column, preserved across rows + float ContentMaxXFrozen; // Contents maximum position for frozen rows (apart from headers), from which we can infer content width. + float ContentMaxXUnfrozen; + float ContentMaxXHeadersUsed; // Contents maximum position for headers rows (regardless of freezing). TableHeader() automatically softclip itself + report ideal desired size, to avoid creating extraneous draw calls + float ContentMaxXHeadersIdeal; + ImS16 NameOffset; // Offset into parent ColumnsNames[] + ImGuiTableColumnIdx DisplayOrder; // Index within Table's IndexToDisplayOrder[] (column may be reordered by users) + ImGuiTableColumnIdx IndexWithinEnabledSet; // Index within enabled/visible set (<= IndexToDisplayOrder) + ImGuiTableColumnIdx PrevEnabledColumn; // Index of prev enabled/visible column within Columns[], -1 if first enabled/visible column + ImGuiTableColumnIdx NextEnabledColumn; // Index of next enabled/visible column within Columns[], -1 if last enabled/visible column + ImGuiTableColumnIdx SortOrder; // Index of this column within sort specs, -1 if not sorting on this column, 0 for single-sort, may be >0 on multi-sort + ImGuiTableDrawChannelIdx DrawChannelCurrent; // Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx DrawChannelFrozen; // Draw channels for frozen rows (often headers) + ImGuiTableDrawChannelIdx DrawChannelUnfrozen; // Draw channels for unfrozen rows + bool IsEnabled; // IsUserEnabled && (Flags & ImGuiTableColumnFlags_Disabled) == 0 + bool IsUserEnabled; // Is the column not marked Hidden by the user? (unrelated to being off view, e.g. clipped by scrolling). + bool IsUserEnabledNextFrame; + bool IsVisibleX; // Is actually in view (e.g. overlapping the host window clipping rectangle, not scrolled). + bool IsVisibleY; + bool IsRequestOutput; // Return value for TableSetColumnIndex() / TableNextColumn(): whether we request user to output contents or not. + bool IsSkipItems; // Do we want item submissions to this column to be completely ignored (no layout will happen). + bool IsPreserveWidthAuto; + ImS8 NavLayerCurrent; // ImGuiNavLayer in 1 byte + ImU8 AutoFitQueue; // Queue of 8 values for the next 8 frames to request auto-fit + ImU8 CannotSkipItemsQueue; // Queue of 8 values for the next 8 frames to disable Clipped/SkipItem + ImU8 SortDirection : 2; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + ImU8 SortDirectionsAvailCount : 2; // Number of available sort directions (0 to 3) + ImU8 SortDirectionsAvailMask : 4; // Mask of available sort directions (1-bit each) + ImU8 SortDirectionsAvailList; // Ordered list of available sort directions (2-bits each, total 8-bits) + + ImGuiTableColumn() + { + memset(this, 0, sizeof(*this)); + StretchWeight = WidthRequest = -1.0f; + NameOffset = -1; + DisplayOrder = IndexWithinEnabledSet = -1; + PrevEnabledColumn = NextEnabledColumn = -1; + SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + DrawChannelCurrent = DrawChannelFrozen = DrawChannelUnfrozen = (ImU8)-1; + } +}; + +// Transient cell data stored per row. +// sizeof() ~ 6 bytes +struct ImGuiTableCellData +{ + ImU32 BgColor; // Actual color + ImGuiTableColumnIdx Column; // Column number +}; + +// Parameters for TableAngledHeadersRowEx() +// This may end up being refactored for more general purpose. +// sizeof() ~ 12 bytes +struct ImGuiTableHeaderData +{ + ImGuiTableColumnIdx Index; // Column index + ImU32 TextColor; + ImU32 BgColor0; + ImU32 BgColor1; +}; + +// Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?) +// sizeof() ~ 24 bytes +struct ImGuiTableInstanceData +{ + ImGuiID TableInstanceID; + float LastOuterHeight; // Outer height from last frame + float LastTopHeadersRowHeight; // Height of first consecutive header rows from last frame (FIXME: this is used assuming consecutive headers are in same frozen set) + float LastFrozenHeight; // Height of frozen section from last frame + int HoveredRowLast; // Index of row which was hovered last frame. + int HoveredRowNext; // Index of row hovered this frame, set after encountering it. + + ImGuiTableInstanceData() { TableInstanceID = 0; LastOuterHeight = LastTopHeadersRowHeight = LastFrozenHeight = 0.0f; HoveredRowLast = HoveredRowNext = -1; } +}; + +// sizeof() ~ 592 bytes + heap allocs described in TableBeginInitMemory() +struct IMGUI_API ImGuiTable +{ + ImGuiID ID; + ImGuiTableFlags Flags; + void* RawData; // Single allocation to hold Columns[], DisplayOrderToIndex[], and RowCellData[] + ImGuiTableTempData* TempData; // Transient data while table is active. Point within g.CurrentTableStack[] + ImSpan Columns; // Point within RawData[] + ImSpan DisplayOrderToIndex; // Point within RawData[]. Store display order of columns (when not reordered, the values are 0...Count-1) + ImSpan RowCellData; // Point within RawData[]. Store cells background requests for current row. + ImBitArrayPtr EnabledMaskByDisplayOrder; // Column DisplayOrder -> IsEnabled map + ImBitArrayPtr EnabledMaskByIndex; // Column Index -> IsEnabled map (== not hidden by user/api) in a format adequate for iterating column without touching cold data + ImBitArrayPtr VisibleMaskByIndex; // Column Index -> IsVisibleX|IsVisibleY map (== not hidden by user/api && not hidden by scrolling/cliprect) + ImGuiTableFlags SettingsLoadedFlags; // Which data were loaded from the .ini file (e.g. when order is not altered we won't save order) + int SettingsOffset; // Offset in g.SettingsTables + int LastFrameActive; + int ColumnsCount; // Number of columns declared in BeginTable() + int CurrentRow; + int CurrentColumn; + ImS16 InstanceCurrent; // Count of BeginTable() calls with same ID in the same frame (generally 0). This is a little bit similar to BeginCount for a window, but multiple tables with the same ID are multiple tables, they are just synced. + ImS16 InstanceInteracted; // Mark which instance (generally 0) of the same ID is being interacted with + float RowPosY1; + float RowPosY2; + float RowMinHeight; // Height submitted to TableNextRow() + float RowCellPaddingY; // Top and bottom padding. Reloaded during row change. + float RowTextBaseline; + float RowIndentOffsetX; + ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ + ImGuiTableRowFlags LastRowFlags : 16; + int RowBgColorCounter; // Counter for alternating background colors (can be fast-forwarded by e.g clipper), not same as CurrentRow because header rows typically don't increase this. + ImU32 RowBgColor[2]; // Background color override for current row. + ImU32 BorderColorStrong; + ImU32 BorderColorLight; + float BorderX1; + float BorderX2; + float HostIndentX; + float MinColumnWidth; + float OuterPaddingX; + float CellPaddingX; // Padding from each borders. Locked in BeginTable()/Layout. + float CellSpacingX1; // Spacing between non-bordered cells. Locked in BeginTable()/Layout. + float CellSpacingX2; + float InnerWidth; // User value passed to BeginTable(), see comments at the top of BeginTable() for details. + float ColumnsGivenWidth; // Sum of current column width + float ColumnsAutoFitWidth; // Sum of ideal column width in order nothing to be clipped, used for auto-fitting and content width submission in outer window + float ColumnsStretchSumWeights; // Sum of weight of all enabled stretching columns + float ResizedColumnNextWidth; + float ResizeLockMinContentsX2; // Lock minimum contents width while resizing down in order to not create feedback loops. But we allow growing the table. + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + float AngledHeadersHeight; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + float AngledHeadersSlope; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + ImRect OuterRect; // Note: for non-scrolling table, OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). + ImRect InnerRect; // InnerRect but without decoration. As with OuterRect, for non-scrolling tables, InnerRect.Max.y is " + ImRect WorkRect; + ImRect InnerClipRect; + ImRect BgClipRect; // We use this to cpu-clip cell background color fill, evolve during the frame as we cross frozen rows boundaries + ImRect Bg0ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG0/1 channel. This tends to be == OuterWindow->ClipRect at BeginTable() because output in BG0/BG1 is cpu-clipped + ImRect Bg2ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG2 channel. This tends to be a correct, tight-fit, because output to BG2 are done by widgets relying on regular ClipRect. + ImRect HostClipRect; // This is used to check if we can eventually merge our columns draw calls into the current draw call of the current window. + ImRect HostBackupInnerClipRect; // Backup of InnerWindow->ClipRect during PushTableBackground()/PopTableBackground() + ImGuiWindow* OuterWindow; // Parent window for the table + ImGuiWindow* InnerWindow; // Window holding the table data (== OuterWindow or a child window) + ImGuiTextBuffer ColumnsNames; // Contiguous buffer holding columns names + ImDrawListSplitter* DrawSplitter; // Shortcut to TempData->DrawSplitter while in table. Isolate draw commands per columns to avoid switching clip rect constantly + ImGuiTableInstanceData InstanceDataFirst; + ImVector InstanceDataExtra; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableColumnSortSpecs SortSpecsSingle; + ImVector SortSpecsMulti; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() + ImGuiTableColumnIdx SortSpecsCount; + ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns using fixed width (<= ColumnsCount) + ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() + ImGuiTableColumnIdx AngledHeadersCount; // Count columns with angled headers + ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! + ImGuiTableColumnIdx HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImGuiTableColumnIdx HighlightColumnHeader; // Index of column which should be highlighted. + ImGuiTableColumnIdx AutoFitSingleColumn; // Index of single column requesting auto-fit. + ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. + ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. + ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. + ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumnDir; // -1 or +1 + ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. + ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. + ImGuiTableColumnIdx LeftMostStretchedColumn; // Index of left-most stretched column. + ImGuiTableColumnIdx RightMostStretchedColumn; // Index of right-most stretched column. + ImGuiTableColumnIdx ContextPopupColumn; // Column right-clicked on, of -1 if opening context menu from a neutral/empty spot + ImGuiTableColumnIdx FreezeRowsRequest; // Requested frozen rows count + ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count + ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row + ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. + ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; + ImS8 NavLayer; // ImGuiNavLayer at the time of BeginTable(). + bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. + bool IsInsideRow; // Set when inside TableBeginRow()/TableEndRow(). + bool IsInitializing; + bool IsSortSpecsDirty; + bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. + bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). + bool DisableDefaultContextMenu; // Disable default context menu contents. You may submit your own using TableBeginContextMenuPopup()/EndPopup() + bool IsSettingsRequestLoad; + bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. + bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) + bool IsResetAllRequest; + bool IsResetDisplayOrderRequest; + bool IsUnfrozenRows; // Set when we got past the frozen row. + bool IsDefaultSizingPolicy; // Set if user didn't explicitly set a sizing policy in BeginTable() + bool IsActiveIdAliveBeforeTable; + bool IsActiveIdInTable; + bool HasScrollbarYCurr; // Whether ANY instance of this table had a vertical scrollbar during the current frame. + bool HasScrollbarYPrev; // Whether ANY instance of this table had a vertical scrollbar during the previous. + bool MemoryCompacted; + bool HostSkipItems; // Backup of InnerWindow->SkipItem at the end of BeginTable(), because we will overwrite InnerWindow->SkipItem on a per-column basis + + ImGuiTable() { memset(this, 0, sizeof(*this)); LastFrameActive = -1; } + ~ImGuiTable() { IM_FREE(RawData); } +}; + +// Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). +// - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. +// - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. +// FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs. +// sizeof() ~ 136 bytes. +struct IMGUI_API ImGuiTableTempData +{ + int TableIndex; // Index in g.Tables.Buf[] pool + float LastTimeActive; // Last timestamp this structure was used + float AngledHeadersExtraWidth; // Used in EndTable() + ImVector AngledHeadersRequests; // Used in TableAngledHeadersRow() + + ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() + ImDrawListSplitter DrawSplitter; + + ImRect HostBackupWorkRect; // Backup of InnerWindow->WorkRect at the end of BeginTable() + ImRect HostBackupParentWorkRect; // Backup of InnerWindow->ParentWorkRect at the end of BeginTable() + ImVec2 HostBackupPrevLineSize; // Backup of InnerWindow->DC.PrevLineSize at the end of BeginTable() + ImVec2 HostBackupCurrLineSize; // Backup of InnerWindow->DC.CurrLineSize at the end of BeginTable() + ImVec2 HostBackupCursorMaxPos; // Backup of InnerWindow->DC.CursorMaxPos at the end of BeginTable() + ImVec1 HostBackupColumnsOffset; // Backup of OuterWindow->DC.ColumnsOffset at the end of BeginTable() + float HostBackupItemWidth; // Backup of OuterWindow->DC.ItemWidth at the end of BeginTable() + int HostBackupItemWidthStackSize;//Backup of OuterWindow->DC.ItemWidthStack.Size at the end of BeginTable() + + ImGuiTableTempData() { memset(this, 0, sizeof(*this)); LastTimeActive = -1.0f; } +}; + +// sizeof() ~ 16 +struct ImGuiTableColumnSettings +{ + float WidthOrWeight; + ImGuiID UserID; + ImGuiTableColumnIdx Index; + ImGuiTableColumnIdx DisplayOrder; + ImGuiTableColumnIdx SortOrder; + ImU8 SortDirection : 2; + ImS8 IsEnabled : 2; // "Visible" in ini file + ImU8 IsStretch : 1; + + ImGuiTableColumnSettings() + { + WidthOrWeight = 0.0f; + UserID = 0; + Index = -1; + DisplayOrder = SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + IsEnabled = -1; + IsStretch = 0; + } +}; + +// This is designed to be stored in a single ImChunkStream (1 header followed by N ImGuiTableColumnSettings, etc.) +struct ImGuiTableSettings +{ + ImGuiID ID; // Set to 0 to invalidate/delete the setting + ImGuiTableFlags SaveFlags; // Indicate data we want to save using the Resizable/Reorderable/Sortable/Hideable flags (could be using its own flags..) + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + ImGuiTableColumnIdx ColumnsCount; + ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + + ImGuiTableSettings() { memset(this, 0, sizeof(*this)); } + ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGui internal API +// No guarantee of forward compatibility here! +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // Windows + // We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) + // If this ever crashes because g.CurrentWindow is NULL, it means that either: + // - ImGui::NewFrame() has never been called, which is illegal. + // - You are calling ImGui functions after ImGui::EndFrame()/ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. + IMGUI_API ImGuiIO& GetIO(ImGuiContext* ctx); + IMGUI_API ImGuiPlatformIO& GetPlatformIO(ImGuiContext* ctx); + inline ImGuiWindow* GetCurrentWindowRead() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } + inline ImGuiWindow* GetCurrentWindow() { ImGuiContext& g = *GImGui; g.CurrentWindow->WriteAccessed = true; return g.CurrentWindow; } + IMGUI_API ImGuiWindow* FindWindowByID(ImGuiID id); + IMGUI_API ImGuiWindow* FindWindowByName(const char* name); + IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); + IMGUI_API void UpdateWindowSkipRefresh(ImGuiWindow* window); + IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy); + IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API bool IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below); + IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); + IMGUI_API void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond = 0); + IMGUI_API void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond = 0); + IMGUI_API void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond = 0); + IMGUI_API void SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size); + IMGUI_API void SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window); + inline void SetWindowParentWindowForFocusRoute(ImGuiWindow* window, ImGuiWindow* parent_window) { window->ParentWindowForFocusRoute = parent_window; } + inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } + inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } + inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } + + // Windows: Display Order and Focus Order + IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); + IMGUI_API void FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags); + IMGUI_API void BringWindowToFocusFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBack(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* above_window); + IMGUI_API int FindWindowDisplayIndex(ImGuiWindow* window); + IMGUI_API ImGuiWindow* FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* window); + + // Windows: Idle, Refresh Policies [EXPERIMENTAL] + IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); + + // Fonts, drawing + IMGUI_API void SetCurrentFont(ImFont* font); + inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } + IMGUI_API void PushPasswordFont(); + inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { IM_UNUSED(window); return GetForegroundDrawList(); } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches. + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport); // get background draw list for the given viewport. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport); // get foreground draw list for the given viewport. this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. + IMGUI_API void AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list); + + // Init + IMGUI_API void Initialize(); + IMGUI_API void Shutdown(); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). + + // NewFrame + IMGUI_API void UpdateInputEvents(bool trickle_fast_inputs); + IMGUI_API void UpdateHoveredWindowAndCaptureFlags(); + IMGUI_API void FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window); + IMGUI_API void StartMouseMovingWindow(ImGuiWindow* window); + IMGUI_API void UpdateMouseMovingWindowNewFrame(); + IMGUI_API void UpdateMouseMovingWindowEndFrame(); + + // Generic context hooks + IMGUI_API ImGuiID AddContextHook(ImGuiContext* context, const ImGuiContextHook* hook); + IMGUI_API void RemoveContextHook(ImGuiContext* context, ImGuiID hook_to_remove); + IMGUI_API void CallContextHooks(ImGuiContext* context, ImGuiContextHookType type); + + // Viewports + IMGUI_API void ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale); + IMGUI_API void SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + + // Settings + IMGUI_API void MarkIniSettingsDirty(); + IMGUI_API void MarkIniSettingsDirty(ImGuiWindow* window); + IMGUI_API void ClearIniSettings(); + IMGUI_API void AddSettingsHandler(const ImGuiSettingsHandler* handler); + IMGUI_API void RemoveSettingsHandler(const char* type_name); + IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); + + // Settings - Windows + IMGUI_API ImGuiWindowSettings* CreateNewWindowSettings(const char* name); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByID(ImGuiID id); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByWindow(ImGuiWindow* window); + IMGUI_API void ClearWindowSettings(const char* name); + + // Localization + IMGUI_API void LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count); + inline const char* LocalizeGetMsg(ImGuiLocKey key) { ImGuiContext& g = *GImGui; const char* msg = g.LocalizationTable[key]; return msg ? msg : "*Missing Text*"; } + + // Scrolling + IMGUI_API void SetScrollX(ImGuiWindow* window, float scroll_x); + IMGUI_API void SetScrollY(ImGuiWindow* window, float scroll_y); + IMGUI_API void SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio); + IMGUI_API void SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio); + + // Early work-in-progress API (ScrollToItem() will become public) + IMGUI_API void ScrollToItem(ImGuiScrollFlags flags = 0); + IMGUI_API void ScrollToRect(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); + IMGUI_API ImVec2 ScrollToRectEx(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); +//#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& rect) { ScrollToRect(window, rect, ImGuiScrollFlags_KeepVisibleEdgeY); } +//#endif + + // Basic Accessors + inline ImGuiItemStatusFlags GetItemStatusFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.StatusFlags; } + inline ImGuiItemFlags GetItemFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.ItemFlags; } + inline ImGuiID GetActiveID() { ImGuiContext& g = *GImGui; return g.ActiveId; } + inline ImGuiID GetFocusID() { ImGuiContext& g = *GImGui; return g.NavId; } + IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void SetFocusID(ImGuiID id, ImGuiWindow* window); + IMGUI_API void ClearActiveID(); + IMGUI_API ImGuiID GetHoveredID(); + IMGUI_API void SetHoveredID(ImGuiID id); + IMGUI_API void KeepAliveID(ImGuiID id); + IMGUI_API void MarkItemEdited(ImGuiID id); // Mark data associated to given item as "edited", used by IsItemDeactivatedAfterEdit() function. + IMGUI_API void PushOverrideID(ImGuiID id); // Push given value as-is at the top of the ID stack (whereas PushID combines old and new hashes) + IMGUI_API ImGuiID GetIDWithSeed(const char* str_id_begin, const char* str_id_end, ImGuiID seed); + IMGUI_API ImGuiID GetIDWithSeed(int n, ImGuiID seed); + + // Basic Helpers for widget code + IMGUI_API void ItemSize(const ImVec2& size, float text_baseline_y = -1.0f); + inline void ItemSize(const ImRect& bb, float text_baseline_y = -1.0f) { ItemSize(bb.GetSize(), text_baseline_y); } // FIXME: This is a misleading API since we expect CursorPos to be bb.Min. + IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags); + IMGUI_API bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags = 0); + IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id); + IMGUI_API void SetLastItemData(ImGuiID item_id, ImGuiItemFlags item_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect); + IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h); + IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); + IMGUI_API void PushMultiItemsWidths(int components, float width_full); + IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess); + + // Parameter stacks (shared) + IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx); + IMGUI_API void BeginDisabledOverrideReenable(); + IMGUI_API void EndDisabledOverrideReenable(); + + // Logging/Capture + IMGUI_API void LogBegin(ImGuiLogFlags flags, int auto_open_depth); // -> BeginCapture() when we design v2 api, for now stay under the radar by using the old name. + IMGUI_API void LogToBuffer(int auto_open_depth = -1); // Start logging/capturing to internal buffer + IMGUI_API void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL); + IMGUI_API void LogSetNextTextDecoration(const char* prefix, const char* suffix); + + // Childs + IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); + + // Popups, Modals + IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginPopupMenuEx(ImGuiID id, const char* label, ImGuiWindowFlags extra_window_flags); + IMGUI_API void OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_None); + IMGUI_API void ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup); + IMGUI_API void ClosePopupsExceptModals(); + IMGUI_API bool IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags); + IMGUI_API ImRect GetPopupAllowedExtentRect(ImGuiWindow* window); + IMGUI_API ImGuiWindow* GetTopMostPopupModal(); + IMGUI_API ImGuiWindow* GetTopMostAndVisiblePopupModal(); + IMGUI_API ImGuiWindow* FindBlockingModal(ImGuiWindow* window); + IMGUI_API ImVec2 FindBestWindowPosForPopup(ImGuiWindow* window); + IMGUI_API ImVec2 FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy); + + // Tooltips + IMGUI_API bool BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginTooltipHidden(); + + // Menus + IMGUI_API bool BeginViewportSideBar(const char* name, ImGuiViewport* viewport, ImGuiDir dir, float size, ImGuiWindowFlags window_flags); + IMGUI_API bool BeginMenuEx(const char* label, const char* icon, bool enabled = true); + IMGUI_API bool MenuItemEx(const char* label, const char* icon, const char* shortcut = NULL, bool selected = false, bool enabled = true); + + // Combos + IMGUI_API bool BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags); + IMGUI_API bool BeginComboPreview(); + IMGUI_API void EndComboPreview(); + + // Keyboard/Gamepad Navigation + IMGUI_API void NavInitWindow(ImGuiWindow* window, bool force_reinit); + IMGUI_API void NavInitRequestApplyResult(); + IMGUI_API bool NavMoveRequestButNoResultYet(); + IMGUI_API void NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); + IMGUI_API void NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data); + IMGUI_API void NavMoveRequestCancel(); + IMGUI_API void NavMoveRequestApplyResult(); + IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags); + IMGUI_API void NavHighlightActivated(ImGuiID id); + IMGUI_API void NavClearPreferredPosForAxis(ImGuiAxis axis); + IMGUI_API void SetNavCursorVisibleAfterMove(); + IMGUI_API void NavUpdateCurrentWindowIsScrollPushableX(); + IMGUI_API void SetNavWindow(ImGuiWindow* window); + IMGUI_API void SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel); + IMGUI_API void SetNavFocusScope(ImGuiID focus_scope_id); + + // Focus/Activation + // This should be part of a larger set of API: FocusItem(offset = -1), FocusItemByID(id), ActivateItem(offset = -1), ActivateItemByID(id) etc. which are + // much harder to design and implement than expected. I have a couple of private branches on this matter but it's not simple. For now implementing the easy ones. + IMGUI_API void FocusItem(); // Focus last item (no selection/activation). + IMGUI_API void ActivateItemByID(ImGuiID id); // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again. + + // Inputs + // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions. + inline bool IsNamedKey(ImGuiKey key) { return key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END; } + inline bool IsNamedKeyOrMod(ImGuiKey key) { return (key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END) || key == ImGuiMod_Ctrl || key == ImGuiMod_Shift || key == ImGuiMod_Alt || key == ImGuiMod_Super; } + inline bool IsLegacyKey(ImGuiKey key) { return key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_LegacyNativeKey_END; } + inline bool IsKeyboardKey(ImGuiKey key) { return key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END; } + inline bool IsGamepadKey(ImGuiKey key) { return key >= ImGuiKey_Gamepad_BEGIN && key < ImGuiKey_Gamepad_END; } + inline bool IsMouseKey(ImGuiKey key) { return key >= ImGuiKey_Mouse_BEGIN && key < ImGuiKey_Mouse_END; } + inline bool IsAliasKey(ImGuiKey key) { return key >= ImGuiKey_Aliases_BEGIN && key < ImGuiKey_Aliases_END; } + inline bool IsLRModKey(ImGuiKey key) { return key >= ImGuiKey_LeftCtrl && key <= ImGuiKey_RightSuper; } + ImGuiKeyChord FixupKeyChord(ImGuiKeyChord key_chord); + inline ImGuiKey ConvertSingleModFlagToKey(ImGuiKey key) + { + if (key == ImGuiMod_Ctrl) return ImGuiKey_ReservedForModCtrl; + if (key == ImGuiMod_Shift) return ImGuiKey_ReservedForModShift; + if (key == ImGuiMod_Alt) return ImGuiKey_ReservedForModAlt; + if (key == ImGuiMod_Super) return ImGuiKey_ReservedForModSuper; + return key; + } + + IMGUI_API ImGuiKeyData* GetKeyData(ImGuiContext* ctx, ImGuiKey key); + inline ImGuiKeyData* GetKeyData(ImGuiKey key) { ImGuiContext& g = *GImGui; return GetKeyData(&g, key); } + IMGUI_API const char* GetKeyChordName(ImGuiKeyChord key_chord); + inline ImGuiKey MouseButtonToKey(ImGuiMouseButton button) { IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); return (ImGuiKey)(ImGuiKey_MouseLeft + button); } + IMGUI_API bool IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold = -1.0f); + IMGUI_API ImVec2 GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down); + IMGUI_API float GetNavTweakPressedAmount(ImGuiAxis axis); + IMGUI_API int CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate); + IMGUI_API void GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate); + IMGUI_API void TeleportMousePos(const ImVec2& pos); + IMGUI_API void SetActiveIdUsingAllKeyboardKeys(); + inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } + + // [EXPERIMENTAL] Low-Level: Key/Input Ownership + // - The idea is that instead of "eating" a given input, we can link to an owner id. + // - Ownership is most often claimed as a result of reacting to a press/down event (but occasionally may be claimed ahead). + // - Input queries can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Legacy input queries (without specifying an owner or _Any or _None) are equivalent to using ImGuiKeyOwner_Any (== 0). + // - Input ownership is automatically released on the frame after a key is released. Therefore: + // - for ownership registration happening as a result of a down/press event, the SetKeyOwner() call may be done once (common case). + // - for ownership registration happening ahead of a down/press event, the SetKeyOwner() call needs to be made every frame (happens if e.g. claiming ownership on hover). + // - SetItemKeyOwner() is a shortcut for common simple case. A custom widget will probably want to call SetKeyOwner() multiple times directly based on its interaction state. + // - This is marked experimental because not all widgets are fully honoring the Set/Test idioms. We will need to move forward step by step. + // Please open a GitHub Issue to submit your usage scenario or if there's a use case you need solved. + IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key); + IMGUI_API void SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetKeyOwnersForKeyChord(ImGuiKeyChord key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); // Set key owner to last item if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + IMGUI_API bool TestKeyOwner(ImGuiKey key, ImGuiID owner_id); // Test that key is either not owned, either owned by 'owner_id' + inline ImGuiKeyOwnerData* GetKeyOwnerData(ImGuiContext* ctx, ImGuiKey key) { if (key & ImGuiMod_Mask_) key = ConvertSingleModFlagToKey(key); IM_ASSERT(IsNamedKey(key)); return &ctx->KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; } + + // [EXPERIMENTAL] High-Level: Input Access functions w/ support for Key/Input Ownership + // - Important: legacy IsKeyPressed(ImGuiKey, bool repeat=true) _DEFAULTS_ to repeat, new IsKeyPressed() requires _EXPLICIT_ ImGuiInputFlags_Repeat flag. + // - Expected to be later promoted to public API, the prototypes are designed to replace existing ones (since owner_id can default to Any == 0) + // - Specifying a value for 'ImGuiID owner' will test that EITHER the key is NOT owned (UNLESS locked), EITHER the key is owned by 'owner'. + // Legacy functions use ImGuiKeyOwner_Any meaning that they typically ignore ownership, unless a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. + // - Binding generators may want to ignore those for now, or suffix them with Ex() until we decide if this gets moved into public API. + IMGUI_API bool IsKeyDown(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); // Important: when transitioning from old to new IsKeyPressed(): old API has "bool repeat = true", so would default to repeat. New API requiress explicit ImGuiInputFlags_Repeat. + IMGUI_API bool IsKeyReleased(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id); + + // Shortcut Testing & Routing + // - Set Shortcut() and SetNextItemShortcut() in imgui.h + // - When a policy (except for ImGuiInputFlags_RouteAlways *) is set, Shortcut() will register itself with SetShortcutRouting(), + // allowing the system to decide where to route the input among other route-aware calls. + // (* using ImGuiInputFlags_RouteAlways is roughly equivalent to calling IsKeyChordPressed(key) and bypassing route registration and check) + // - When using one of the routing option: + // - The default route is ImGuiInputFlags_RouteFocused (accept inputs if window is in focus stack. Deep-most focused window takes inputs. ActiveId takes inputs over deep-most focused window.) + // - Routes are requested given a chord (key + modifiers) and a routing policy. + // - Routes are resolved during NewFrame(): if keyboard modifiers are matching current ones: SetKeyOwner() is called + route is granted for the frame. + // - Each route may be granted to a single owner. When multiple requests are made we have policies to select the winning route (e.g. deep most window). + // - Multiple read sites may use the same owner id can all access the granted route. + // - When owner_id is 0 we use the current Focus Scope ID as a owner ID in order to identify our location. + // - You can chain two unrelated windows in the focus stack using SetWindowParentWindowForFocusRoute() + // e.g. if you have a tool window associated to a document, and you want document shortcuts to run when the tool is focused. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); + IMGUI_API bool SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); // owner_id needs to be explicit and cannot be 0 + IMGUI_API bool TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id); + IMGUI_API ImGuiKeyRoutingData* GetShortcutRoutingData(ImGuiKeyChord key_chord); + + // [EXPERIMENTAL] Focus Scope + // This is generally used to identify a unique input location (for e.g. a selection set) + // There is one per window (automatically set in Begin), but: + // - Selection patterns generally need to react (e.g. clear a selection) when landing on one item of the set. + // So in order to identify a set multiple lists in same window may each need a focus scope. + // If you imagine an hypothetical BeginSelectionGroup()/EndSelectionGroup() api, it would likely call PushFocusScope()/EndFocusScope() + // - Shortcut routing also use focus scope as a default location identifier if an owner is not provided. + // We don't use the ID Stack for this as it is common to want them separate. + IMGUI_API void PushFocusScope(ImGuiID id); + IMGUI_API void PopFocusScope(); + inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope() + + // Drag and Drop + IMGUI_API bool IsDragDropActive(); + IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); + IMGUI_API void ClearDragDrop(); + IMGUI_API bool IsDragDropPayloadBeingAccepted(); + IMGUI_API void RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect); + + // Typing-Select API + // (provide Windows Explorer style "select items by typing partial name" + "cycle through items by typing same letter" feature) + // (this is currently not documented nor used by main library, but should work. See "widgets_typingselect" in imgui_test_suite for usage code. Please let us know if you use this!) + IMGUI_API ImGuiTypingSelectRequest* GetTypingSelectRequest(ImGuiTypingSelectFlags flags = ImGuiTypingSelectFlags_None); + IMGUI_API int TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); + + // Box-Select API + IMGUI_API bool BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags); + IMGUI_API void EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags); + + // Multi-Select API + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); + IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + IMGUI_API void MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected); + IMGUI_API void MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item); + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } + inline ImGuiMultiSelectState* GetMultiSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return g.MultiSelectStorage.GetByKey(id); } + + // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) + IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); + IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). + IMGUI_API void EndColumns(); // close columns + IMGUI_API void PushColumnClipRect(int column_index); + IMGUI_API void PushColumnsBackground(); + IMGUI_API void PopColumnsBackground(); + IMGUI_API ImGuiID GetColumnsID(const char* str_id, int count); + IMGUI_API ImGuiOldColumns* FindOrCreateColumns(ImGuiWindow* window, ImGuiID id); + IMGUI_API float GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm); + IMGUI_API float GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset); + + // Tables: Candidates for public API + IMGUI_API void TableOpenContextMenu(int column_n = -1); + IMGUI_API void TableSetColumnWidth(int column_n, float width); + IMGUI_API void TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs); + IMGUI_API int TableGetHoveredRow(); // Retrieve *PREVIOUS FRAME* hovered row. This difference with TableGetHoveredColumn() is the reason why this is not public yet. + IMGUI_API float TableGetHeaderRowHeight(); + IMGUI_API float TableGetHeaderAngledMaxLabelWidth(); + IMGUI_API void TablePushBackgroundChannel(); + IMGUI_API void TablePopBackgroundChannel(); + IMGUI_API void TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count); + + // Tables: Internals + inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } + IMGUI_API ImGuiTable* TableFindByID(ImGuiID id); + IMGUI_API bool BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + IMGUI_API void TableBeginInitMemory(ImGuiTable* table, int columns_count); + IMGUI_API void TableBeginApplyRequests(ImGuiTable* table); + IMGUI_API void TableSetupDrawChannels(ImGuiTable* table); + IMGUI_API void TableUpdateLayout(ImGuiTable* table); + IMGUI_API void TableUpdateBorders(ImGuiTable* table); + IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); + IMGUI_API void TableDrawBorders(ImGuiTable* table); + IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display); + IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table); + IMGUI_API void TableMergeDrawChannels(ImGuiTable* table); + inline ImGuiTableInstanceData* TableGetInstanceData(ImGuiTable* table, int instance_no) { if (instance_no == 0) return &table->InstanceDataFirst; return &table->InstanceDataExtra[instance_no - 1]; } + inline ImGuiID TableGetInstanceID(ImGuiTable* table, int instance_no) { return TableGetInstanceData(table, instance_no)->TableInstanceID; } + IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); + IMGUI_API void TableSortSpecsBuild(ImGuiTable* table); + IMGUI_API ImGuiSortDirection TableGetColumnNextSortDirection(ImGuiTableColumn* column); + IMGUI_API void TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API float TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API void TableBeginRow(ImGuiTable* table); + IMGUI_API void TableEndRow(ImGuiTable* table); + IMGUI_API void TableBeginCell(ImGuiTable* table, int column_n); + IMGUI_API void TableEndCell(ImGuiTable* table); + IMGUI_API ImRect TableGetCellBgRect(const ImGuiTable* table, int column_n); + IMGUI_API const char* TableGetColumnName(const ImGuiTable* table, int column_n); + IMGUI_API ImGuiID TableGetColumnResizeID(ImGuiTable* table, int column_n, int instance_no = 0); + IMGUI_API float TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); + IMGUI_API void TableRemove(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); + IMGUI_API void TableGcCompactSettings(); + + // Tables: Settings + IMGUI_API void TableLoadSettings(ImGuiTable* table); + IMGUI_API void TableSaveSettings(ImGuiTable* table); + IMGUI_API void TableResetSettings(ImGuiTable* table); + IMGUI_API ImGuiTableSettings* TableGetBoundSettings(ImGuiTable* table); + IMGUI_API void TableSettingsAddSettingsHandler(); + IMGUI_API ImGuiTableSettings* TableSettingsCreate(ImGuiID id, int columns_count); + IMGUI_API ImGuiTableSettings* TableSettingsFindByID(ImGuiID id); + + // Tab Bars + inline ImGuiTabBar* GetCurrentTabBar() { ImGuiContext& g = *GImGui; return g.CurrentTabBar; } + IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); + IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API ImGuiTabItem* TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order); + IMGUI_API ImGuiTabItem* TabBarGetCurrentTab(ImGuiTabBar* tab_bar); + inline int TabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { return tab_bar->Tabs.index_from_ptr(tab); } + IMGUI_API const char* TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API void TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name); + IMGUI_API void TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset); + IMGUI_API void TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, ImVec2 mouse_pos); + IMGUI_API bool TabBarProcessReorder(ImGuiTabBar* tab_bar); + IMGUI_API bool TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window); + IMGUI_API void TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width); + IMGUI_API ImVec2 TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker); + IMGUI_API ImVec2 TabItemCalcSize(ImGuiWindow* window); + IMGUI_API void TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col); + IMGUI_API void TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped); + + // Render helpers + // AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. + // NB: All position are in absolute pixels coordinates (we are never using window coordinates internally) + IMGUI_API void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true); + IMGUI_API void RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known); + IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders = true, float rounding = 0.0f); + IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); + IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0); + IMGUI_API void RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags = ImGuiNavRenderCursorFlags_None); // Navigation highlight +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags = ImGuiNavRenderCursorFlags_None) { RenderNavCursor(bb, id, flags); } // Renamed in 1.91.4 +#endif + IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. + IMGUI_API void RenderMouseCursor(ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); + + // Render helpers (those functions don't access any ImGui state!) + IMGUI_API void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale = 1.0f); + IMGUI_API void RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); + IMGUI_API void RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); + IMGUI_API void RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col); + IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); + IMGUI_API void RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding); + + // Widgets + IMGUI_API void TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0); + IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); + IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0); + IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0); + IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags, float thickness = 1.0f); + IMGUI_API void SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_width); + IMGUI_API bool CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value); + IMGUI_API bool CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value); + + // Widgets: Window Decorations + IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos); + IMGUI_API void Scrollbar(ImGuiAxis axis); + IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 avail_v, ImS64 contents_v, ImDrawFlags draw_rounding_flags = 0); + IMGUI_API ImRect GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowResizeCornerID(ImGuiWindow* window, int n); // 0..3: corners + IMGUI_API ImGuiID GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); + + // Widgets low-level behaviors + IMGUI_API bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0); + IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f, ImU32 bg_col = 0); + + // Widgets: Tree Nodes + IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); + IMGUI_API void TreePushOverrideID(ImGuiID id); + IMGUI_API bool TreeNodeGetOpen(ImGuiID storage_id); + IMGUI_API void TreeNodeSetOpen(ImGuiID storage_id, bool open); + IMGUI_API bool TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags); // Return open state. Consume previous SetNextItemOpen() data, if any. May return true when logging. + + // Template functions are instantiated in imgui_widgets.cpp for a finite number of types. + // To use them externally (for custom widget) you may need an "extern template" statement in your code in order to link to existing instances and silence Clang warnings (see #2036). + // e.g. " extern template IMGUI_API float RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, float v); " + template IMGUI_API float ScaleRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API T ScaleValueFromRatioT(ImGuiDataType data_type, float t, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, ImGuiSliderFlags flags); + template IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + template IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v); + template IMGUI_API bool CheckboxFlagsT(const char* label, T* flags, T flags_value); + + // Data type helpers + IMGUI_API const ImGuiDataTypeInfo* DataTypeGetInfo(ImGuiDataType data_type); + IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format); + IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty = NULL); + IMGUI_API int DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max); + IMGUI_API bool DataTypeIsZero(ImGuiDataType data_type, const void* p_data); + + // InputText + IMGUI_API bool InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API void InputTextDeactivateHook(ImGuiID id); + IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags); + IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL); + inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputId == id); } + inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active + IMGUI_API void SetNextItemRefVal(ImGuiDataType data_type, void* p_data); + inline bool IsItemActiveAsInputText() { ImGuiContext& g = *GImGui; return g.ActiveId != 0 && g.ActiveId == g.LastItemData.ID && g.InputTextState.ID == g.LastItemData.ID; } // This may be useful to apply workaround that a based on distinguish whenever an item is active as a text input field. + + // Color + IMGUI_API void ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags); + IMGUI_API void ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags); + + // Plot + IMGUI_API int PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg); + + // Shade functions (write over already created vertices) + IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); + IMGUI_API void ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + IMGUI_API void ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out); + + // Garbage collection + IMGUI_API void GcCompactTransientMiscBuffers(); + IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); + IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); + + // Error handling, State Recovery + IMGUI_API bool ErrorLog(const char* msg); + IMGUI_API void ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out); + IMGUI_API void ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in); + IMGUI_API void ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in); + IMGUI_API void ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + IMGUI_API void ErrorCheckEndFrameFinalizeErrorTooltip(); + IMGUI_API bool BeginErrorTooltip(); + IMGUI_API void EndErrorTooltip(); + + // Debug Tools + IMGUI_API void DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size); // size >= 0 : alloc, size = -1 : free + IMGUI_API void DebugDrawCursorPos(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawLineExtents(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawItemRect(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end); + IMGUI_API void DebugLocateItem(ImGuiID target_id); // Call sparingly: only 1 at the same time! + IMGUI_API void DebugLocateItemOnHover(ImGuiID target_id); // Only call on reaction to a mouse Hover: because only 1 at the same time! + IMGUI_API void DebugLocateItemResolveWithLastItem(); + IMGUI_API void DebugBreakClearData(); + IMGUI_API bool DebugBreakButton(const char* label, const char* description_of_location); + IMGUI_API void DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location); + IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); + IMGUI_API void DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end); + IMGUI_API void DebugNodeColumns(ImGuiOldColumns* columns); + IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label); + IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); + IMGUI_API void DebugNodeFont(ImFont* font); + IMGUI_API void DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); + IMGUI_API void DebugNodeStorage(ImGuiStorage* storage, const char* label); + IMGUI_API void DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); + IMGUI_API void DebugNodeTable(ImGuiTable* table); + IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings); + IMGUI_API void DebugNodeInputTextState(ImGuiInputTextState* state); + IMGUI_API void DebugNodeTypingSelectState(ImGuiTypingSelectState* state); + IMGUI_API void DebugNodeMultiSelectState(ImGuiMultiSelectState* state); + IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); + IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); + IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); + IMGUI_API void DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack); + IMGUI_API void DebugNodeViewport(ImGuiViewportP* viewport); + IMGUI_API void DebugRenderKeyboardPreview(ImDrawList* draw_list); + IMGUI_API void DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb); + + // Obsolete functions +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //inline void SetItemUsingMouseWheel() { SetItemKeyOwner(ImGuiKey_MouseWheelY); } // Changed in 1.89 + //inline bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0) { return TreeNodeUpdateNextOpen(id, flags); } // Renamed in 1.89 + //inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { IM_ASSERT(IsNamedKey(key)); return IsKeyPressed(key, repeat); } // Removed in 1.87: Mapping from named key is always identity! + + // Refactored focus/nav/tabbing system in 1.82 and 1.84. If you have old/custom copy-and-pasted widgets which used FocusableItemRegister(): + // (Old) IMGUI_VERSION_NUM < 18209: using 'ItemAdd(....)' and 'bool tab_focused = FocusableItemRegister(...)' + // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Focused) != 0' + // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))' + //inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() + //inline void FocusableItemUnregister(ImGuiWindow* window) // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem +#endif + +} // namespace ImGui + + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas internal API +//----------------------------------------------------------------------------- + +// This structure is likely to evolve as we add support for incremental atlas updates. +// Conceptually this could be in ImGuiPlatformIO, but we are far from ready to make this public. +struct ImFontBuilderIO +{ + bool (*FontBuilder_Build)(ImFontAtlas* atlas); +}; + +// Helper for font builder +#ifdef IMGUI_ENABLE_STB_TRUETYPE +IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); +#endif +IMGUI_API void ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src, float ascent, float descent); +IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); +IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); +IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); +IMGUI_API void ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v); + +IMGUI_API bool ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + +//----------------------------------------------------------------------------- +// [SECTION] Test Engine specific hooks (imgui_test_engine) +//----------------------------------------------------------------------------- + +#ifdef IMGUI_ENABLE_TEST_ENGINE +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ctx, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data); // item_data may be NULL +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); +extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const char* fmt, ...); +extern const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ctx, ImGuiID id); + +// In IMGUI_VERSION_NUM >= 18934: changed IMGUI_TEST_ENGINE_ITEM_ADD(bb,id) to IMGUI_TEST_ENGINE_ITEM_ADD(id,bb,item_data); +#define IMGUI_TEST_ENGINE_ITEM_ADD(_ID,_BB,_ITEM_DATA) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemAdd(&g, _ID, _BB, _ITEM_DATA) // Register item bounding box +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS) // Register item label and status flags (optional) +#define IMGUI_TEST_ENGINE_LOG(_FMT,...) ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__) // Custom log entry from user land into test log +#else +#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID) ((void)0) +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) ((void)g) +#endif + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imgui_tables.cpp b/thirdparty/imgui/imgui_tables.cpp new file mode 100644 index 00000000..c98aea2d --- /dev/null +++ b/thirdparty/imgui/imgui_tables.cpp @@ -0,0 +1,4525 @@ +// dear imgui, v1.91b +// (tables and columns code) + +/* + +Index of this file: + +// [SECTION] Commentary +// [SECTION] Header mess +// [SECTION] Tables: Main code +// [SECTION] Tables: Simple accessors +// [SECTION] Tables: Row changes +// [SECTION] Tables: Columns changes +// [SECTION] Tables: Columns width management +// [SECTION] Tables: Drawing +// [SECTION] Tables: Sorting +// [SECTION] Tables: Headers +// [SECTION] Tables: Context Menu +// [SECTION] Tables: Settings (.ini data) +// [SECTION] Tables: Garbage Collection +// [SECTION] Tables: Debugging +// [SECTION] Columns, BeginColumns, EndColumns, etc. + +*/ + +// Navigating this file: +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. + +//----------------------------------------------------------------------------- +// [SECTION] Commentary +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Typical tables call flow: (root level is generally public API): +//----------------------------------------------------------------------------- +// - BeginTable() user begin into a table +// | BeginChild() - (if ScrollX/ScrollY is set) +// | TableBeginInitMemory() - first time table is used +// | TableResetSettings() - on settings reset +// | TableLoadSettings() - on settings load +// | TableBeginApplyRequests() - apply queued resizing/reordering/hiding requests +// | - TableSetColumnWidth() - apply resizing width (for mouse resize, often requested by previous frame) +// | - TableUpdateColumnsWeightFromWidth()- recompute columns weights (of stretch columns) from their respective width +// - TableSetupColumn() user submit columns details (optional) +// - TableSetupScrollFreeze() user submit scroll freeze information (optional) +//----------------------------------------------------------------------------- +// - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow(). +// | TableSetupDrawChannels() - setup ImDrawList channels +// | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission +// | TableBeginContextMenuPopup() +// | - TableDrawDefaultContextMenu() - draw right-click context menu contents +//----------------------------------------------------------------------------- +// - TableHeadersRow() or TableHeader() user submit a headers row (optional) +// | TableSortSpecsClickColumn() - when left-clicked: alter sort order and sort direction +// | TableOpenContextMenu() - when right-clicked: trigger opening of the default context menu +// - TableGetSortSpecs() user queries updated sort specs (optional, generally after submitting headers) +// - TableNextRow() user begin into a new row (also automatically called by TableHeadersRow()) +// | TableEndRow() - finish existing row +// | TableBeginRow() - add a new row +// - TableSetColumnIndex() / TableNextColumn() user begin into a cell +// | TableEndCell() - close existing column/cell +// | TableBeginCell() - enter into current column/cell +// - [...] user emit contents +//----------------------------------------------------------------------------- +// - EndTable() user ends the table +// | TableDrawBorders() - draw outer borders, inner vertical borders +// | TableMergeDrawChannels() - merge draw channels if clipping isn't required +// | EndChild() - (if ScrollX/ScrollY is set) +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// TABLE SIZING +//----------------------------------------------------------------------------- +// (Read carefully because this is subtle but it does make sense!) +//----------------------------------------------------------------------------- +// About 'outer_size': +// Its meaning needs to differ slightly depending on if we are using ScrollX/ScrollY flags. +// Default value is ImVec2(0.0f, 0.0f). +// X +// - outer_size.x <= 0.0f -> Right-align from window/work-rect right-most edge. With -FLT_MIN or 0.0f will align exactly on right-most edge. +// - outer_size.x > 0.0f -> Set Fixed width. +// Y with ScrollX/ScrollY disabled: we output table directly in current window +// - outer_size.y < 0.0f -> Bottom-align (but will auto extend, unless _NoHostExtendY is set). Not meaningful if parent window can vertically scroll. +// - outer_size.y = 0.0f -> No minimum height (but will auto extend, unless _NoHostExtendY is set) +// - outer_size.y > 0.0f -> Set Minimum height (but will auto extend, unless _NoHostExtendY is set) +// Y with ScrollX/ScrollY enabled: using a child window for scrolling +// - outer_size.y < 0.0f -> Bottom-align. Not meaningful if parent window can vertically scroll. +// - outer_size.y = 0.0f -> Bottom-align, consistent with BeginChild(). Not recommended unless table is last item in parent window. +// - outer_size.y > 0.0f -> Set Exact height. Recommended when using Scrolling on any axis. +//----------------------------------------------------------------------------- +// Outer size is also affected by the NoHostExtendX/NoHostExtendY flags. +// Important to note how the two flags have slightly different behaviors! +// - ImGuiTableFlags_NoHostExtendX -> Make outer width auto-fit to columns (overriding outer_size.x value). Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. +// - ImGuiTableFlags_NoHostExtendY -> Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY is disabled. Data below the limit will be clipped and not visible. +// In theory ImGuiTableFlags_NoHostExtendY could be the default and any non-scrolling tables with outer_size.y != 0.0f would use exact height. +// This would be consistent but perhaps less useful and more confusing (as vertically clipped items are not useful and not easily noticeable). +//----------------------------------------------------------------------------- +// About 'inner_width': +// With ScrollX disabled: +// - inner_width -> *ignored* +// With ScrollX enabled: +// - inner_width < 0.0f -> *illegal* fit in known width (right align from outer_size.x) <-- weird +// - inner_width = 0.0f -> fit in outer_width: Fixed size columns will take space they need (if avail, otherwise shrink down), Stretch columns becomes Fixed columns. +// - inner_width > 0.0f -> override scrolling width, generally to be larger than outer_size.x. Fixed column take space they need (if avail, otherwise shrink down), Stretch columns share remaining space! +//----------------------------------------------------------------------------- +// Details: +// - If you want to use Stretch columns with ScrollX, you generally need to specify 'inner_width' otherwise the concept +// of "available space" doesn't make sense. +// - Even if not really useful, we allow 'inner_width < outer_size.x' for consistency and to facilitate understanding +// of what the value does. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// COLUMNS SIZING POLICIES +// (Reference: ImGuiTableFlags_SizingXXX flags and ImGuiTableColumnFlags_WidthXXX flags) +//----------------------------------------------------------------------------- +// About overriding column sizing policy and width/weight with TableSetupColumn(): +// We use a default parameter of -1 for 'init_width'/'init_weight'. +// - with ImGuiTableColumnFlags_WidthFixed, init_width <= 0 (default) --> width is automatic +// - with ImGuiTableColumnFlags_WidthFixed, init_width > 0 (explicit) --> width is custom +// - with ImGuiTableColumnFlags_WidthStretch, init_weight <= 0 (default) --> weight is 1.0f +// - with ImGuiTableColumnFlags_WidthStretch, init_weight > 0 (explicit) --> weight is custom +// Widths are specified _without_ CellPadding. If you specify a width of 100.0f, the column will be cover (100.0f + Padding * 2.0f) +// and you can fit a 100.0f wide item in it without clipping and with padding honored. +//----------------------------------------------------------------------------- +// About default sizing policy (if you don't specify a ImGuiTableColumnFlags_WidthXXXX flag) +// - with Table policy ImGuiTableFlags_SizingFixedFit --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is equal to contents width +// - with Table policy ImGuiTableFlags_SizingFixedSame --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is max of all contents width +// - with Table policy ImGuiTableFlags_SizingStretchSame --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is 1.0f +// - with Table policy ImGuiTableFlags_SizingStretchWeight --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is proportional to contents +// Default Width and default Weight can be overridden when calling TableSetupColumn(). +//----------------------------------------------------------------------------- +// About mixing Fixed/Auto and Stretch columns together: +// - the typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// - using mixed policies with ScrollX does not make much sense, as using Stretch columns with ScrollX does not make much sense in the first place! +// that is, unless 'inner_width' is passed to BeginTable() to explicitly provide a total width to layout columns in. +// - when using ImGuiTableFlags_SizingFixedSame with mixed columns, only the Fixed/Auto columns will match their widths to the width of the maximum contents. +// - when using ImGuiTableFlags_SizingStretchSame with mixed columns, only the Stretch columns will match their weights/widths. +//----------------------------------------------------------------------------- +// About using column width: +// If a column is manually resizable or has a width specified with TableSetupColumn(): +// - you may use GetContentRegionAvail().x to query the width available in a given column. +// - right-side alignment features such as SetNextItemWidth(-x) or PushItemWidth(-x) will rely on this width. +// If the column is not resizable and has no width specified with TableSetupColumn(): +// - its width will be automatic and be set to the max of items submitted. +// - therefore you generally cannot have ALL items of the columns use e.g. SetNextItemWidth(-FLT_MIN). +// - but if the column has one or more items of known/fixed size, this will become the reference width used by SetNextItemWidth(-FLT_MIN). +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// TABLES CLIPPING/CULLING +//----------------------------------------------------------------------------- +// About clipping/culling of Rows in Tables: +// - For large numbers of rows, it is recommended you use ImGuiListClipper to submit only visible rows. +// ImGuiListClipper is reliant on the fact that rows are of equal height. +// See 'Demo->Tables->Vertical Scrolling' or 'Demo->Tables->Advanced' for a demo of using the clipper. +// - Note that auto-resizing columns don't play well with using the clipper. +// By default a table with _ScrollX but without _Resizable will have column auto-resize. +// So, if you want to use the clipper, make sure to either enable _Resizable, either setup columns width explicitly with _WidthFixed. +//----------------------------------------------------------------------------- +// About clipping/culling of Columns in Tables: +// - Both TableSetColumnIndex() and TableNextColumn() return true when the column is visible or performing +// width measurements. Otherwise, you may skip submitting the contents of a cell/column, BUT ONLY if you know +// it is not going to contribute to row height. +// In many situations, you may skip submitting contents for every column but one (e.g. the first one). +// - Case A: column is not hidden by user, and at least partially in sight (most common case). +// - Case B: column is clipped / out of sight (because of scrolling or parent ClipRect): TableNextColumn() return false as a hint but we still allow layout output. +// - Case C: column is hidden explicitly by the user (e.g. via the context menu, or _DefaultHide column flag, etc.). +// +// [A] [B] [C] +// TableNextColumn(): true false false -> [userland] when TableNextColumn() / TableSetColumnIndex() returns false, user can skip submitting items but only if the column doesn't contribute to row height. +// SkipItems: false false true -> [internal] when SkipItems is true, most widgets will early out if submitted, resulting is no layout output. +// ClipRect: normal zero-width zero-width -> [internal] when ClipRect is zero, ItemAdd() will return false and most widgets will early out mid-way. +// ImDrawList output: normal dummy dummy -> [internal] when using the dummy channel, ImDrawList submissions (if any) will be wasted (because cliprect is zero-width anyway). +// +// - We need to distinguish those cases because non-hidden columns that are clipped outside of scrolling bounds should still contribute their height to the row. +// However, in the majority of cases, the contribution to row height is the same for all columns, or the tallest cells are known by the programmer. +//----------------------------------------------------------------------------- +// About clipping/culling of whole Tables: +// - Scrolling tables with a known outer size can be clipped earlier as BeginTable() will return false. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" + +// System includes +#include // intptr_t + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wstrict-overflow" +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Main code +//----------------------------------------------------------------------------- +// - TableFixFlags() [Internal] +// - TableFindByID() [Internal] +// - BeginTable() +// - BeginTableEx() [Internal] +// - TableBeginInitMemory() [Internal] +// - TableBeginApplyRequests() [Internal] +// - TableSetupColumnFlags() [Internal] +// - TableUpdateLayout() [Internal] +// - TableUpdateBorders() [Internal] +// - EndTable() +// - TableSetupColumn() +// - TableSetupScrollFreeze() +//----------------------------------------------------------------------------- + +// Configuration +static const int TABLE_DRAW_CHANNEL_BG0 = 0; +static const int TABLE_DRAW_CHANNEL_BG2_FROZEN = 1; +static const int TABLE_DRAW_CHANNEL_NOCLIP = 2; // When using ImGuiTableFlags_NoClip (this becomes the last visible channel) +static const float TABLE_BORDER_SIZE = 1.0f; // FIXME-TABLE: Currently hard-coded because of clipping assumptions with outer borders rendering. +static const float TABLE_RESIZE_SEPARATOR_HALF_THICKNESS = 4.0f; // Extend outside inner borders. +static const float TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER = 0.06f; // Delay/timer before making the hover feedback (color+cursor) visible because tables/columns tends to be more cramped. + +// Helper +inline ImGuiTableFlags TableFixFlags(ImGuiTableFlags flags, ImGuiWindow* outer_window) +{ + // Adjust flags: set default sizing policy + if ((flags & ImGuiTableFlags_SizingMask_) == 0) + flags |= ((flags & ImGuiTableFlags_ScrollX) || (outer_window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) ? ImGuiTableFlags_SizingFixedFit : ImGuiTableFlags_SizingStretchSame; + + // Adjust flags: enable NoKeepColumnsVisible when using ImGuiTableFlags_SizingFixedSame + if ((flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableFlags_NoKeepColumnsVisible; + + // Adjust flags: enforce borders when resizable + if (flags & ImGuiTableFlags_Resizable) + flags |= ImGuiTableFlags_BordersInnerV; + + // Adjust flags: disable NoHostExtendX/NoHostExtendY if we have any scrolling going on + if (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) + flags &= ~(ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_NoHostExtendY); + + // Adjust flags: NoBordersInBodyUntilResize takes priority over NoBordersInBody + if (flags & ImGuiTableFlags_NoBordersInBodyUntilResize) + flags &= ~ImGuiTableFlags_NoBordersInBody; + + // Adjust flags: disable saved settings if there's nothing to save + if ((flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable)) == 0) + flags |= ImGuiTableFlags_NoSavedSettings; + + // Inherit _NoSavedSettings from top-level window (child windows always have _NoSavedSettings set) + if (outer_window->RootWindow->Flags & ImGuiWindowFlags_NoSavedSettings) + flags |= ImGuiTableFlags_NoSavedSettings; + + return flags; +} + +ImGuiTable* ImGui::TableFindByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return g.Tables.GetByKey(id); +} + +// Read about "TABLE SIZING" at the top of this file. +bool ImGui::BeginTable(const char* str_id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiID id = GetID(str_id); + return BeginTableEx(str_id, id, columns_count, flags, outer_size, inner_width); +} + +bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* outer_window = GetCurrentWindow(); + if (outer_window->SkipItems) // Consistent with other tables + beneficial side effect that assert on miscalling EndTable() will be more visible. + return false; + + // Sanity checks + IM_ASSERT(columns_count > 0 && columns_count < IMGUI_TABLE_MAX_COLUMNS); + if (flags & ImGuiTableFlags_ScrollX) + IM_ASSERT(inner_width >= 0.0f); + + // If an outer size is specified ahead we will be able to early out when not visible. Exact clipping criteria may evolve. + // FIXME: coarse clipping because access to table data causes two issues: + // - instance numbers varying/unstable. may not be a direct problem for users, but could make outside access broken or confusing, e.g. TestEngine. + // - can't implement support for ImGuiChildFlags_ResizeY as we need to somehow pull the height data from somewhere. this also needs stable instance numbers. + // The side-effects of accessing table data on coarse clip would be: + // - always reserving the pooled ImGuiTable data ahead for a fully clipped table (minor IMHO). Also the 'outer_window_is_measuring_size' criteria may already be defeating this in some situations. + // - always performing the GetOrAddByKey() O(log N) query in g.Tables.Map[]. + const bool use_child_window = (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0; + const ImVec2 avail_size = GetContentRegionAvail(); + const ImVec2 actual_outer_size = ImTrunc(CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f)); + const ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); + const bool outer_window_is_measuring_size = (outer_window->AutoFitFramesX > 0) || (outer_window->AutoFitFramesY > 0); // Doesn't apply to AlwaysAutoResize windows! + if (use_child_window && IsClippedEx(outer_rect, 0) && !outer_window_is_measuring_size) + { + ItemSize(outer_rect); + ItemAdd(outer_rect, id); + g.NextWindowData.ClearFlags(); + return false; + } + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInTable == id) + IM_DEBUG_BREAK(); + + // Acquire storage for the table + ImGuiTable* table = g.Tables.GetOrAddByKey(id); + + // Acquire temporary buffers + const int table_idx = g.Tables.GetIndex(table); + if (++g.TablesTempDataStacked > g.TablesTempData.Size) + g.TablesTempData.resize(g.TablesTempDataStacked, ImGuiTableTempData()); + ImGuiTableTempData* temp_data = table->TempData = &g.TablesTempData[g.TablesTempDataStacked - 1]; + temp_data->TableIndex = table_idx; + table->DrawSplitter = &table->TempData->DrawSplitter; + table->DrawSplitter->Clear(); + + // Fix flags + table->IsDefaultSizingPolicy = (flags & ImGuiTableFlags_SizingMask_) == 0; + flags = TableFixFlags(flags, outer_window); + + // Initialize + const int previous_frame_active = table->LastFrameActive; + const int instance_no = (previous_frame_active != g.FrameCount) ? 0 : table->InstanceCurrent + 1; + const ImGuiTableFlags previous_flags = table->Flags; + table->ID = id; + table->Flags = flags; + table->LastFrameActive = g.FrameCount; + table->OuterWindow = table->InnerWindow = outer_window; + table->ColumnsCount = columns_count; + table->IsLayoutLocked = false; + table->InnerWidth = inner_width; + table->NavLayer = (ImS8)outer_window->DC.NavLayerCurrent; + temp_data->UserOuterSize = outer_size; + + // Instance data (for instance 0, TableID == TableInstanceID) + ImGuiID instance_id; + table->InstanceCurrent = (ImS16)instance_no; + if (instance_no > 0) + { + IM_ASSERT(table->ColumnsCount == columns_count && "BeginTable(): Cannot change columns count mid-frame while preserving same ID"); + if (table->InstanceDataExtra.Size < instance_no) + table->InstanceDataExtra.push_back(ImGuiTableInstanceData()); + instance_id = GetIDWithSeed(instance_no, GetIDWithSeed("##Instances", NULL, id)); // Push "##Instances" followed by (int)instance_no in ID stack. + } + else + { + instance_id = id; + } + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + table_instance->TableInstanceID = instance_id; + + // When not using a child window, WorkRect.Max will grow as we append contents. + if (use_child_window) + { + // Ensure no vertical scrollbar appears if we only want horizontal one, to make flag consistent + // (we have no other way to disable vertical scrollbar of a window while keeping the horizontal one showing) + ImVec2 override_content_size(FLT_MAX, FLT_MAX); + if ((flags & ImGuiTableFlags_ScrollX) && !(flags & ImGuiTableFlags_ScrollY)) + override_content_size.y = FLT_MIN; + + // Ensure specified width (when not specified, Stretched columns will act as if the width == OuterWidth and + // never lead to any scrolling). We don't handle inner_width < 0.0f, we could potentially use it to right-align + // based on the right side of the child window work rect, which would require knowing ahead if we are going to + // have decoration taking horizontal spaces (typically a vertical scrollbar). + if ((flags & ImGuiTableFlags_ScrollX) && inner_width > 0.0f) + override_content_size.x = inner_width; + + if (override_content_size.x != FLT_MAX || override_content_size.y != FLT_MAX) + SetNextWindowContentSize(ImVec2(override_content_size.x != FLT_MAX ? override_content_size.x : 0.0f, override_content_size.y != FLT_MAX ? override_content_size.y : 0.0f)); + + // Reset scroll if we are reactivating it + if ((previous_flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) == 0) + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasScroll) == 0) + SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + // Create scrolling region (without border and zero window padding) + ImGuiChildFlags child_child_flags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : ImGuiChildFlags_None; + ImGuiWindowFlags child_window_flags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasWindowFlags) ? g.NextWindowData.WindowFlags : ImGuiWindowFlags_None; + if (flags & ImGuiTableFlags_ScrollX) + child_window_flags |= ImGuiWindowFlags_HorizontalScrollbar; + BeginChildEx(name, instance_id, outer_rect.GetSize(), child_child_flags, child_window_flags); + table->InnerWindow = g.CurrentWindow; + table->WorkRect = table->InnerWindow->WorkRect; + table->OuterRect = table->InnerWindow->Rect(); + table->InnerRect = table->InnerWindow->InnerRect; + IM_ASSERT(table->InnerWindow->WindowPadding.x == 0.0f && table->InnerWindow->WindowPadding.y == 0.0f && table->InnerWindow->WindowBorderSize == 0.0f); + + // Allow submitting when host is measuring + if (table->InnerWindow->SkipItems && outer_window_is_measuring_size) + table->InnerWindow->SkipItems = false; + + // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned) + if (instance_no == 0) + { + table->HasScrollbarYPrev = table->HasScrollbarYCurr; + table->HasScrollbarYCurr = false; + } + table->HasScrollbarYCurr |= table->InnerWindow->ScrollbarY; + } + else + { + // For non-scrolling tables, WorkRect == OuterRect == InnerRect. + // But at this point we do NOT have a correct value for .Max.y (unless a height has been explicitly passed in). It will only be updated in EndTable(). + table->WorkRect = table->OuterRect = table->InnerRect = outer_rect; + table->HasScrollbarYPrev = table->HasScrollbarYCurr = false; + } + + // Push a standardized ID for both child-using and not-child-using tables + PushOverrideID(id); + if (instance_no > 0) + PushOverrideID(instance_id); // FIXME: Somehow this is not resolved by stack-tool, even tho GetIDWithSeed() submitted the symbol. + + // Backup a copy of host window members we will modify + ImGuiWindow* inner_window = table->InnerWindow; + table->HostIndentX = inner_window->DC.Indent.x; + table->HostClipRect = inner_window->ClipRect; + table->HostSkipItems = inner_window->SkipItems; + temp_data->HostBackupWorkRect = inner_window->WorkRect; + temp_data->HostBackupParentWorkRect = inner_window->ParentWorkRect; + temp_data->HostBackupColumnsOffset = outer_window->DC.ColumnsOffset; + temp_data->HostBackupPrevLineSize = inner_window->DC.PrevLineSize; + temp_data->HostBackupCurrLineSize = inner_window->DC.CurrLineSize; + temp_data->HostBackupCursorMaxPos = inner_window->DC.CursorMaxPos; + temp_data->HostBackupItemWidth = outer_window->DC.ItemWidth; + temp_data->HostBackupItemWidthStackSize = outer_window->DC.ItemWidthStack.Size; + inner_window->DC.PrevLineSize = inner_window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + + // Make borders not overlap our contents by offsetting HostClipRect (#6765, #7428, #3752) + // (we normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the + // limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap + // problem only affect scrolling tables in this case we can get away with doing it without extra cost). + if (inner_window != outer_window) + { + // FIXME: Because inner_window's Scrollbar doesn't know about border size, since it's not encoded in window->WindowBorderSize, + // it already overlaps it and doesn't need an extra offset. Ideally we should be able to pass custom border size with + // different x/y values to BeginChild(). + if (flags & ImGuiTableFlags_BordersOuterV) + { + table->HostClipRect.Min.x = ImMin(table->HostClipRect.Min.x + TABLE_BORDER_SIZE, table->HostClipRect.Max.x); + if (inner_window->DecoOuterSizeX2 == 0.0f) + table->HostClipRect.Max.x = ImMax(table->HostClipRect.Max.x - TABLE_BORDER_SIZE, table->HostClipRect.Min.x); + } + if (flags & ImGuiTableFlags_BordersOuterH) + { + table->HostClipRect.Min.y = ImMin(table->HostClipRect.Min.y + TABLE_BORDER_SIZE, table->HostClipRect.Max.y); + if (inner_window->DecoOuterSizeY2 == 0.0f) + table->HostClipRect.Max.y = ImMax(table->HostClipRect.Max.y - TABLE_BORDER_SIZE, table->HostClipRect.Min.y); + } + } + + // Padding and Spacing + // - None ........Content..... Pad .....Content........ + // - PadOuter | Pad ..Content..... Pad .....Content.. Pad | + // - PadInner ........Content.. Pad | Pad ..Content........ + // - PadOuter+PadInner | Pad ..Content.. Pad | Pad ..Content.. Pad | + const bool pad_outer_x = (flags & ImGuiTableFlags_NoPadOuterX) ? false : (flags & ImGuiTableFlags_PadOuterX) ? true : (flags & ImGuiTableFlags_BordersOuterV) != 0; + const bool pad_inner_x = (flags & ImGuiTableFlags_NoPadInnerX) ? false : true; + const float inner_spacing_for_border = (flags & ImGuiTableFlags_BordersInnerV) ? TABLE_BORDER_SIZE : 0.0f; + const float inner_spacing_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) == 0) ? g.Style.CellPadding.x : 0.0f; + const float inner_padding_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) != 0) ? g.Style.CellPadding.x : 0.0f; + table->CellSpacingX1 = inner_spacing_explicit + inner_spacing_for_border; + table->CellSpacingX2 = inner_spacing_explicit; + table->CellPaddingX = inner_padding_explicit; + + const float outer_padding_for_border = (flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + const float outer_padding_explicit = pad_outer_x ? g.Style.CellPadding.x : 0.0f; + table->OuterPaddingX = (outer_padding_for_border + outer_padding_explicit) - table->CellPaddingX; + + table->CurrentColumn = -1; + table->CurrentRow = -1; + table->RowBgColorCounter = 0; + table->LastRowFlags = ImGuiTableRowFlags_None; + table->InnerClipRect = (inner_window == outer_window) ? table->WorkRect : inner_window->ClipRect; + table->InnerClipRect.ClipWith(table->WorkRect); // We need this to honor inner_width + table->InnerClipRect.ClipWithFull(table->HostClipRect); + table->InnerClipRect.Max.y = (flags & ImGuiTableFlags_NoHostExtendY) ? ImMin(table->InnerClipRect.Max.y, inner_window->WorkRect.Max.y) : table->HostClipRect.Max.y; + + table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow + table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow() + table->RowCellPaddingY = 0.0f; + table->FreezeRowsRequest = table->FreezeRowsCount = 0; // This will be setup by TableSetupScrollFreeze(), if any + table->FreezeColumnsRequest = table->FreezeColumnsCount = 0; + table->IsUnfrozenRows = true; + table->DeclColumnsCount = table->AngledHeadersCount = 0; + if (previous_frame_active + 1 < g.FrameCount) + table->IsActiveIdInTable = false; + table->AngledHeadersHeight = 0.0f; + temp_data->AngledHeadersExtraWidth = 0.0f; + + // Using opaque colors facilitate overlapping lines of the grid, otherwise we'd need to improve TableDrawBorders() + table->BorderColorStrong = GetColorU32(ImGuiCol_TableBorderStrong); + table->BorderColorLight = GetColorU32(ImGuiCol_TableBorderLight); + + // Make table current + g.CurrentTable = table; + outer_window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX(); + outer_window->DC.CurrentTableIdx = table_idx; + if (inner_window != outer_window) // So EndChild() within the inner window can restore the table properly. + inner_window->DC.CurrentTableIdx = table_idx; + + if ((previous_flags & ImGuiTableFlags_Reorderable) && (flags & ImGuiTableFlags_Reorderable) == 0) + table->IsResetDisplayOrderRequest = true; + + // Mark as used to avoid GC + if (table_idx >= g.TablesLastTimeActive.Size) + g.TablesLastTimeActive.resize(table_idx + 1, -1.0f); + g.TablesLastTimeActive[table_idx] = (float)g.Time; + temp_data->LastTimeActive = (float)g.Time; + table->MemoryCompacted = false; + + // Setup memory buffer (clear data if columns count changed) + ImGuiTableColumn* old_columns_to_preserve = NULL; + void* old_columns_raw_data = NULL; + const int old_columns_count = table->Columns.size(); + if (old_columns_count != 0 && old_columns_count != columns_count) + { + // Attempt to preserve width on column count change (#4046) + old_columns_to_preserve = table->Columns.Data; + old_columns_raw_data = table->RawData; + table->RawData = NULL; + } + if (table->RawData == NULL) + { + TableBeginInitMemory(table, columns_count); + table->IsInitializing = table->IsSettingsRequestLoad = true; + } + if (table->IsResetAllRequest) + TableResetSettings(table); + if (table->IsInitializing) + { + // Initialize + table->SettingsOffset = -1; + table->IsSortSpecsDirty = true; + table->IsSettingsDirty = true; // Records itself into .ini file even when in default state (#7934) + table->InstanceInteracted = -1; + table->ContextPopupColumn = -1; + table->ReorderColumn = table->ResizedColumn = table->LastResizedColumn = -1; + table->AutoFitSingleColumn = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; + for (int n = 0; n < columns_count; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + if (old_columns_to_preserve && n < old_columns_count) + { + // FIXME: We don't attempt to preserve column order in this path. + *column = old_columns_to_preserve[n]; + } + else + { + float width_auto = column->WidthAuto; + *column = ImGuiTableColumn(); + column->WidthAuto = width_auto; + column->IsPreserveWidthAuto = true; // Preserve WidthAuto when reinitializing a live table: not technically necessary but remove a visible flicker + column->IsEnabled = column->IsUserEnabled = column->IsUserEnabledNextFrame = true; + } + column->DisplayOrder = table->DisplayOrderToIndex[n] = (ImGuiTableColumnIdx)n; + } + } + if (old_columns_raw_data) + IM_FREE(old_columns_raw_data); + + // Load settings + if (table->IsSettingsRequestLoad) + TableLoadSettings(table); + + // Handle DPI/font resize + // This is designed to facilitate DPI changes with the assumption that e.g. style.CellPadding has been scaled as well. + // It will also react to changing fonts with mixed results. It doesn't need to be perfect but merely provide a decent transition. + // FIXME-DPI: Provide consistent standards for reference size. Perhaps using g.CurrentDpiScale would be more self explanatory. + // This is will lead us to non-rounded WidthRequest in columns, which should work but is a poorly tested path. + const float new_ref_scale_unit = g.FontSize; // g.Font->GetCharAdvance('A') ? + if (table->RefScale != 0.0f && table->RefScale != new_ref_scale_unit) + { + const float scale_factor = new_ref_scale_unit / table->RefScale; + //IMGUI_DEBUG_PRINT("[table] %08X RefScaleUnit %.3f -> %.3f, scaling width by %.3f\n", table->ID, table->RefScaleUnit, new_ref_scale_unit, scale_factor); + for (int n = 0; n < columns_count; n++) + table->Columns[n].WidthRequest = table->Columns[n].WidthRequest * scale_factor; + } + table->RefScale = new_ref_scale_unit; + + // Disable output until user calls TableNextRow() or TableNextColumn() leading to the TableUpdateLayout() call.. + // This is not strictly necessary but will reduce cases were "out of table" output will be misleading to the user. + // Because we cannot safely assert in EndTable() when no rows have been created, this seems like our best option. + inner_window->SkipItems = true; + + // Clear names + // At this point the ->NameOffset field of each column will be invalid until TableUpdateLayout() or the first call to TableSetupColumn() + if (table->ColumnsNames.Buf.Size > 0) + table->ColumnsNames.Buf.resize(0); + + // Apply queued resizing/reordering/hiding requests + TableBeginApplyRequests(table); + + return true; +} + +// For reference, the average total _allocation count_ for a table is: +// + 0 (for ImGuiTable instance, we are pooling allocations in g.Tables[]) +// + 1 (for table->RawData allocated below) +// + 1 (for table->ColumnsNames, if names are used) +// Shared allocations for the maximum number of simultaneously nested tables (generally a very small number) +// + 1 (for table->Splitter._Channels) +// + 2 * active_channels_count (for ImDrawCmd and ImDrawIdx buffers inside channels) +// Where active_channels_count is variable but often == columns_count or == columns_count + 1, see TableSetupDrawChannels() for details. +// Unused channels don't perform their +2 allocations. +void ImGui::TableBeginInitMemory(ImGuiTable* table, int columns_count) +{ + // Allocate single buffer for our arrays + const int columns_bit_array_size = (int)ImBitArrayGetStorageSizeInBytes(columns_count); + ImSpanAllocator<6> span_allocator; + span_allocator.Reserve(0, columns_count * sizeof(ImGuiTableColumn)); + span_allocator.Reserve(1, columns_count * sizeof(ImGuiTableColumnIdx)); + span_allocator.Reserve(2, columns_count * sizeof(ImGuiTableCellData), 4); + for (int n = 3; n < 6; n++) + span_allocator.Reserve(n, columns_bit_array_size); + table->RawData = IM_ALLOC(span_allocator.GetArenaSizeInBytes()); + memset(table->RawData, 0, span_allocator.GetArenaSizeInBytes()); + span_allocator.SetArenaBasePtr(table->RawData); + span_allocator.GetSpan(0, &table->Columns); + span_allocator.GetSpan(1, &table->DisplayOrderToIndex); + span_allocator.GetSpan(2, &table->RowCellData); + table->EnabledMaskByDisplayOrder = (ImU32*)span_allocator.GetSpanPtrBegin(3); + table->EnabledMaskByIndex = (ImU32*)span_allocator.GetSpanPtrBegin(4); + table->VisibleMaskByIndex = (ImU32*)span_allocator.GetSpanPtrBegin(5); +} + +// Apply queued resizing/reordering/hiding requests +void ImGui::TableBeginApplyRequests(ImGuiTable* table) +{ + // Handle resizing request + // (We process this in the TableBegin() of the first instance of each table) + // FIXME-TABLE: Contains columns if our work area doesn't allow for scrolling? + if (table->InstanceCurrent == 0) + { + if (table->ResizedColumn != -1 && table->ResizedColumnNextWidth != FLT_MAX) + TableSetColumnWidth(table->ResizedColumn, table->ResizedColumnNextWidth); + table->LastResizedColumn = table->ResizedColumn; + table->ResizedColumnNextWidth = FLT_MAX; + table->ResizedColumn = -1; + + // Process auto-fit for single column, which is a special case for stretch columns and fixed columns with FixedSame policy. + // FIXME-TABLE: Would be nice to redistribute available stretch space accordingly to other weights, instead of giving it all to siblings. + if (table->AutoFitSingleColumn != -1) + { + TableSetColumnWidth(table->AutoFitSingleColumn, table->Columns[table->AutoFitSingleColumn].WidthAuto); + table->AutoFitSingleColumn = -1; + } + } + + // Handle reordering request + // Note: we don't clear ReorderColumn after handling the request. + if (table->InstanceCurrent == 0) + { + if (table->HeldHeaderColumn == -1 && table->ReorderColumn != -1) + table->ReorderColumn = -1; + table->HeldHeaderColumn = -1; + if (table->ReorderColumn != -1 && table->ReorderColumnDir != 0) + { + // We need to handle reordering across hidden columns. + // In the configuration below, moving C to the right of E will lead to: + // ... C [D] E ---> ... [D] E C (Column name/index) + // ... 2 3 4 ... 2 3 4 (Display order) + const int reorder_dir = table->ReorderColumnDir; + IM_ASSERT(reorder_dir == -1 || reorder_dir == +1); + IM_ASSERT(table->Flags & ImGuiTableFlags_Reorderable); + ImGuiTableColumn* src_column = &table->Columns[table->ReorderColumn]; + ImGuiTableColumn* dst_column = &table->Columns[(reorder_dir == -1) ? src_column->PrevEnabledColumn : src_column->NextEnabledColumn]; + IM_UNUSED(dst_column); + const int src_order = src_column->DisplayOrder; + const int dst_order = dst_column->DisplayOrder; + src_column->DisplayOrder = (ImGuiTableColumnIdx)dst_order; + for (int order_n = src_order + reorder_dir; order_n != dst_order + reorder_dir; order_n += reorder_dir) + table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder -= (ImGuiTableColumnIdx)reorder_dir; + IM_ASSERT(dst_column->DisplayOrder == dst_order - reorder_dir); + + // Display order is stored in both columns->IndexDisplayOrder and table->DisplayOrder[]. Rebuild later from the former. + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; + table->ReorderColumnDir = 0; + table->IsSettingsDirty = true; + } + } + + // Handle display order reset request + if (table->IsResetDisplayOrderRequest) + { + for (int n = 0; n < table->ColumnsCount; n++) + table->DisplayOrderToIndex[n] = table->Columns[n].DisplayOrder = (ImGuiTableColumnIdx)n; + table->IsResetDisplayOrderRequest = false; + table->IsSettingsDirty = true; + } +} + +// Adjust flags: default width mode + stretch columns are not allowed when auto extending +static void TableSetupColumnFlags(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags flags_in) +{ + ImGuiTableColumnFlags flags = flags_in; + + // Sizing Policy + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0) + { + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + if (table_sizing_policy == ImGuiTableFlags_SizingFixedFit || table_sizing_policy == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + else + flags |= ImGuiTableColumnFlags_WidthStretch; + } + else + { + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_WidthMask_)); // Check that only 1 of each set is used. + } + + // Resize + if ((table->Flags & ImGuiTableFlags_Resizable) == 0) + flags |= ImGuiTableColumnFlags_NoResize; + + // Sorting + if ((flags & ImGuiTableColumnFlags_NoSortAscending) && (flags & ImGuiTableColumnFlags_NoSortDescending)) + flags |= ImGuiTableColumnFlags_NoSort; + + // Indentation + if ((flags & ImGuiTableColumnFlags_IndentMask_) == 0) + flags |= (table->Columns.index_from_ptr(column) == 0) ? ImGuiTableColumnFlags_IndentEnable : ImGuiTableColumnFlags_IndentDisable; + + // Alignment + //if ((flags & ImGuiTableColumnFlags_AlignMask_) == 0) + // flags |= ImGuiTableColumnFlags_AlignCenter; + //IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_AlignMask_)); // Check that only 1 of each set is used. + + // Preserve status flags + column->Flags = flags | (column->Flags & ImGuiTableColumnFlags_StatusMask_); + + // Build an ordered list of available sort directions + column->SortDirectionsAvailCount = column->SortDirectionsAvailMask = column->SortDirectionsAvailList = 0; + if (table->Flags & ImGuiTableFlags_Sortable) + { + int count = 0, mask = 0, list = 0; + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) != 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) != 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) == 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) == 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((table->Flags & ImGuiTableFlags_SortTristate) || count == 0) { mask |= 1 << ImGuiSortDirection_None; count++; } + column->SortDirectionsAvailList = (ImU8)list; + column->SortDirectionsAvailMask = (ImU8)mask; + column->SortDirectionsAvailCount = (ImU8)count; + ImGui::TableFixColumnSortDirection(table, column); + } +} + +// Layout columns for the frame. This is in essence the followup to BeginTable() and this is our largest function. +// Runs on the first call to TableNextRow(), to give a chance for TableSetupColumn() and other TableSetupXXXXX() functions to be called first. +// FIXME-TABLE: Our width (and therefore our WorkRect) will be minimal in the first frame for _WidthAuto columns. +// Increase feedback side-effect with widgets relying on WorkRect.Max.x... Maybe provide a default distribution for _WidthAuto columns? +void ImGui::TableUpdateLayout(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->IsLayoutLocked == false); + + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + table->IsDefaultDisplayOrder = true; + table->ColumnsEnabledCount = 0; + ImBitArrayClearAllBits(table->EnabledMaskByIndex, table->ColumnsCount); + ImBitArrayClearAllBits(table->EnabledMaskByDisplayOrder, table->ColumnsCount); + table->LeftMostEnabledColumn = -1; + table->MinColumnWidth = ImMax(1.0f, g.Style.FramePadding.x * 1.0f); // g.Style.ColumnsMinSpacing; // FIXME-TABLE + + // [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns. + // Process columns in their visible orders as we are building the Prev/Next indices. + int count_fixed = 0; // Number of columns that have fixed sizing policies + int count_stretch = 0; // Number of columns that have stretch sizing policies + int prev_visible_column_idx = -1; + bool has_auto_fit_request = false; + bool has_resizable = false; + float stretch_sum_width_auto = 0.0f; + float fixed_max_width_auto = 0.0f; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + if (column_n != order_n) + table->IsDefaultDisplayOrder = false; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Clear column setup if not submitted by user. Currently we make it mandatory to call TableSetupColumn() every frame. + // It would easily work without but we're not ready to guarantee it since e.g. names need resubmission anyway. + // We take a slight shortcut but in theory we could be calling TableSetupColumn() here with dummy values, it should yield the same effect. + if (table->DeclColumnsCount <= column_n) + { + TableSetupColumnFlags(table, column, ImGuiTableColumnFlags_None); + column->NameOffset = -1; + column->UserID = 0; + column->InitStretchWeightOrWidth = -1.0f; + } + + // Update Enabled state, mark settings and sort specs dirty + if (!(table->Flags & ImGuiTableFlags_Hideable) || (column->Flags & ImGuiTableColumnFlags_NoHide)) + column->IsUserEnabledNextFrame = true; + if (column->IsUserEnabled != column->IsUserEnabledNextFrame) + { + column->IsUserEnabled = column->IsUserEnabledNextFrame; + table->IsSettingsDirty = true; + } + column->IsEnabled = column->IsUserEnabled && (column->Flags & ImGuiTableColumnFlags_Disabled) == 0; + + if (column->SortOrder != -1 && !column->IsEnabled) + table->IsSortSpecsDirty = true; + if (column->SortOrder > 0 && !(table->Flags & ImGuiTableFlags_SortMulti)) + table->IsSortSpecsDirty = true; + + // Auto-fit unsized columns + const bool start_auto_fit = (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? (column->WidthRequest < 0.0f) : (column->StretchWeight < 0.0f); + if (start_auto_fit) + column->AutoFitQueue = column->CannotSkipItemsQueue = (1 << 3) - 1; // Fit for three frames + + if (!column->IsEnabled) + { + column->IndexWithinEnabledSet = -1; + continue; + } + + // Mark as enabled and link to previous/next enabled column + column->PrevEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + column->NextEnabledColumn = -1; + if (prev_visible_column_idx != -1) + table->Columns[prev_visible_column_idx].NextEnabledColumn = (ImGuiTableColumnIdx)column_n; + else + table->LeftMostEnabledColumn = (ImGuiTableColumnIdx)column_n; + column->IndexWithinEnabledSet = table->ColumnsEnabledCount++; + ImBitArraySetBit(table->EnabledMaskByIndex, column_n); + ImBitArraySetBit(table->EnabledMaskByDisplayOrder, column->DisplayOrder); + prev_visible_column_idx = column_n; + IM_ASSERT(column->IndexWithinEnabledSet <= column->DisplayOrder); + + // Calculate ideal/auto column width (that's the width required for all contents to be visible without clipping) + // Combine width from regular rows + width from headers unless requested not to. + if (!column->IsPreserveWidthAuto && table->InstanceCurrent == 0) + column->WidthAuto = TableGetColumnWidthAuto(table, column); + + // Non-resizable columns keep their requested width (apply user value regardless of IsPreserveWidthAuto) + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column_is_resizable) + has_resizable = true; + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f && !column_is_resizable) + column->WidthAuto = column->InitStretchWeightOrWidth; + + if (column->AutoFitQueue != 0x00) + has_auto_fit_request = true; + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + stretch_sum_width_auto += column->WidthAuto; + count_stretch++; + } + else + { + fixed_max_width_auto = ImMax(fixed_max_width_auto, column->WidthAuto); + count_fixed++; + } + } + if ((table->Flags & ImGuiTableFlags_Sortable) && table->SortSpecsCount == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + table->IsSortSpecsDirty = true; + table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); + + // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid + // the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). Also see #6510. + // FIXME-TABLE: for always auto-resizing columns may not want to do that all the time. + if (has_auto_fit_request && table->OuterWindow != table->InnerWindow) + table->InnerWindow->SkipItems = false; + if (has_auto_fit_request) + table->IsSettingsDirty = true; + + // [Part 3] Fix column flags and record a few extra information. + float sum_width_requests = 0.0f; // Sum of all width for fixed and auto-resize columns, excluding width contributed by Stretch columns but including spacing/padding. + float stretch_sum_weights = 0.0f; // Sum of all weights for stretch columns. + table->LeftMostStretchedColumn = table->RightMostStretchedColumn = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column->Flags & ImGuiTableColumnFlags_WidthFixed) + { + // Apply same widths policy + float width_auto = column->WidthAuto; + if (table_sizing_policy == ImGuiTableFlags_SizingFixedSame && (column->AutoFitQueue != 0x00 || !column_is_resizable)) + width_auto = fixed_max_width_auto; + + // Apply automatic width + // Latch initial size for fixed columns and update it constantly for auto-resizing column (unless clipped!) + if (column->AutoFitQueue != 0x00) + column->WidthRequest = width_auto; + else if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !column_is_resizable && column->IsRequestOutput) + column->WidthRequest = width_auto; + + // FIXME-TABLE: Increase minimum size during init frame to avoid biasing auto-fitting widgets + // (e.g. TextWrapped) too much. Otherwise what tends to happen is that TextWrapped would output a very + // large height (= first frame scrollbar display very off + clipper would skip lots of items). + // This is merely making the side-effect less extreme, but doesn't properly fixes it. + // FIXME: Move this to ->WidthGiven to avoid temporary lossyless? + // FIXME: This break IsPreserveWidthAuto from not flickering if the stored WidthAuto was smaller. + if (column->AutoFitQueue > 0x01 && table->IsInitializing && !column->IsPreserveWidthAuto) + column->WidthRequest = ImMax(column->WidthRequest, table->MinColumnWidth * 4.0f); // FIXME-TABLE: Another constant/scale? + sum_width_requests += column->WidthRequest; + } + else + { + // Initialize stretch weight + if (column->AutoFitQueue != 0x00 || column->StretchWeight < 0.0f || !column_is_resizable) + { + if (column->InitStretchWeightOrWidth > 0.0f) + column->StretchWeight = column->InitStretchWeightOrWidth; + else if (table_sizing_policy == ImGuiTableFlags_SizingStretchProp) + column->StretchWeight = (column->WidthAuto / stretch_sum_width_auto) * count_stretch; + else + column->StretchWeight = 1.0f; + } + + stretch_sum_weights += column->StretchWeight; + if (table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder > column->DisplayOrder) + table->LeftMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + if (table->RightMostStretchedColumn == -1 || table->Columns[table->RightMostStretchedColumn].DisplayOrder < column->DisplayOrder) + table->RightMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + } + column->IsPreserveWidthAuto = false; + sum_width_requests += table->CellPaddingX * 2.0f; + } + table->ColumnsEnabledFixedCount = (ImGuiTableColumnIdx)count_fixed; + table->ColumnsStretchSumWeights = stretch_sum_weights; + + // [Part 4] Apply final widths based on requested widths + const ImRect work_rect = table->WorkRect; + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + const float width_removed = (table->HasScrollbarYPrev && !table->InnerWindow->ScrollbarY) ? g.Style.ScrollbarSize : 0.0f; // To synchronize decoration width of synced tables with mismatching scrollbar state (#5920) + const float width_avail = ImMax(1.0f, (((table->Flags & ImGuiTableFlags_ScrollX) && table->InnerWidth == 0.0f) ? table->InnerClipRect.GetWidth() : work_rect.GetWidth()) - width_removed); + const float width_avail_for_stretched_columns = width_avail - width_spacings - sum_width_requests; + float width_remaining_for_stretched_columns = width_avail_for_stretched_columns; + table->ColumnsGivenWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Allocate width for stretched/weighted columns (StretchWeight gets converted into WidthRequest) + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + float weight_ratio = column->StretchWeight / stretch_sum_weights; + column->WidthRequest = IM_TRUNC(ImMax(width_avail_for_stretched_columns * weight_ratio, table->MinColumnWidth) + 0.01f); + width_remaining_for_stretched_columns -= column->WidthRequest; + } + + // [Resize Rule 1] The right-most Visible column is not resizable if there is at least one Stretch column + // See additional comments in TableSetColumnWidth(). + if (column->NextEnabledColumn == -1 && table->LeftMostStretchedColumn != -1) + column->Flags |= ImGuiTableColumnFlags_NoDirectResize_; + + // Assign final width, record width in case we will need to shrink + column->WidthGiven = ImTrunc(ImMax(column->WidthRequest, table->MinColumnWidth)); + table->ColumnsGivenWidth += column->WidthGiven; + } + + // [Part 5] Redistribute stretch remainder width due to rounding (remainder width is < 1.0f * number of Stretch column). + // Using right-to-left distribution (more likely to match resizing cursor). + if (width_remaining_for_stretched_columns >= 1.0f && !(table->Flags & ImGuiTableFlags_PreciseWidths)) + for (int order_n = table->ColumnsCount - 1; stretch_sum_weights > 0.0f && width_remaining_for_stretched_columns >= 1.0f && order_n >= 0; order_n--) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + ImGuiTableColumn* column = &table->Columns[table->DisplayOrderToIndex[order_n]]; + if (!(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->WidthRequest += 1.0f; + column->WidthGiven += 1.0f; + width_remaining_for_stretched_columns -= 1.0f; + } + + // Determine if table is hovered which will be used to flag columns as hovered. + // - In principle we'd like to use the equivalent of IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), + // but because our item is partially submitted at this point we use ItemHoverable() and a workaround (temporarily + // clear ActiveId, which is equivalent to the change provided by _AllowWhenBLockedByActiveItem). + // - This allows columns to be marked as hovered when e.g. clicking a button inside the column, or using drag and drop. + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + table_instance->HoveredRowLast = table_instance->HoveredRowNext; + table_instance->HoveredRowNext = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; + const ImRect mouse_hit_rect(table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.Max.x, ImMax(table->OuterRect.Max.y, table->OuterRect.Min.y + table_instance->LastOuterHeight)); + const ImGuiID backup_active_id = g.ActiveId; + g.ActiveId = 0; + const bool is_hovering_table = ItemHoverable(mouse_hit_rect, 0, ImGuiItemFlags_None); + g.ActiveId = backup_active_id; + + // Determine skewed MousePos.x to support angled headers. + float mouse_skewed_x = g.IO.MousePos.x; + if (table->AngledHeadersHeight > 0.0f) + if (g.IO.MousePos.y >= table->OuterRect.Min.y && g.IO.MousePos.y <= table->OuterRect.Min.y + table->AngledHeadersHeight) + mouse_skewed_x += ImTrunc((table->OuterRect.Min.y + table->AngledHeadersHeight - g.IO.MousePos.y) * table->AngledHeadersSlope); + + // [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column + // Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping. + int visible_n = 0; + bool has_at_least_one_column_requesting_output = false; + bool offset_x_frozen = (table->FreezeColumnsCount > 0); + float offset_x = ((table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x) + table->OuterPaddingX - table->CellSpacingX1; + ImRect host_clip_rect = table->InnerClipRect; + //host_clip_rect.Max.x += table->CellPaddingX + table->CellSpacingX2; + ImBitArrayClearAllBits(table->VisibleMaskByIndex, table->ColumnsCount); + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Initial nav layer: using FreezeRowsCount, NOT FreezeRowsRequest, so Header line changes layer when frozen + column->NavLayerCurrent = (ImS8)(table->FreezeRowsCount > 0 ? ImGuiNavLayer_Menu : (ImGuiNavLayer)table->NavLayer); + + if (offset_x_frozen && table->FreezeColumnsCount == visible_n) + { + offset_x += work_rect.Min.x - table->OuterRect.Min.x; + offset_x_frozen = false; + } + + // Clear status flags + column->Flags &= ~ImGuiTableColumnFlags_StatusMask_; + + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + { + // Hidden column: clear a few fields and we are done with it for the remainder of the function. + // We set a zero-width clip rect but set Min.y/Max.y properly to not interfere with the clipper. + column->MinX = column->MaxX = column->WorkMinX = column->ClipRect.Min.x = column->ClipRect.Max.x = offset_x; + column->WidthGiven = 0.0f; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + column->IsVisibleX = column->IsVisibleY = column->IsRequestOutput = false; + column->IsSkipItems = true; + column->ItemWidth = 1.0f; + continue; + } + + // Lock start position + column->MinX = offset_x; + + // Lock width based on start position and minimum/maximum width for this position + column->WidthMax = TableCalcMaxColumnWidth(table, column_n); + column->WidthGiven = ImMin(column->WidthGiven, column->WidthMax); + column->WidthGiven = ImMax(column->WidthGiven, ImMin(column->WidthRequest, table->MinColumnWidth)); + column->MaxX = offset_x + column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + + // Lock other positions + // - ClipRect.Min.x: Because merging draw commands doesn't compare min boundaries, we make ClipRect.Min.x match left bounds to be consistent regardless of merging. + // - ClipRect.Max.x: using WorkMaxX instead of MaxX (aka including padding) makes things more consistent when resizing down, tho slightly detrimental to visibility in very-small column. + // - ClipRect.Max.x: using MaxX makes it easier for header to receive hover highlight with no discontinuity and display sorting arrow. + // - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter. + const float previous_instance_work_min_x = column->WorkMinX; + column->WorkMinX = column->MinX + table->CellPaddingX + table->CellSpacingX1; + column->WorkMaxX = column->MaxX - table->CellPaddingX - table->CellSpacingX2; // Expected max + column->ItemWidth = ImTrunc(column->WidthGiven * 0.65f); + column->ClipRect.Min.x = column->MinX; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.x = column->MaxX; //column->WorkMaxX; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + + // Mark column as Clipped (not in sight) + // Note that scrolling tables (where inner_window != outer_window) handle Y clipped earlier in BeginTable() so IsVisibleY really only applies to non-scrolling tables. + // FIXME-TABLE: Because InnerClipRect.Max.y is conservatively ==outer_window->ClipRect.Max.y, we never can mark columns _Above_ the scroll line as not IsVisibleY. + // Taking advantage of LastOuterHeight would yield good results there... + // FIXME-TABLE: Y clipping is disabled because it effectively means not submitting will reduce contents width which is fed to outer_window->DC.CursorMaxPos.x, + // and this may be used (e.g. typically by outer_window using AlwaysAutoResize or outer_window's horizontal scrollbar, but could be something else). + // Possible solution to preserve last known content width for clipped column. Test 'table_reported_size' fails when enabling Y clipping and window is resized small. + column->IsVisibleX = (column->ClipRect.Max.x > column->ClipRect.Min.x); + column->IsVisibleY = true; // (column->ClipRect.Max.y > column->ClipRect.Min.y); + const bool is_visible = column->IsVisibleX; //&& column->IsVisibleY; + if (is_visible) + ImBitArraySetBit(table->VisibleMaskByIndex, column_n); + + // Mark column as requesting output from user. Note that fixed + non-resizable sets are auto-fitting at all times and therefore always request output. + column->IsRequestOutput = is_visible || column->AutoFitQueue != 0 || column->CannotSkipItemsQueue != 0; + + // Mark column as SkipItems (ignoring all items/layout) + // (table->HostSkipItems is a copy of inner_window->SkipItems before we cleared it above in Part 2) + column->IsSkipItems = !column->IsEnabled || table->HostSkipItems; + if (column->IsSkipItems) + IM_ASSERT(!is_visible); + if (column->IsRequestOutput && !column->IsSkipItems) + has_at_least_one_column_requesting_output = true; + + // Update status flags + column->Flags |= ImGuiTableColumnFlags_IsEnabled; + if (is_visible) + column->Flags |= ImGuiTableColumnFlags_IsVisible; + if (column->SortOrder != -1) + column->Flags |= ImGuiTableColumnFlags_IsSorted; + + // Detect hovered column + if (is_hovering_table && mouse_skewed_x >= column->ClipRect.Min.x && mouse_skewed_x < column->ClipRect.Max.x) + { + column->Flags |= ImGuiTableColumnFlags_IsHovered; + table->HoveredColumnBody = (ImGuiTableColumnIdx)column_n; + } + + // Alignment + // FIXME-TABLE: This align based on the whole column width, not per-cell, and therefore isn't useful in + // many cases (to be able to honor this we might be able to store a log of cells width, per row, for + // visible rows, but nav/programmatic scroll would have visible artifacts.) + //if (column->Flags & ImGuiTableColumnFlags_AlignRight) + // column->WorkMinX = ImMax(column->WorkMinX, column->MaxX - column->ContentWidthRowsUnfrozen); + //else if (column->Flags & ImGuiTableColumnFlags_AlignCenter) + // column->WorkMinX = ImLerp(column->WorkMinX, ImMax(column->StartX, column->MaxX - column->ContentWidthRowsUnfrozen), 0.5f); + + // Reset content width variables + if (table->InstanceCurrent == 0) + { + column->ContentMaxXFrozen = column->WorkMinX; + column->ContentMaxXUnfrozen = column->WorkMinX; + column->ContentMaxXHeadersUsed = column->WorkMinX; + column->ContentMaxXHeadersIdeal = column->WorkMinX; + } + else + { + // As we store an absolute value to make per-cell updates faster, we need to offset values used for width computation. + const float offset_from_previous_instance = column->WorkMinX - previous_instance_work_min_x; + column->ContentMaxXFrozen += offset_from_previous_instance; + column->ContentMaxXUnfrozen += offset_from_previous_instance; + column->ContentMaxXHeadersUsed += offset_from_previous_instance; + column->ContentMaxXHeadersIdeal += offset_from_previous_instance; + } + + // Don't decrement auto-fit counters until container window got a chance to submit its items + if (table->HostSkipItems == false && table->InstanceCurrent == 0) + { + column->AutoFitQueue >>= 1; + column->CannotSkipItemsQueue >>= 1; + } + + if (visible_n < table->FreezeColumnsCount) + host_clip_rect.Min.x = ImClamp(column->MaxX + TABLE_BORDER_SIZE, host_clip_rect.Min.x, host_clip_rect.Max.x); + + offset_x += column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + visible_n++; + } + + // In case the table is visible (e.g. decorations) but all columns clipped, we keep a column visible. + // Else if give no chance to a clipper-savy user to submit rows and therefore total contents height used by scrollbar. + if (has_at_least_one_column_requesting_output == false) + { + table->Columns[table->LeftMostEnabledColumn].IsRequestOutput = true; + table->Columns[table->LeftMostEnabledColumn].IsSkipItems = false; + } + + // [Part 7] Detect/store when we are hovering the unused space after the right-most column (so e.g. context menus can react on it) + // Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either + // because of using _WidthAuto/_WidthStretch). This will hide the resizing option from the context menu. + const float unused_x1 = ImMax(table->WorkRect.Min.x, table->Columns[table->RightMostEnabledColumn].ClipRect.Max.x); + if (is_hovering_table && table->HoveredColumnBody == -1) + if (mouse_skewed_x >= unused_x1) + table->HoveredColumnBody = (ImGuiTableColumnIdx)table->ColumnsCount; + if (has_resizable == false && (table->Flags & ImGuiTableFlags_Resizable)) + table->Flags &= ~ImGuiTableFlags_Resizable; + + table->IsActiveIdAliveBeforeTable = (g.ActiveIdIsAlive != 0); + + // [Part 8] Lock actual OuterRect/WorkRect right-most position. + // This is done late to handle the case of fixed-columns tables not claiming more widths that they need. + // Because of this we are careful with uses of WorkRect and InnerClipRect before this point. + if (table->RightMostStretchedColumn != -1) + table->Flags &= ~ImGuiTableFlags_NoHostExtendX; + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + table->OuterRect.Max.x = table->WorkRect.Max.x = unused_x1; + table->InnerClipRect.Max.x = ImMin(table->InnerClipRect.Max.x, unused_x1); + } + table->InnerWindow->ParentWorkRect = table->WorkRect; + table->BorderX1 = table->InnerClipRect.Min.x; + table->BorderX2 = table->InnerClipRect.Max.x; + + // Setup window's WorkRect.Max.y for GetContentRegionAvail(). Other values will be updated in each TableBeginCell() call. + float window_content_max_y; + if (table->Flags & ImGuiTableFlags_NoHostExtendY) + window_content_max_y = table->OuterRect.Max.y; + else + window_content_max_y = ImMax(table->InnerWindow->ContentRegionRect.Max.y, (table->Flags & ImGuiTableFlags_ScrollY) ? 0.0f : table->OuterRect.Max.y); + table->InnerWindow->WorkRect.Max.y = ImClamp(window_content_max_y - g.Style.CellPadding.y, table->InnerWindow->WorkRect.Min.y, table->InnerWindow->WorkRect.Max.y); + + // [Part 9] Allocate draw channels and setup background cliprect + TableSetupDrawChannels(table); + + // [Part 10] Hit testing on borders + if (table->Flags & ImGuiTableFlags_Resizable) + TableUpdateBorders(table); + table_instance->LastTopHeadersRowHeight = 0.0f; + table->IsLayoutLocked = true; + table->IsUsingHeaders = false; + + // Highlight header + table->HighlightColumnHeader = -1; + if (table->IsContextPopupOpen && table->ContextPopupColumn != -1 && table->InstanceInteracted == table->InstanceCurrent) + table->HighlightColumnHeader = table->ContextPopupColumn; + else if ((table->Flags & ImGuiTableFlags_HighlightHoveredColumn) && table->HoveredColumnBody != -1 && table->HoveredColumnBody != table->ColumnsCount && table->HoveredColumnBorder == -1) + if (g.ActiveId == 0 || (table->IsActiveIdInTable || g.DragDropActive)) + table->HighlightColumnHeader = table->HoveredColumnBody; + + // [Part 11] Default context menu + // - To append to this menu: you can call TableBeginContextMenuPopup()/.../EndPopup(). + // - To modify or replace this: set table->IsContextPopupNoDefaultContents = true, then call TableBeginContextMenuPopup()/.../EndPopup(). + // - You may call TableDrawDefaultContextMenu() with selected flags to display specific sections of the default menu, + // e.g. TableDrawDefaultContextMenu(table, table->Flags & ~ImGuiTableFlags_Hideable) will display everything EXCEPT columns visibility options. + if (table->DisableDefaultContextMenu == false && TableBeginContextMenuPopup(table)) + { + TableDrawDefaultContextMenu(table, table->Flags); + EndPopup(); + } + + // [Part 12] Sanitize and build sort specs before we have a chance to use them for display. + // This path will only be exercised when sort specs are modified before header rows (e.g. init or visibility change) + if (table->IsSortSpecsDirty && (table->Flags & ImGuiTableFlags_Sortable)) + TableSortSpecsBuild(table); + + // [Part 13] Setup inner window decoration size (for scrolling / nav tracking to properly take account of frozen rows/columns) + if (table->FreezeColumnsRequest > 0) + table->InnerWindow->DecoInnerSizeX1 = table->Columns[table->DisplayOrderToIndex[table->FreezeColumnsRequest - 1]].MaxX - table->OuterRect.Min.x; + if (table->FreezeRowsRequest > 0) + table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight; + table_instance->LastFrozenHeight = 0.0f; + + // Initial state + ImGuiWindow* inner_window = table->InnerWindow; + if (table->Flags & ImGuiTableFlags_NoClip) + table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + else + inner_window->DrawList->PushClipRect(inner_window->InnerClipRect.Min, inner_window->InnerClipRect.Max, false); // FIXME: use table->InnerClipRect? +} + +// Process hit-testing on resizing borders. Actual size change will be applied in EndTable() +// - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise. +void ImGui::TableUpdateBorders(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->Flags & ImGuiTableFlags_Resizable); + + // At this point OuterRect height may be zero or under actual final height, so we rely on temporal coherency and + // use the final height from last frame. Because this is only affecting _interaction_ with columns, it is not + // really problematic (whereas the actual visual will be displayed in EndTable() and using the current frame height). + // Actual columns highlight/render will be performed in EndTable() and not be affected. + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + const float hit_half_width = ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale); + const float hit_y1 = (table->FreezeRowsCount >= 1 ? table->OuterRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight; + const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table_instance->LastOuterHeight - table->AngledHeadersHeight); + const float hit_y2_head = hit_y1 + table_instance->LastTopHeadersRowHeight; + + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) + continue; + + // ImGuiTableFlags_NoBordersInBodyUntilResize will be honored in TableDrawBorders() + const float border_y2_hit = (table->Flags & ImGuiTableFlags_NoBordersInBody) ? hit_y2_head : hit_y2_body; + if ((table->Flags & ImGuiTableFlags_NoBordersInBody) && table->IsUsingHeaders == false) + continue; + + if (!column->IsVisibleX && table->LastResizedColumn != column_n) + continue; + + ImGuiID column_id = TableGetColumnResizeID(table, column_n, table->InstanceCurrent); + ImRect hit_rect(column->MaxX - hit_half_width, hit_y1, column->MaxX + hit_half_width, border_y2_hit); + ItemAdd(hit_rect, column_id, NULL, ImGuiItemFlags_NoNav); + //GetForegroundDrawList()->AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100)); + + bool hovered = false, held = false; + bool pressed = ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_NoNavFocus); + if (pressed && IsMouseDoubleClicked(0)) + { + TableSetColumnWidthAutoSingle(table, column_n); + ClearActiveID(); + held = false; + } + if (held) + { + if (table->LastResizedColumn == -1) + table->ResizeLockMinContentsX2 = table->RightMostEnabledColumn != -1 ? table->Columns[table->RightMostEnabledColumn].MaxX : -FLT_MAX; + table->ResizedColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + } + if ((hovered && g.HoveredIdTimer > TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER) || held) + { + table->HoveredColumnBorder = (ImGuiTableColumnIdx)column_n; + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + } + } +} + +void ImGui::EndTable() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "EndTable() call should only be done while in BeginTable() scope!"); + return; + } + + // This assert would be very useful to catch a common error... unfortunately it would probably trigger in some + // cases, and for consistency user may sometimes output empty tables (and still benefit from e.g. outer border) + //IM_ASSERT(table->IsLayoutLocked && "Table unused: never called TableNextRow(), is that the intent?"); + + // If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves to ensure all our + // code paths are consistent (instead of just hoping that TableBegin/TableEnd will work), get borders drawn, etc. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + const ImGuiTableFlags flags = table->Flags; + ImGuiWindow* inner_window = table->InnerWindow; + ImGuiWindow* outer_window = table->OuterWindow; + ImGuiTableTempData* temp_data = table->TempData; + IM_ASSERT(inner_window == g.CurrentWindow); + IM_ASSERT(outer_window == inner_window || outer_window == inner_window->ParentWindow); + + if (table->IsInsideRow) + TableEndRow(table); + + // Context menu in columns body + if (flags & ImGuiTableFlags_ContextMenuInBody) + if (table->HoveredColumnBody != -1 && !IsAnyItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) + TableOpenContextMenu((int)table->HoveredColumnBody); + + // Finalize table height + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + inner_window->DC.PrevLineSize = temp_data->HostBackupPrevLineSize; + inner_window->DC.CurrLineSize = temp_data->HostBackupCurrLineSize; + inner_window->DC.CursorMaxPos = temp_data->HostBackupCursorMaxPos; + const float inner_content_max_y = table->RowPosY2; + IM_ASSERT(table->RowPosY2 == inner_window->DC.CursorPos.y); + if (inner_window != outer_window) + inner_window->DC.CursorMaxPos.y = inner_content_max_y; + else if (!(flags & ImGuiTableFlags_NoHostExtendY)) + table->OuterRect.Max.y = table->InnerRect.Max.y = ImMax(table->OuterRect.Max.y, inner_content_max_y); // Patch OuterRect/InnerRect height + table->WorkRect.Max.y = ImMax(table->WorkRect.Max.y, table->OuterRect.Max.y); + table_instance->LastOuterHeight = table->OuterRect.GetHeight(); + + // Setup inner scrolling range + // FIXME: This ideally should be done earlier, in BeginTable() SetNextWindowContentSize call, just like writing to inner_window->DC.CursorMaxPos.y, + // but since the later is likely to be impossible to do we'd rather update both axes together. + if (table->Flags & ImGuiTableFlags_ScrollX) + { + const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + float max_pos_x = table->InnerWindow->DC.CursorMaxPos.x; + if (table->RightMostEnabledColumn != -1) + max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border); + if (table->ResizedColumn != -1) + max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2); + table->InnerWindow->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth; + } + + // Pop clipping rect + if (!(flags & ImGuiTableFlags_NoClip)) + inner_window->DrawList->PopClipRect(); + inner_window->ClipRect = inner_window->DrawList->_ClipRectStack.back(); + + // Draw borders + if ((flags & ImGuiTableFlags_Borders) != 0) + TableDrawBorders(table); + +#if 0 + // Strip out dummy channel draw calls + // We have no way to prevent user submitting direct ImDrawList calls into a hidden column (but ImGui:: calls will be clipped out) + // Pros: remove draw calls which will have no effect. since they'll have zero-size cliprect they may be early out anyway. + // Cons: making it harder for users watching metrics/debugger to spot the wasted vertices. + if (table->DummyDrawChannel != (ImGuiTableColumnIdx)-1) + { + ImDrawChannel* dummy_channel = &table->DrawSplitter._Channels[table->DummyDrawChannel]; + dummy_channel->_CmdBuffer.resize(0); + dummy_channel->_IdxBuffer.resize(0); + } +#endif + + // Flatten channels and merge draw calls + ImDrawListSplitter* splitter = table->DrawSplitter; + splitter->SetCurrentChannel(inner_window->DrawList, 0); + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + TableMergeDrawChannels(table); + splitter->Merge(inner_window->DrawList); + + // Update ColumnsAutoFitWidth to get us ahead for host using our size to auto-resize without waiting for next BeginTable() + float auto_fit_width_for_fixed = 0.0f; + float auto_fit_width_for_stretched = 0.0f; + float auto_fit_width_for_stretched_min = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + float column_width_request = ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !(column->Flags & ImGuiTableColumnFlags_NoResize)) ? column->WidthRequest : TableGetColumnWidthAuto(table, column); + if (column->Flags & ImGuiTableColumnFlags_WidthFixed) + auto_fit_width_for_fixed += column_width_request; + else + auto_fit_width_for_stretched += column_width_request; + if ((column->Flags & ImGuiTableColumnFlags_WidthStretch) && (column->Flags & ImGuiTableColumnFlags_NoResize) != 0) + auto_fit_width_for_stretched_min = ImMax(auto_fit_width_for_stretched_min, column_width_request / (column->StretchWeight / table->ColumnsStretchSumWeights)); + } + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + table->ColumnsAutoFitWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount + auto_fit_width_for_fixed + ImMax(auto_fit_width_for_stretched, auto_fit_width_for_stretched_min); + + // Update scroll + if ((table->Flags & ImGuiTableFlags_ScrollX) == 0 && inner_window != outer_window) + { + inner_window->Scroll.x = 0.0f; + } + else if (table->LastResizedColumn != -1 && table->ResizedColumn == -1 && inner_window->ScrollbarX && table->InstanceInteracted == table->InstanceCurrent) + { + // When releasing a column being resized, scroll to keep the resulting column in sight + const float neighbor_width_to_keep_visible = table->MinColumnWidth + table->CellPaddingX * 2.0f; + ImGuiTableColumn* column = &table->Columns[table->LastResizedColumn]; + if (column->MaxX < table->InnerClipRect.Min.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x - neighbor_width_to_keep_visible, 1.0f); + else if (column->MaxX > table->InnerClipRect.Max.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x + neighbor_width_to_keep_visible, 1.0f); + } + + // Apply resizing/dragging at the end of the frame + if (table->ResizedColumn != -1 && table->InstanceCurrent == table->InstanceInteracted) + { + ImGuiTableColumn* column = &table->Columns[table->ResizedColumn]; + const float new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale)); + const float new_width = ImTrunc(new_x2 - column->MinX - table->CellSpacingX1 - table->CellPaddingX * 2.0f); + table->ResizedColumnNextWidth = new_width; + } + + table->IsActiveIdInTable = (g.ActiveIdIsAlive != 0 && table->IsActiveIdAliveBeforeTable == false); + + // Pop from id stack + IM_ASSERT_USER_ERROR(inner_window->IDStack.back() == table_instance->TableInstanceID, "Mismatching PushID/PopID!"); + IM_ASSERT_USER_ERROR(outer_window->DC.ItemWidthStack.Size >= temp_data->HostBackupItemWidthStackSize, "Too many PopItemWidth!"); + if (table->InstanceCurrent > 0) + PopID(); + PopID(); + + // Restore window data that we modified + const ImVec2 backup_outer_max_pos = outer_window->DC.CursorMaxPos; + inner_window->WorkRect = temp_data->HostBackupWorkRect; + inner_window->ParentWorkRect = temp_data->HostBackupParentWorkRect; + inner_window->SkipItems = table->HostSkipItems; + outer_window->DC.CursorPos = table->OuterRect.Min; + outer_window->DC.ItemWidth = temp_data->HostBackupItemWidth; + outer_window->DC.ItemWidthStack.Size = temp_data->HostBackupItemWidthStackSize; + outer_window->DC.ColumnsOffset = temp_data->HostBackupColumnsOffset; + + // Layout in outer window + // (FIXME: To allow auto-fit and allow desirable effect of SameLine() we dissociate 'used' vs 'ideal' size by overriding + // CursorPosPrevLine and CursorMaxPos manually. That should be a more general layout feature, see same problem e.g. #3414) + if (inner_window != outer_window) + { + short backup_nav_layers_active_mask = inner_window->DC.NavLayersActiveMask; + inner_window->DC.NavLayersActiveMask |= 1 << table->NavLayer; // So empty table don't appear to navigate differently. + g.CurrentTable = NULL; // To avoid error recovery recursing + EndChild(); + g.CurrentTable = table; + inner_window->DC.NavLayersActiveMask = backup_nav_layers_active_mask; + } + else + { + ItemSize(table->OuterRect.GetSize()); + ItemAdd(table->OuterRect, 0); + } + + // Override declared contents width/height to enable auto-resize while not needlessly adding a scrollbar + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + // FIXME-TABLE: Could we remove this section? + // ColumnsAutoFitWidth may be one frame ahead here since for Fixed+NoResize is calculated from latest contents + IM_ASSERT((table->Flags & ImGuiTableFlags_ScrollX) == 0); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth); + } + else if (temp_data->UserOuterSize.x <= 0.0f) + { + // Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block + // - Checking for ImGuiTableFlags_ScrollX/ScrollY flag makes us a frame ahead when disabling those flags. + // - FIXME-TABLE: Would make sense to pre-compute expected scrollbar visibility/sizes to generally save a frame of feedback. + const float inner_content_max_x = table->OuterRect.Min.x + table->ColumnsAutoFitWidth; // Slightly misleading name but used for code symmetry with inner_content_max_y + const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((table->Flags & ImGuiTableFlags_ScrollY) ? inner_window->ScrollbarSizes.x : 0.0f); + outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, inner_content_max_x + decoration_size - temp_data->UserOuterSize.x); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, inner_content_max_x + decoration_size)); + } + else + { + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Max.x); + } + if (temp_data->UserOuterSize.y <= 0.0f) + { + const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.y : 0.0f; + outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, inner_content_max_y + decoration_size - temp_data->UserOuterSize.y); + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table->OuterRect.Max.y, inner_content_max_y + decoration_size)); + } + else + { + // OuterRect.Max.y may already have been pushed downward from the initial value (unless ImGuiTableFlags_NoHostExtendY is set) + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, table->OuterRect.Max.y); + } + + // Save settings + if (table->IsSettingsDirty) + TableSaveSettings(table); + table->IsInitializing = false; + + // Clear or restore current table, if any + IM_ASSERT(g.CurrentWindow == outer_window && g.CurrentTable == table); + IM_ASSERT(g.TablesTempDataStacked > 0); + temp_data = (--g.TablesTempDataStacked > 0) ? &g.TablesTempData[g.TablesTempDataStacked - 1] : NULL; + g.CurrentTable = temp_data ? g.Tables.GetByIndex(temp_data->TableIndex) : NULL; + if (g.CurrentTable) + { + g.CurrentTable->TempData = temp_data; + g.CurrentTable->DrawSplitter = &temp_data->DrawSplitter; + } + outer_window->DC.CurrentTableIdx = g.CurrentTable ? g.Tables.GetIndex(g.CurrentTable) : -1; + NavUpdateCurrentWindowIsScrollPushableX(); +} + +// Called in TableSetupColumn() when initializing and in TableLoadSettings() for defaults before applying stored settings. +// 'init_mask' specify which fields to initialize. +static void TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags init_mask) +{ + ImGuiTableColumnFlags flags = column->Flags; + if (init_mask & ImGuiTableFlags_Resizable) + { + float init_width_or_weight = column->InitStretchWeightOrWidth; + column->WidthRequest = ((flags & ImGuiTableColumnFlags_WidthFixed) && init_width_or_weight > 0.0f) ? init_width_or_weight : -1.0f; + column->StretchWeight = (init_width_or_weight > 0.0f && (flags & ImGuiTableColumnFlags_WidthStretch)) ? init_width_or_weight : -1.0f; + if (init_width_or_weight > 0.0f) // Disable auto-fit if an explicit width/weight has been specified + column->AutoFitQueue = 0x00; + } + if (init_mask & ImGuiTableFlags_Reorderable) + column->DisplayOrder = (ImGuiTableColumnIdx)table->Columns.index_from_ptr(column); + if (init_mask & ImGuiTableFlags_Hideable) + column->IsUserEnabled = column->IsUserEnabledNextFrame = (flags & ImGuiTableColumnFlags_DefaultHide) ? 0 : 1; + if (init_mask & ImGuiTableFlags_Sortable) + { + // Multiple columns using _DefaultSort will be reassigned unique SortOrder values when building the sort specs. + column->SortOrder = (flags & ImGuiTableColumnFlags_DefaultSort) ? 0 : -1; + column->SortDirection = (flags & ImGuiTableColumnFlags_DefaultSort) ? ((flags & ImGuiTableColumnFlags_PreferSortDescending) ? (ImS8)ImGuiSortDirection_Descending : (ImU8)(ImGuiSortDirection_Ascending)) : (ImS8)ImGuiSortDirection_None; + } +} + +// See "COLUMNS SIZING POLICIES" comments at the top of this file +// If (init_width_or_weight <= 0.0f) it is ignored +void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->IsLayoutLocked == false && "Need to call TableSetupColumn() before first row!"); + IM_ASSERT((flags & ImGuiTableColumnFlags_StatusMask_) == 0 && "Illegal to pass StatusMask values to TableSetupColumn()"); + if (table->DeclColumnsCount >= table->ColumnsCount) + { + IM_ASSERT_USER_ERROR(table->DeclColumnsCount < table->ColumnsCount, "Called TableSetupColumn() too many times!"); + return; + } + + ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount]; + table->DeclColumnsCount++; + + // Assert when passing a width or weight if policy is entirely left to default, to avoid storing width into weight and vice-versa. + // Give a grace to users of ImGuiTableFlags_ScrollX. + if (table->IsDefaultSizingPolicy && (flags & ImGuiTableColumnFlags_WidthMask_) == 0 && (flags & ImGuiTableFlags_ScrollX) == 0) + IM_ASSERT(init_width_or_weight <= 0.0f && "Can only specify width/weight if sizing policy is set explicitly in either Table or Column."); + + // When passing a width automatically enforce WidthFixed policy + // (whereas TableSetupColumnFlags would default to WidthAuto if table is not resizable) + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0 && init_width_or_weight > 0.0f) + if ((table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedFit || (table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + if (flags & ImGuiTableColumnFlags_AngledHeader) + { + flags |= ImGuiTableColumnFlags_NoHeaderLabel; + table->AngledHeadersCount++; + } + + TableSetupColumnFlags(table, column, flags); + column->UserID = user_id; + flags = column->Flags; + + // Initialize defaults + column->InitStretchWeightOrWidth = init_width_or_weight; + if (table->IsInitializing) + { + ImGuiTableFlags init_flags = ~table->SettingsLoadedFlags; + if (column->WidthRequest < 0.0f && column->StretchWeight < 0.0f) + init_flags |= ImGuiTableFlags_Resizable; + TableInitColumnDefaults(table, column, init_flags); + } + + // Store name (append with zero-terminator in contiguous buffer) + // FIXME: If we recorded the number of \n in names we could compute header row height + column->NameOffset = -1; + if (label != NULL && label[0] != 0) + { + column->NameOffset = (ImS16)table->ColumnsNames.size(); + table->ColumnsNames.append(label, label + ImStrlen(label) + 1); + } +} + +// [Public] +void ImGui::TableSetupScrollFreeze(int columns, int rows) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->IsLayoutLocked == false && "Need to call TableSetupColumn() before first row!"); + IM_ASSERT(columns >= 0 && columns < IMGUI_TABLE_MAX_COLUMNS); + IM_ASSERT(rows >= 0 && rows < 128); // Arbitrary limit + + table->FreezeColumnsRequest = (table->Flags & ImGuiTableFlags_ScrollX) ? (ImGuiTableColumnIdx)ImMin(columns, table->ColumnsCount) : 0; + table->FreezeColumnsCount = (table->InnerWindow->Scroll.x != 0.0f) ? table->FreezeColumnsRequest : 0; + table->FreezeRowsRequest = (table->Flags & ImGuiTableFlags_ScrollY) ? (ImGuiTableColumnIdx)rows : 0; + table->FreezeRowsCount = (table->InnerWindow->Scroll.y != 0.0f) ? table->FreezeRowsRequest : 0; + table->IsUnfrozenRows = (table->FreezeRowsCount == 0); // Make sure this is set before TableUpdateLayout() so ImGuiListClipper can benefit from it.b + + // Ensure frozen columns are ordered in their section. We still allow multiple frozen columns to be reordered. + // FIXME-TABLE: This work for preserving 2143 into 21|43. How about 4321 turning into 21|43? (preserve relative order in each section) + for (int column_n = 0; column_n < table->FreezeColumnsRequest; column_n++) + { + int order_n = table->DisplayOrderToIndex[column_n]; + if (order_n != column_n && order_n >= table->FreezeColumnsRequest) + { + ImSwap(table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder, table->Columns[table->DisplayOrderToIndex[column_n]].DisplayOrder); + ImSwap(table->DisplayOrderToIndex[order_n], table->DisplayOrderToIndex[column_n]); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Simple accessors +//----------------------------------------------------------------------------- +// - TableGetColumnCount() +// - TableGetColumnName() +// - TableGetColumnName() [Internal] +// - TableSetColumnEnabled() +// - TableGetColumnFlags() +// - TableGetCellBgRect() [Internal] +// - TableGetColumnResizeID() [Internal] +// - TableGetHoveredColumn() [Internal] +// - TableGetHoveredRow() [Internal] +// - TableSetBgColor() +//----------------------------------------------------------------------------- + +int ImGui::TableGetColumnCount() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + return table ? table->ColumnsCount : 0; +} + +const char* ImGui::TableGetColumnName(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return NULL; + if (column_n < 0) + column_n = table->CurrentColumn; + return TableGetColumnName(table, column_n); +} + +const char* ImGui::TableGetColumnName(const ImGuiTable* table, int column_n) +{ + if (table->IsLayoutLocked == false && column_n >= table->DeclColumnsCount) + return ""; // NameOffset is invalid at this point + const ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->NameOffset == -1) + return ""; + return &table->ColumnsNames.Buf[column->NameOffset]; +} + +// Change user accessible enabled/disabled state of a column (often perceived as "showing/hiding" from users point of view) +// Note that end-user can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) +// - Require table to have the ImGuiTableFlags_Hideable flag because we are manipulating user accessible state. +// - Request will be applied during next layout, which happens on the first call to TableNextRow() after BeginTable(). +// - For the getter you can test (TableGetColumnFlags() & ImGuiTableColumnFlags_IsEnabled) != 0. +// - Alternative: the ImGuiTableColumnFlags_Disabled is an overriding/master disable flag which will also hide the column from context menu. +void ImGui::TableSetColumnEnabled(int column_n, bool enabled) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->Flags & ImGuiTableFlags_Hideable); // See comments above + if (column_n < 0) + column_n = table->CurrentColumn; + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column = &table->Columns[column_n]; + column->IsUserEnabledNextFrame = enabled; +} + +// We allow querying for an extra column in order to poll the IsHovered state of the right-most section +ImGuiTableColumnFlags ImGui::TableGetColumnFlags(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return ImGuiTableColumnFlags_None; + if (column_n < 0) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) + return (table->HoveredColumnBody == column_n) ? ImGuiTableColumnFlags_IsHovered : ImGuiTableColumnFlags_None; + return table->Columns[column_n].Flags; +} + +// Return the cell rectangle based on currently known height. +// - Important: we generally don't know our row height until the end of the row, so Max.y will be incorrect in many situations. +// The only case where this is correct is if we provided a min_row_height to TableNextRow() and don't go below it, or in TableEndRow() when we locked that height. +// - Important: if ImGuiTableFlags_PadOuterX is set but ImGuiTableFlags_PadInnerX is not set, the outer-most left and right +// columns report a small offset so their CellBgRect can extend up to the outer border. +// FIXME: But the rendering code in TableEndRow() nullifies that with clamping required for scrolling. +ImRect ImGui::TableGetCellBgRect(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float x1 = column->MinX; + float x2 = column->MaxX; + //if (column->PrevEnabledColumn == -1) + // x1 -= table->OuterPaddingX; + //if (column->NextEnabledColumn == -1) + // x2 += table->OuterPaddingX; + x1 = ImMax(x1, table->WorkRect.Min.x); + x2 = ImMin(x2, table->WorkRect.Max.x); + return ImRect(x1, table->RowPosY1, x2, table->RowPosY2); +} + +// Return the resizing ID for the right-side of the given column. +ImGuiID ImGui::TableGetColumnResizeID(ImGuiTable* table, int column_n, int instance_no) +{ + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiID instance_id = TableGetInstanceID(table, instance_no); + return instance_id + 1 + column_n; // FIXME: #6140: still not ideal +} + +// Return -1 when table is not hovered. return columns_count if hovering the unused space at the right of the right-most visible column. +int ImGui::TableGetHoveredColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + return (int)table->HoveredColumnBody; +} + +// Return -1 when table is not hovered. Return maxrow+1 if in table but below last submitted row. +// *IMPORTANT* Unlike TableGetHoveredColumn(), this has a one frame latency in updating the value. +// This difference with is the reason why this is not public yet. +int ImGui::TableGetHoveredRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + return (int)table_instance->HoveredRowLast; +} + +void ImGui::TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(target != ImGuiTableBgTarget_None); + + if (color == IM_COL32_DISABLE) + color = 0; + + // We cannot draw neither the cell or row background immediately as we don't know the row height at this point in time. + switch (target) + { + case ImGuiTableBgTarget_CellBg: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + if (column_n == -1) + column_n = table->CurrentColumn; + if (!IM_BITARRAY_TESTBIT(table->VisibleMaskByIndex, column_n)) + return; + if (table->RowCellDataCurrent < 0 || table->RowCellData[table->RowCellDataCurrent].Column != column_n) + table->RowCellDataCurrent++; + ImGuiTableCellData* cell_data = &table->RowCellData[table->RowCellDataCurrent]; + cell_data->BgColor = color; + cell_data->Column = (ImGuiTableColumnIdx)column_n; + break; + } + case ImGuiTableBgTarget_RowBg0: + case ImGuiTableBgTarget_RowBg1: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + IM_ASSERT(column_n == -1); + int bg_idx = (target == ImGuiTableBgTarget_RowBg1) ? 1 : 0; + table->RowBgColor[bg_idx] = color; + break; + } + default: + IM_ASSERT(0); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Row changes +//------------------------------------------------------------------------- +// - TableGetRowIndex() +// - TableNextRow() +// - TableBeginRow() [Internal] +// - TableEndRow() [Internal] +//------------------------------------------------------------------------- + +// [Public] Note: for row coloring we use ->RowBgColorCounter which is the same value without counting header rows +int ImGui::TableGetRowIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentRow; +} + +// [Public] Starts into the first cell of a new row +void ImGui::TableNextRow(ImGuiTableRowFlags row_flags, float row_min_height) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + if (table->IsInsideRow) + TableEndRow(table); + + table->LastRowFlags = table->RowFlags; + table->RowFlags = row_flags; + table->RowCellPaddingY = g.Style.CellPadding.y; + table->RowMinHeight = row_min_height; + TableBeginRow(table); + + // We honor min_row_height requested by user, but cannot guarantee per-row maximum height, + // because that would essentially require a unique clipping rectangle per-cell. + table->RowPosY2 += table->RowCellPaddingY * 2.0f; + table->RowPosY2 = ImMax(table->RowPosY2, table->RowPosY1 + row_min_height); + + // Disable output until user calls TableNextColumn() + table->InnerWindow->SkipItems = true; +} + +// [Internal] Only called by TableNextRow() +void ImGui::TableBeginRow(ImGuiTable* table) +{ + ImGuiWindow* window = table->InnerWindow; + IM_ASSERT(!table->IsInsideRow); + + // New row + table->CurrentRow++; + table->CurrentColumn = -1; + table->RowBgColor[0] = table->RowBgColor[1] = IM_COL32_DISABLE; + table->RowCellDataCurrent = -1; + table->IsInsideRow = true; + + // Begin frozen rows + float next_y1 = table->RowPosY2; + if (table->CurrentRow == 0 && table->FreezeRowsCount > 0) + next_y1 = window->DC.CursorPos.y = table->OuterRect.Min.y; + + table->RowPosY1 = table->RowPosY2 = next_y1; + table->RowTextBaseline = 0.0f; + table->RowIndentOffsetX = window->DC.Indent.x - table->HostIndentX; // Lock indent + + window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x, window->DC.CursorPos.y + table->RowCellPaddingY); // This allows users to call SameLine() to share LineSize between columns. + window->DC.PrevLineSize = window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); // This allows users to call SameLine() to share LineSize between columns, and to call it from first column too. + window->DC.IsSameLine = window->DC.IsSetPos = false; + window->DC.CursorMaxPos.y = next_y1; + + // Making the header BG color non-transparent will allow us to overlay it multiple times when handling smooth dragging. + if (table->RowFlags & ImGuiTableRowFlags_Headers) + { + TableSetBgColor(ImGuiTableBgTarget_RowBg0, GetColorU32(ImGuiCol_TableHeaderBg)); + if (table->CurrentRow == 0) + table->IsUsingHeaders = true; + } +} + +// [Internal] Called by TableNextRow() +void ImGui::TableEndRow(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window == table->InnerWindow); + IM_ASSERT(table->IsInsideRow); + + if (table->CurrentColumn != -1) + TableEndCell(table); + + // Logging + if (g.LogEnabled) + LogRenderedText(NULL, "|"); + + // Position cursor at the bottom of our row so it can be used for e.g. clipping calculation. However it is + // likely that the next call to TableBeginCell() will reposition the cursor to take account of vertical padding. + window->DC.CursorPos.y = table->RowPosY2; + + // Row background fill + const float bg_y1 = table->RowPosY1; + const float bg_y2 = table->RowPosY2; + const bool unfreeze_rows_actual = (table->CurrentRow + 1 == table->FreezeRowsCount); + const bool unfreeze_rows_request = (table->CurrentRow + 1 == table->FreezeRowsRequest); + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + if ((table->RowFlags & ImGuiTableRowFlags_Headers) && (table->CurrentRow == 0 || (table->LastRowFlags & ImGuiTableRowFlags_Headers))) + table_instance->LastTopHeadersRowHeight += bg_y2 - bg_y1; + + const bool is_visible = (bg_y2 >= table->InnerClipRect.Min.y && bg_y1 <= table->InnerClipRect.Max.y); + if (is_visible) + { + // Update data for TableGetHoveredRow() + if (table->HoveredColumnBody != -1 && g.IO.MousePos.y >= bg_y1 && g.IO.MousePos.y < bg_y2 && table_instance->HoveredRowNext < 0) + table_instance->HoveredRowNext = table->CurrentRow; + + // Decide of background color for the row + ImU32 bg_col0 = 0; + ImU32 bg_col1 = 0; + if (table->RowBgColor[0] != IM_COL32_DISABLE) + bg_col0 = table->RowBgColor[0]; + else if (table->Flags & ImGuiTableFlags_RowBg) + bg_col0 = GetColorU32((table->RowBgColorCounter & 1) ? ImGuiCol_TableRowBgAlt : ImGuiCol_TableRowBg); + if (table->RowBgColor[1] != IM_COL32_DISABLE) + bg_col1 = table->RowBgColor[1]; + + // Decide of top border color + ImU32 top_border_col = 0; + const float border_size = TABLE_BORDER_SIZE; + if (table->CurrentRow > 0 && (table->Flags & ImGuiTableFlags_BordersInnerH)) + top_border_col = (table->LastRowFlags & ImGuiTableRowFlags_Headers) ? table->BorderColorStrong : table->BorderColorLight; + + const bool draw_cell_bg_color = table->RowCellDataCurrent >= 0; + const bool draw_strong_bottom_border = unfreeze_rows_actual; + if ((bg_col0 | bg_col1 | top_border_col) != 0 || draw_strong_bottom_border || draw_cell_bg_color) + { + // In theory we could call SetWindowClipRectBeforeSetChannel() but since we know TableEndRow() is + // always followed by a change of clipping rectangle we perform the smallest overwrite possible here. + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + window->DrawList->_CmdHeader.ClipRect = table->Bg0ClipRectForDrawCmd.ToVec4(); + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_BG0); + } + + // Draw row background + // We soft/cpu clip this so all backgrounds and borders can share the same clipping rectangle + if (bg_col0 || bg_col1) + { + ImRect row_rect(table->WorkRect.Min.x, bg_y1, table->WorkRect.Max.x, bg_y2); + row_rect.ClipWith(table->BgClipRect); + if (bg_col0 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col0); + if (bg_col1 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col1); + } + + // Draw cell background color + if (draw_cell_bg_color) + { + ImGuiTableCellData* cell_data_end = &table->RowCellData[table->RowCellDataCurrent]; + for (ImGuiTableCellData* cell_data = &table->RowCellData[0]; cell_data <= cell_data_end; cell_data++) + { + // As we render the BG here we need to clip things (for layout we would not) + // FIXME: This cancels the OuterPadding addition done by TableGetCellBgRect(), need to keep it while rendering correctly while scrolling. + const ImGuiTableColumn* column = &table->Columns[cell_data->Column]; + ImRect cell_bg_rect = TableGetCellBgRect(table, cell_data->Column); + cell_bg_rect.ClipWith(table->BgClipRect); + cell_bg_rect.Min.x = ImMax(cell_bg_rect.Min.x, column->ClipRect.Min.x); // So that first column after frozen one gets clipped when scrolling + cell_bg_rect.Max.x = ImMin(cell_bg_rect.Max.x, column->MaxX); + if (cell_bg_rect.Min.y < cell_bg_rect.Max.y) + window->DrawList->AddRectFilled(cell_bg_rect.Min, cell_bg_rect.Max, cell_data->BgColor); + } + } + + // Draw top border + if (top_border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), top_border_col, border_size); + + // Draw bottom border at the row unfreezing mark (always strong) + if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y2), ImVec2(table->BorderX2, bg_y2), table->BorderColorStrong, border_size); + } + + // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle) + // We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark end of row and + // get the new cursor position. + if (unfreeze_rows_request) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->Columns[column_n].NavLayerCurrent = table->NavLayer; + const float y0 = ImMax(table->RowPosY2 + 1, table->InnerClipRect.Min.y); + table_instance->LastFrozenHeight = y0 - table->OuterRect.Min.y; + + if (unfreeze_rows_actual) + { + IM_ASSERT(table->IsUnfrozenRows == false); + table->IsUnfrozenRows = true; + + // BgClipRect starts as table->InnerClipRect, reduce it now and make BgClipRectForDrawCmd == BgClipRect + table->BgClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y = ImMin(y0, table->InnerClipRect.Max.y); + table->BgClipRect.Max.y = table->Bg2ClipRectForDrawCmd.Max.y = table->InnerClipRect.Max.y; + table->Bg2DrawChannelCurrent = table->Bg2DrawChannelUnfrozen; + IM_ASSERT(table->Bg2ClipRectForDrawCmd.Min.y <= table->Bg2ClipRectForDrawCmd.Max.y); + + float row_height = table->RowPosY2 - table->RowPosY1; + table->RowPosY2 = window->DC.CursorPos.y = table->WorkRect.Min.y + table->RowPosY2 - table->OuterRect.Min.y; + table->RowPosY1 = table->RowPosY2 - row_height; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + column->DrawChannelCurrent = column->DrawChannelUnfrozen; + column->ClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y; + } + + // Update cliprect ahead of TableBeginCell() so clipper can access to new ClipRect->Min.y + SetWindowClipRectBeforeSetChannel(window, table->Columns[0].ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Columns[0].DrawChannelCurrent); + } + } + + if (!(table->RowFlags & ImGuiTableRowFlags_Headers)) + table->RowBgColorCounter++; + table->IsInsideRow = false; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns changes +//------------------------------------------------------------------------- +// - TableGetColumnIndex() +// - TableSetColumnIndex() +// - TableNextColumn() +// - TableBeginCell() [Internal] +// - TableEndCell() [Internal] +//------------------------------------------------------------------------- + +int ImGui::TableGetColumnIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentColumn; +} + +// [Public] Append into a specific column +bool ImGui::TableSetColumnIndex(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->CurrentColumn != column_n) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + if ((column_n >= 0 && column_n < table->ColumnsCount) == false) + { + IM_ASSERT_USER_ERROR(column_n >= 0 && column_n < table->ColumnsCount, "TableSetColumnIndex() invalid column index!"); + return false; + } + TableBeginCell(table, column_n); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + return table->Columns[column_n].IsRequestOutput; +} + +// [Public] Append into the next column, wrap and create a new row when already on last column +bool ImGui::TableNextColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->IsInsideRow && table->CurrentColumn + 1 < table->ColumnsCount) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + TableBeginCell(table, table->CurrentColumn + 1); + } + else + { + TableNextRow(); + TableBeginCell(table, 0); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + return table->Columns[table->CurrentColumn].IsRequestOutput; +} + + +// [Internal] Called by TableSetColumnIndex()/TableNextColumn() +// This is called very frequently, so we need to be mindful of unnecessary overhead. +// FIXME-TABLE FIXME-OPT: Could probably shortcut some things for non-active or clipped columns. +void ImGui::TableBeginCell(ImGuiTable* table, int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTableColumn* column = &table->Columns[column_n]; + ImGuiWindow* window = table->InnerWindow; + table->CurrentColumn = column_n; + + // Start position is roughly ~~ CellRect.Min + CellPadding + Indent + float start_x = column->WorkMinX; + if (column->Flags & ImGuiTableColumnFlags_IndentEnable) + start_x += table->RowIndentOffsetX; // ~~ += window.DC.Indent.x - table->HostIndentX, except we locked it for the row. + + window->DC.CursorPos.x = start_x; + window->DC.CursorPos.y = table->RowPosY1 + table->RowCellPaddingY; + window->DC.CursorMaxPos.x = window->DC.CursorPos.x; + window->DC.ColumnsOffset.x = start_x - window->Pos.x - window->DC.Indent.x; // FIXME-WORKRECT + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x; // PrevLine.y is preserved. This allows users to call SameLine() to share LineSize between columns. + window->DC.CurrLineTextBaseOffset = table->RowTextBaseline; + window->DC.NavLayerCurrent = (ImGuiNavLayer)column->NavLayerCurrent; + + // Note how WorkRect.Max.y is only set once during layout + window->WorkRect.Min.y = window->DC.CursorPos.y; + window->WorkRect.Min.x = column->WorkMinX; + window->WorkRect.Max.x = column->WorkMaxX; + window->DC.ItemWidth = column->ItemWidth; + + window->SkipItems = column->IsSkipItems; + if (column->IsSkipItems) + { + g.LastItemData.ID = 0; + g.LastItemData.StatusFlags = 0; + } + + if (table->Flags & ImGuiTableFlags_NoClip) + { + // FIXME: if we end up drawing all borders/bg in EndTable, could remove this and just assert that channel hasn't changed. + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + //IM_ASSERT(table->DrawSplitter._Current == TABLE_DRAW_CHANNEL_NOCLIP); + } + else + { + // FIXME-TABLE: Could avoid this if draw channel is dummy channel? + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); + } + + // Logging + if (g.LogEnabled && !column->IsSkipItems) + { + LogRenderedText(&window->DC.CursorPos, "|"); + g.LogLinePosY = FLT_MAX; + } +} + +// [Internal] Called by TableNextRow()/TableSetColumnIndex()/TableNextColumn() +void ImGui::TableEndCell(ImGuiTable* table) +{ + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + ImGuiWindow* window = table->InnerWindow; + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Report maximum position so we can infer content size per column. + float* p_max_pos_x; + if (table->RowFlags & ImGuiTableRowFlags_Headers) + p_max_pos_x = &column->ContentMaxXHeadersUsed; // Useful in case user submit contents in header row that is not a TableHeader() call + else + p_max_pos_x = table->IsUnfrozenRows ? &column->ContentMaxXUnfrozen : &column->ContentMaxXFrozen; + *p_max_pos_x = ImMax(*p_max_pos_x, window->DC.CursorMaxPos.x); + if (column->IsEnabled) + table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y + table->RowCellPaddingY); + column->ItemWidth = window->DC.ItemWidth; + + // Propagate text baseline for the entire row + // FIXME-TABLE: Here we propagate text baseline from the last line of the cell.. instead of the first one. + table->RowTextBaseline = ImMax(table->RowTextBaseline, window->DC.PrevLineTextBaseOffset); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns width management +//------------------------------------------------------------------------- +// - TableGetMaxColumnWidth() [Internal] +// - TableGetColumnWidthAuto() [Internal] +// - TableSetColumnWidth() +// - TableSetColumnWidthAutoSingle() [Internal] +// - TableSetColumnWidthAutoAll() [Internal] +// - TableUpdateColumnsWeightFromWidth() [Internal] +//------------------------------------------------------------------------- +// Note that actual columns widths are computed in TableUpdateLayout(). +//------------------------------------------------------------------------- + +// Maximum column content width given current layout. Use column->MinX so this value differs on a per-column basis. +float ImGui::TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float max_width = FLT_MAX; + const float min_column_distance = table->MinColumnWidth + table->CellPaddingX * 2.0f + table->CellSpacingX1 + table->CellSpacingX2; + if (table->Flags & ImGuiTableFlags_ScrollX) + { + // Frozen columns can't reach beyond visible width else scrolling will naturally break. + // (we use DisplayOrder as within a set of multiple frozen column reordering is possible) + if (column->DisplayOrder < table->FreezeColumnsRequest) + { + max_width = (table->InnerClipRect.Max.x - (table->FreezeColumnsRequest - column->DisplayOrder) * min_column_distance) - column->MinX; + max_width = max_width - table->OuterPaddingX - table->CellPaddingX - table->CellSpacingX2; + } + } + else if ((table->Flags & ImGuiTableFlags_NoKeepColumnsVisible) == 0) + { + // If horizontal scrolling if disabled, we apply a final lossless shrinking of columns in order to make + // sure they are all visible. Because of this we also know that all of the columns will always fit in + // table->WorkRect and therefore in table->InnerRect (because ScrollX is off) + // FIXME-TABLE: This is solved incorrectly but also quite a difficult problem to fix as we also want ClipRect width to match. + // See "table_width_distrib" and "table_width_keep_visible" tests + max_width = table->WorkRect.Max.x - (table->ColumnsEnabledCount - column->IndexWithinEnabledSet - 1) * min_column_distance - column->MinX; + //max_width -= table->CellSpacingX1; + max_width -= table->CellSpacingX2; + max_width -= table->CellPaddingX * 2.0f; + max_width -= table->OuterPaddingX; + } + return max_width; +} + +// Note this is meant to be stored in column->WidthAuto, please generally use the WidthAuto field +float ImGui::TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column) +{ + const float content_width_body = ImMax(column->ContentMaxXFrozen, column->ContentMaxXUnfrozen) - column->WorkMinX; + const float content_width_headers = column->ContentMaxXHeadersIdeal - column->WorkMinX; + float width_auto = content_width_body; + if (!(column->Flags & ImGuiTableColumnFlags_NoHeaderWidth)) + width_auto = ImMax(width_auto, content_width_headers); + + // Non-resizable fixed columns preserve their requested width + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f) + if (!(table->Flags & ImGuiTableFlags_Resizable) || (column->Flags & ImGuiTableColumnFlags_NoResize)) + width_auto = column->InitStretchWeightOrWidth; + + return ImMax(width_auto, table->MinColumnWidth); +} + +// 'width' = inner column width, without padding +void ImGui::TableSetColumnWidth(int column_n, float width) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && table->IsLayoutLocked == false); + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column_0 = &table->Columns[column_n]; + float column_0_width = width; + + // Apply constraints early + // Compare both requested and actual given width to avoid overwriting requested width when column is stuck (minimum size, bounded) + IM_ASSERT(table->MinColumnWidth > 0.0f); + const float min_width = table->MinColumnWidth; + const float max_width = ImMax(min_width, column_0->WidthMax); // Don't use TableCalcMaxColumnWidth() here as it would rely on MinX from last instance (#7933) + column_0_width = ImClamp(column_0_width, min_width, max_width); + if (column_0->WidthGiven == column_0_width || column_0->WidthRequest == column_0_width) + return; + + //IMGUI_DEBUG_PRINT("TableSetColumnWidth(%d, %.1f->%.1f)\n", column_0_idx, column_0->WidthGiven, column_0_width); + ImGuiTableColumn* column_1 = (column_0->NextEnabledColumn != -1) ? &table->Columns[column_0->NextEnabledColumn] : NULL; + + // In this surprisingly not simple because of how we support mixing Fixed and multiple Stretch columns. + // - All fixed: easy. + // - All stretch: easy. + // - One or more fixed + one stretch: easy. + // - One or more fixed + more than one stretch: tricky. + // Qt when manual resize is enabled only supports a single _trailing_ stretch column, we support more cases here. + + // When forwarding resize from Wn| to Fn+1| we need to be considerate of the _NoResize flag on Fn+1. + // FIXME-TABLE: Find a way to rewrite all of this so interactions feel more consistent for the user. + // Scenarios: + // - F1 F2 F3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. Subsequent columns will be offset. + // - F1 F2 F3 resize from F3| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered. + // - F1 F2 W3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered, but it doesn't make much sense as the Stretch column will always be minimal size. + // - F1 F2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 W3 resize from W1| or W2| --> ok + // - W1 W2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 F3 resize from F3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 resize from F2| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 F3 resize from W1| or W2| --> ok + // - W1 F2 W3 resize from W1| or F2| --> ok + // - F1 W2 F3 resize from W2| --> ok + // - F1 W3 F2 resize from W3| --> ok + // - W1 F2 F3 resize from W1| --> ok: equivalent to resizing |F2. F3 will not move. + // - W1 F2 F3 resize from F2| --> ok + // All resizes from a Wx columns are locking other columns. + + // Possible improvements: + // - W1 W2 W3 resize W1| --> to not be stuck, both W2 and W3 would stretch down. Seems possible to fix. Would be most beneficial to simplify resize of all-weighted columns. + // - W3 F1 F2 resize W3| --> to not be stuck past F1|, both F1 and F2 would need to stretch down, which would be lossy or ambiguous. Seems hard to fix. + + // [Resize Rule 1] Can't resize from right of right-most visible column if there is any Stretch column. Implemented in TableUpdateLayout(). + + // If we have all Fixed columns OR resizing a Fixed column that doesn't come after a Stretch one, we can do an offsetting resize. + // This is the preferred resize path + if (column_0->Flags & ImGuiTableColumnFlags_WidthFixed) + if (!column_1 || table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder >= column_0->DisplayOrder) + { + column_0->WidthRequest = column_0_width; + table->IsSettingsDirty = true; + return; + } + + // We can also use previous column if there's no next one (this is used when doing an auto-fit on the right-most stretch column) + if (column_1 == NULL) + column_1 = (column_0->PrevEnabledColumn != -1) ? &table->Columns[column_0->PrevEnabledColumn] : NULL; + if (column_1 == NULL) + return; + + // Resizing from right-side of a Stretch column before a Fixed column forward sizing to left-side of fixed column. + // (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b) + float column_1_width = ImMax(column_1->WidthRequest - (column_0_width - column_0->WidthRequest), min_width); + column_0_width = column_0->WidthRequest + column_1->WidthRequest - column_1_width; + IM_ASSERT(column_0_width > 0.0f && column_1_width > 0.0f); + column_0->WidthRequest = column_0_width; + column_1->WidthRequest = column_1_width; + if ((column_0->Flags | column_1->Flags) & ImGuiTableColumnFlags_WidthStretch) + TableUpdateColumnsWeightFromWidth(table); + table->IsSettingsDirty = true; +} + +// Disable clipping then auto-fit, will take 2 frames +// (we don't take a shortcut for unclipped columns to reduce inconsistencies when e.g. resizing multiple columns) +void ImGui::TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n) +{ + // Single auto width uses auto-fit + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled) + return; + column->CannotSkipItemsQueue = (1 << 0); + table->AutoFitSingleColumn = (ImGuiTableColumnIdx)column_n; +} + +void ImGui::TableSetColumnWidthAutoAll(ImGuiTable* table) +{ + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) // Cannot reset weight of hidden stretch column + continue; + column->CannotSkipItemsQueue = (1 << 0); + column->AutoFitQueue = (1 << 1); + } +} + +void ImGui::TableUpdateColumnsWeightFromWidth(ImGuiTable* table) +{ + IM_ASSERT(table->LeftMostStretchedColumn != -1 && table->RightMostStretchedColumn != -1); + + // Measure existing quantities + float visible_weight = 0.0f; + float visible_width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + IM_ASSERT(column->StretchWeight > 0.0f); + visible_weight += column->StretchWeight; + visible_width += column->WidthRequest; + } + IM_ASSERT(visible_weight > 0.0f && visible_width > 0.0f); + + // Apply new weights + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->StretchWeight = (column->WidthRequest / visible_width) * visible_weight; + IM_ASSERT(column->StretchWeight > 0.0f); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Drawing +//------------------------------------------------------------------------- +// - TablePushBackgroundChannel() [Internal] +// - TablePopBackgroundChannel() [Internal] +// - TableSetupDrawChannels() [Internal] +// - TableMergeDrawChannels() [Internal] +// - TableGetColumnBorderCol() [Internal] +// - TableDrawBorders() [Internal] +//------------------------------------------------------------------------- + +// Bg2 is used by Selectable (and possibly other widgets) to render to the background. +// Unlike our Bg0/1 channel which we uses for RowBg/CellBg/Borders and where we guarantee all shapes to be CPU-clipped, the Bg2 channel being widgets-facing will rely on regular ClipRect. +void ImGui::TablePushBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + table->HostBackupInnerClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, table->Bg2ClipRectForDrawCmd); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Bg2DrawChannelCurrent); +} + +void ImGui::TablePopBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, table->HostBackupInnerClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); +} + +// Allocate draw channels. Called by TableUpdateLayout() +// - We allocate them following storage order instead of display order so reordering columns won't needlessly +// increase overall dormant memory cost. +// - We isolate headers draw commands in their own channels instead of just altering clip rects. +// This is in order to facilitate merging of draw commands. +// - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels. +// - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other +// channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged. +// - We allocate 1 or 2 background draw channels. This is because we know TablePushBackgroundChannel() is only used for +// horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4). +// Draw channel allocation (before merging): +// - NoClip --> 2+D+1 channels: bg0/1 + bg2 + foreground (same clip rect == always 1 draw call) +// - Clip --> 2+D+N channels +// - FreezeRows --> 2+D+N*2 (unless scrolling value is zero) +// - FreezeRows || FreezeColunns --> 3+D+N*2 (unless scrolling value is zero) +// Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0. +void ImGui::TableSetupDrawChannels(ImGuiTable* table) +{ + const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1; + const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClip) ? 1 : table->ColumnsEnabledCount; + const int channels_for_bg = 1 + 1 * freeze_row_multiplier; + const int channels_for_dummy = (table->ColumnsEnabledCount < table->ColumnsCount || (memcmp(table->VisibleMaskByIndex, table->EnabledMaskByIndex, ImBitArrayGetStorageSizeInBytes(table->ColumnsCount)) != 0)) ? +1 : 0; + const int channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy; + table->DrawSplitter->Split(table->InnerWindow->DrawList, channels_total); + table->DummyDrawChannel = (ImGuiTableDrawChannelIdx)((channels_for_dummy > 0) ? channels_total - 1 : -1); + table->Bg2DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG2_FROZEN; + table->Bg2DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)((table->FreezeRowsCount > 0) ? 2 + channels_for_row : TABLE_DRAW_CHANNEL_BG2_FROZEN); + + int draw_channel_current = 2; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsVisibleX && column->IsVisibleY) + { + column->DrawChannelFrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current); + column->DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row + 1 : 0)); + if (!(table->Flags & ImGuiTableFlags_NoClip)) + draw_channel_current++; + } + else + { + column->DrawChannelFrozen = column->DrawChannelUnfrozen = table->DummyDrawChannel; + } + column->DrawChannelCurrent = column->DrawChannelFrozen; + } + + // Initial draw cmd starts with a BgClipRect that matches the one of its host, to facilitate merge draw commands by default. + // All our cell highlight are manually clipped with BgClipRect. When unfreezing it will be made smaller to fit scrolling rect. + // (This technically isn't part of setting up draw channels, but is reasonably related to be done here) + table->BgClipRect = table->InnerClipRect; + table->Bg0ClipRectForDrawCmd = table->OuterWindow->ClipRect; + table->Bg2ClipRectForDrawCmd = table->HostClipRect; + IM_ASSERT(table->BgClipRect.Min.y <= table->BgClipRect.Max.y); +} + +// This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable(). +// For simplicity we call it TableMergeDrawChannels() but in fact it only reorder channels + overwrite ClipRect, +// actual merging is done by table->DrawSplitter.Merge() which is called right after TableMergeDrawChannels(). +// +// Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve +// this we merge their clip rect and make them contiguous in the channel list, so they can be merged +// by the call to DrawSplitter.Merge() following to the call to this function. +// We reorder draw commands by arranging them into a maximum of 4 distinct groups: +// +// 1 group: 2 groups: 2 groups: 4 groups: +// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze +// [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll +// +// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled). +// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group +// based on its position (within frozen rows/columns groups or not). +// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect. +// This function assume that each column are pointing to a distinct draw channel, +// otherwise merge_group->ChannelsCount will not match set bit count of merge_group->ChannelsMask. +// +// Column channels will not be merged into one of the 1-4 groups in the following cases: +// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value). +// Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui:: bounds +// matches, by e.g. calling SetCursorScreenPos(). +// - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here.. +// we could do better but it's going to be rare and probably not worth the hassle. +// Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd. +// +// This function is particularly tricky to understand.. take a breath. +void ImGui::TableMergeDrawChannels(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImDrawListSplitter* splitter = table->DrawSplitter; + const bool has_freeze_v = (table->FreezeRowsCount > 0); + const bool has_freeze_h = (table->FreezeColumnsCount > 0); + IM_ASSERT(splitter->_Current == 0); + + // Track which groups we are going to attempt to merge, and which channels goes into each group. + struct MergeGroup + { + ImRect ClipRect; + int ChannelsCount = 0; + ImBitArrayPtr ChannelsMask = NULL; + }; + int merge_group_mask = 0x00; + MergeGroup merge_groups[4]; + + // Use a reusable temp buffer for the merge masks as they are dynamically sized. + const int max_draw_channels = (4 + table->ColumnsCount * 2); + const int size_for_masks_bitarrays_one = (int)ImBitArrayGetStorageSizeInBytes(max_draw_channels); + g.TempBuffer.reserve(size_for_masks_bitarrays_one * 5); + memset(g.TempBuffer.Data, 0, size_for_masks_bitarrays_one * 5); + for (int n = 0; n < IM_ARRAYSIZE(merge_groups); n++) + merge_groups[n].ChannelsMask = (ImBitArrayPtr)(void*)(g.TempBuffer.Data + (size_for_masks_bitarrays_one * n)); + ImBitArrayPtr remaining_mask = (ImBitArrayPtr)(void*)(g.TempBuffer.Data + (size_for_masks_bitarrays_one * 4)); + + // 1. Scan channels and take note of those which can be merged + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->VisibleMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const int merge_group_sub_count = has_freeze_v ? 2 : 1; + for (int merge_group_sub_n = 0; merge_group_sub_n < merge_group_sub_count; merge_group_sub_n++) + { + const int channel_no = (merge_group_sub_n == 0) ? column->DrawChannelFrozen : column->DrawChannelUnfrozen; + + // Don't attempt to merge if there are multiple draw calls within the column + ImDrawChannel* src_channel = &splitter->_Channels[channel_no]; + if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0 && src_channel->_CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() + src_channel->_CmdBuffer.pop_back(); + if (src_channel->_CmdBuffer.Size != 1) + continue; + + // Find out the width of this merge group and check if it will fit in our column + // (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it) + if (!(column->Flags & ImGuiTableColumnFlags_NoClip)) + { + float content_max_x; + if (!has_freeze_v) + content_max_x = ImMax(column->ContentMaxXUnfrozen, column->ContentMaxXHeadersUsed); // No row freeze + else if (merge_group_sub_n == 0) + content_max_x = ImMax(column->ContentMaxXFrozen, column->ContentMaxXHeadersUsed); // Row freeze: use width before freeze + else + content_max_x = column->ContentMaxXUnfrozen; // Row freeze: use width after freeze + if (content_max_x > column->ClipRect.Max.x) + continue; + } + + const int merge_group_n = (has_freeze_h && column_n < table->FreezeColumnsCount ? 0 : 1) + (has_freeze_v && merge_group_sub_n == 0 ? 0 : 2); + IM_ASSERT(channel_no < max_draw_channels); + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + merge_group->ClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + ImBitArraySetBit(merge_group->ChannelsMask, channel_no); + merge_group->ChannelsCount++; + merge_group->ClipRect.Add(src_channel->_CmdBuffer[0].ClipRect); + merge_group_mask |= (1 << merge_group_n); + } + + // Invalidate current draw channel + // (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data) + column->DrawChannelCurrent = (ImGuiTableDrawChannelIdx)-1; + } + + // [DEBUG] Display merge groups +#if 0 + if (g.IO.KeyShift) + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + continue; + char buf[32]; + ImFormatString(buf, 32, "MG%d:%d", merge_group_n, merge_group->ChannelsCount); + ImVec2 text_pos = merge_group->ClipRect.Min + ImVec2(4, 4); + ImVec2 text_size = CalcTextSize(buf, NULL); + GetForegroundDrawList()->AddRectFilled(text_pos, text_pos + text_size, IM_COL32(0, 0, 0, 255)); + GetForegroundDrawList()->AddText(text_pos, IM_COL32(255, 255, 0, 255), buf, NULL); + GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 255, 0, 255)); + } +#endif + + // 2. Rewrite channel list in our preferred order + if (merge_group_mask != 0) + { + // We skip channel 0 (Bg0/Bg1) and 1 (Bg2 frozen) from the shuffling since they won't move - see channels allocation in TableSetupDrawChannels(). + const int LEADING_DRAW_CHANNELS = 2; + g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - LEADING_DRAW_CHANNELS); // Use shared temporary storage so the allocation gets amortized + ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data; + ImBitArraySetBitRange(remaining_mask, LEADING_DRAW_CHANNELS, splitter->_Count); + ImBitArrayClearBit(remaining_mask, table->Bg2DrawChannelUnfrozen); + IM_ASSERT(has_freeze_v == false || table->Bg2DrawChannelUnfrozen != TABLE_DRAW_CHANNEL_BG2_FROZEN); + int remaining_count = splitter->_Count - (has_freeze_v ? LEADING_DRAW_CHANNELS + 1 : LEADING_DRAW_CHANNELS); + //ImRect host_rect = (table->InnerWindow == table->OuterWindow) ? table->InnerClipRect : table->HostClipRect; + ImRect host_rect = table->HostClipRect; + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + if (int merge_channels_count = merge_groups[merge_group_n].ChannelsCount) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + ImRect merge_clip_rect = merge_group->ClipRect; + + // Extend outer-most clip limits to match those of host, so draw calls can be merged even if + // outer-most columns have some outer padding offsetting them from their parent ClipRect. + // The principal cases this is dealing with are: + // - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge + // - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge + // FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit + // within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect. + if ((merge_group_n & 1) == 0 || !has_freeze_h) + merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x); + if ((merge_group_n & 2) == 0 || !has_freeze_v) + merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y); + if ((merge_group_n & 1) != 0) + merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x); + if ((merge_group_n & 2) != 0 && (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0) + merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y); + //GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, 0, 1.0f); // [DEBUG] + //GetForegroundDrawList()->AddLine(merge_group->ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200)); + //GetForegroundDrawList()->AddLine(merge_group->ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200)); + remaining_count -= merge_group->ChannelsCount; + for (int n = 0; n < (size_for_masks_bitarrays_one >> 2); n++) + remaining_mask[n] &= ~merge_group->ChannelsMask[n]; + for (int n = 0; n < splitter->_Count && merge_channels_count != 0; n++) + { + // Copy + overwrite new clip rect + if (!IM_BITARRAY_TESTBIT(merge_group->ChannelsMask, n)) + continue; + IM_BITARRAY_CLEARBIT(merge_group->ChannelsMask, n); + merge_channels_count--; + + ImDrawChannel* channel = &splitter->_Channels[n]; + IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect))); + channel->_CmdBuffer[0].ClipRect = merge_clip_rect.ToVec4(); + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + } + } + + // Make sure Bg2DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0/Bg1 and Bg2 frozen are fixed to 0 and 1) + if (merge_group_n == 1 && has_freeze_v) + memcpy(dst_tmp++, &splitter->_Channels[table->Bg2DrawChannelUnfrozen], sizeof(ImDrawChannel)); + } + + // Append unmergeable channels that we didn't reorder at the end of the list + for (int n = 0; n < splitter->_Count && remaining_count != 0; n++) + { + if (!IM_BITARRAY_TESTBIT(remaining_mask, n)) + continue; + ImDrawChannel* channel = &splitter->_Channels[n]; + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + remaining_count--; + } + IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size); + memcpy(splitter->_Channels.Data + LEADING_DRAW_CHANNELS, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - LEADING_DRAW_CHANNELS) * sizeof(ImDrawChannel)); + } +} + +static ImU32 TableGetColumnBorderCol(ImGuiTable* table, int order_n, int column_n) +{ + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (is_resized || is_hovered) + return ImGui::GetColorU32(is_resized ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + if (is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize))) + return table->BorderColorStrong; + return table->BorderColorLight; +} + +// FIXME-TABLE: This is a mess, need to redesign how we render borders (as some are also done in TableEndRow) +void ImGui::TableDrawBorders(ImGuiTable* table) +{ + ImGuiWindow* inner_window = table->InnerWindow; + if (!table->OuterWindow->ClipRect.Overlaps(table->OuterRect)) + return; + + ImDrawList* inner_drawlist = inner_window->DrawList; + table->DrawSplitter->SetCurrentChannel(inner_drawlist, TABLE_DRAW_CHANNEL_BG0); + inner_drawlist->PushClipRect(table->Bg0ClipRectForDrawCmd.Min, table->Bg0ClipRectForDrawCmd.Max, false); + + // Draw inner border and resizing feedback + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + const float border_size = TABLE_BORDER_SIZE; + const float draw_y1 = ImMax(table->InnerRect.Min.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight) + ((table->Flags & ImGuiTableFlags_BordersOuterH) ? 1.0f : 0.0f); + const float draw_y2_body = table->InnerRect.Max.y; + const float draw_y2_head = table->IsUsingHeaders ? ImMin(table->InnerRect.Max.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table_instance->LastTopHeadersRowHeight) : draw_y1; + if (table->Flags & ImGuiTableFlags_BordersInnerV) + { + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_resizable = (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) == 0; + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (column->MaxX > table->InnerClipRect.Max.x && !is_resized) + continue; + + // Decide whether right-most column is visible + if (column->NextEnabledColumn == -1 && !is_resizable) + if ((table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame || (table->Flags & ImGuiTableFlags_NoHostExtendX)) + continue; + if (column->MaxX <= column->ClipRect.Min.x) // FIXME-TABLE FIXME-STYLE: Assume BorderSize==1, this is problematic if we want to increase the border size.. + continue; + + // Draw in outer window so right-most column won't be clipped + // Always draw full height border when being resized/hovered, or on the delimitation of frozen column scrolling. + float draw_y2 = (is_hovered || is_resized || is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) == 0) ? draw_y2_body : draw_y2_head; + if (draw_y2 > draw_y1) + inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), TableGetColumnBorderCol(table, order_n, column_n), border_size); + } + } + + // Draw outer border + // FIXME: could use AddRect or explicit VLine/HLine helper? + if (table->Flags & ImGuiTableFlags_BordersOuter) + { + // Display outer border offset by 1 which is a simple way to display it without adding an extra draw call + // (Without the offset, in outer_window it would be rendered behind cells, because child windows are above their + // parent. In inner_window, it won't reach out over scrollbars. Another weird solution would be to display part + // of it in inner window, and the part that's over scrollbars in the outer window..) + // Either solution currently won't allow us to use a larger border size: the border would clipped. + const ImRect outer_border = table->OuterRect; + const ImU32 outer_col = table->BorderColorStrong; + if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter) + { + inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, 0, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterV) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Min.x, outer_border.Max.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Max.x, outer_border.Min.y), outer_border.Max, outer_col, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterH) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Max.x, outer_border.Min.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Min.x, outer_border.Max.y), outer_border.Max, outer_col, border_size); + } + } + if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y) + { + // Draw bottom-most row border between it is above outer border. + const float border_y = table->RowPosY2; + if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y) + inner_drawlist->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderColorLight, border_size); + } + + inner_drawlist->PopClipRect(); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Sorting +//------------------------------------------------------------------------- +// - TableGetSortSpecs() +// - TableFixColumnSortDirection() [Internal] +// - TableGetColumnNextSortDirection() [Internal] +// - TableSetColumnSortDirection() [Internal] +// - TableSortSpecsSanitize() [Internal] +// - TableSortSpecsBuild() [Internal] +//------------------------------------------------------------------------- + +// Return NULL if no sort specs (most often when ImGuiTableFlags_Sortable is not set) +// When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have +// changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, +// else you may wastefully sort your data every frame! +// Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable()! +ImGuiTableSortSpecs* ImGui::TableGetSortSpecs() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + + if (!(table->Flags & ImGuiTableFlags_Sortable)) + return NULL; + + // Require layout (in case TableHeadersRow() hasn't been called) as it may alter IsSortSpecsDirty in some paths. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + TableSortSpecsBuild(table); + return &table->SortSpecs; +} + +static inline ImGuiSortDirection TableGetColumnAvailSortDirection(ImGuiTableColumn* column, int n) +{ + IM_ASSERT(n < column->SortDirectionsAvailCount); + return (ImGuiSortDirection)((column->SortDirectionsAvailList >> (n << 1)) & 0x03); +} + +// Fix sort direction if currently set on a value which is unavailable (e.g. activating NoSortAscending/NoSortDescending) +void ImGui::TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column) +{ + if (column->SortOrder == -1 || (column->SortDirectionsAvailMask & (1 << column->SortDirection)) != 0) + return; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + table->IsSortSpecsDirty = true; +} + +// Calculate next sort direction that would be set after clicking the column +// - If the PreferSortDescending flag is set, we will default to a Descending direction on the first click. +// - Note that the PreferSortAscending flag is never checked, it is essentially the default and therefore a no-op. +IM_STATIC_ASSERT(ImGuiSortDirection_None == 0 && ImGuiSortDirection_Ascending == 1 && ImGuiSortDirection_Descending == 2); +ImGuiSortDirection ImGui::TableGetColumnNextSortDirection(ImGuiTableColumn* column) +{ + IM_ASSERT(column->SortDirectionsAvailCount > 0); + if (column->SortOrder == -1) + return TableGetColumnAvailSortDirection(column, 0); + for (int n = 0; n < 3; n++) + if (column->SortDirection == TableGetColumnAvailSortDirection(column, n)) + return TableGetColumnAvailSortDirection(column, (n + 1) % column->SortDirectionsAvailCount); + IM_ASSERT(0); + return ImGuiSortDirection_None; +} + +// Note that the NoSortAscending/NoSortDescending flags are processed in TableSortSpecsSanitize(), and they may change/revert +// the value of SortDirection. We could technically also do it here but it would be unnecessary and duplicate code. +void ImGui::TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!(table->Flags & ImGuiTableFlags_SortMulti)) + append_to_sort_specs = false; + if (!(table->Flags & ImGuiTableFlags_SortTristate)) + IM_ASSERT(sort_direction != ImGuiSortDirection_None); + + ImGuiTableColumnIdx sort_order_max = 0; + if (append_to_sort_specs) + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + sort_order_max = ImMax(sort_order_max, table->Columns[other_column_n].SortOrder); + + ImGuiTableColumn* column = &table->Columns[column_n]; + column->SortDirection = (ImU8)sort_direction; + if (column->SortDirection == ImGuiSortDirection_None) + column->SortOrder = -1; + else if (column->SortOrder == -1 || !append_to_sort_specs) + column->SortOrder = append_to_sort_specs ? sort_order_max + 1 : 0; + + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column != column && !append_to_sort_specs) + other_column->SortOrder = -1; + TableFixColumnSortDirection(table, other_column); + } + table->IsSettingsDirty = true; + table->IsSortSpecsDirty = true; +} + +void ImGui::TableSortSpecsSanitize(ImGuiTable* table) +{ + IM_ASSERT(table->Flags & ImGuiTableFlags_Sortable); + + // Clear SortOrder from hidden column and verify that there's no gap or duplicate. + int sort_order_count = 0; + ImU64 sort_order_mask = 0x00; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder != -1 && !column->IsEnabled) + column->SortOrder = -1; + if (column->SortOrder == -1) + continue; + sort_order_count++; + sort_order_mask |= ((ImU64)1 << column->SortOrder); + IM_ASSERT(sort_order_count < (int)sizeof(sort_order_mask) * 8); + } + + const bool need_fix_linearize = ((ImU64)1 << sort_order_count) != (sort_order_mask + 1); + const bool need_fix_single_sort_order = (sort_order_count > 1) && !(table->Flags & ImGuiTableFlags_SortMulti); + if (need_fix_linearize || need_fix_single_sort_order) + { + ImU64 fixed_mask = 0x00; + for (int sort_n = 0; sort_n < sort_order_count; sort_n++) + { + // Fix: Rewrite sort order fields if needed so they have no gap or duplicate. + // (e.g. SortOrder 0 disappeared, SortOrder 1..2 exists --> rewrite then as SortOrder 0..1) + int column_with_smallest_sort_order = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if ((fixed_mask & ((ImU64)1 << (ImU64)column_n)) == 0 && table->Columns[column_n].SortOrder != -1) + if (column_with_smallest_sort_order == -1 || table->Columns[column_n].SortOrder < table->Columns[column_with_smallest_sort_order].SortOrder) + column_with_smallest_sort_order = column_n; + IM_ASSERT(column_with_smallest_sort_order != -1); + fixed_mask |= ((ImU64)1 << column_with_smallest_sort_order); + table->Columns[column_with_smallest_sort_order].SortOrder = (ImGuiTableColumnIdx)sort_n; + + // Fix: Make sure only one column has a SortOrder if ImGuiTableFlags_MultiSortable is not set. + if (need_fix_single_sort_order) + { + sort_order_count = 1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (column_n != column_with_smallest_sort_order) + table->Columns[column_n].SortOrder = -1; + break; + } + } + } + + // Fallback default sort order (if no column with the ImGuiTableColumnFlags_DefaultSort flag) + if (sort_order_count == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + sort_order_count = 1; + column->SortOrder = 0; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + break; + } + } + + table->SortSpecsCount = (ImGuiTableColumnIdx)sort_order_count; +} + +void ImGui::TableSortSpecsBuild(ImGuiTable* table) +{ + bool dirty = table->IsSortSpecsDirty; + if (dirty) + { + TableSortSpecsSanitize(table); + table->SortSpecsMulti.resize(table->SortSpecsCount <= 1 ? 0 : table->SortSpecsCount); + table->SortSpecs.SpecsDirty = true; // Mark as dirty for user + table->IsSortSpecsDirty = false; // Mark as not dirty for us + } + + // Write output + // May be able to move all SortSpecs data from table (48 bytes) to ImGuiTableTempData if we decide to write it back on every BeginTable() + ImGuiTableColumnSortSpecs* sort_specs = (table->SortSpecsCount == 0) ? NULL : (table->SortSpecsCount == 1) ? &table->SortSpecsSingle : table->SortSpecsMulti.Data; + if (dirty && sort_specs != NULL) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder == -1) + continue; + IM_ASSERT(column->SortOrder < table->SortSpecsCount); + ImGuiTableColumnSortSpecs* sort_spec = &sort_specs[column->SortOrder]; + sort_spec->ColumnUserID = column->UserID; + sort_spec->ColumnIndex = (ImGuiTableColumnIdx)column_n; + sort_spec->SortOrder = (ImGuiTableColumnIdx)column->SortOrder; + sort_spec->SortDirection = (ImGuiSortDirection)column->SortDirection; + } + + table->SortSpecs.Specs = sort_specs; + table->SortSpecs.SpecsCount = table->SortSpecsCount; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Headers +//------------------------------------------------------------------------- +// - TableGetHeaderRowHeight() [Internal] +// - TableGetHeaderAngledMaxLabelWidth() [Internal] +// - TableHeadersRow() +// - TableHeader() +// - TableAngledHeadersRow() +// - TableAngledHeadersRowEx() [Internal] +//------------------------------------------------------------------------- + +float ImGui::TableGetHeaderRowHeight() +{ + // Caring for a minor edge case: + // Calculate row height, for the unlikely case that some labels may be taller than others. + // If we didn't do that, uneven header height would highlight but smaller one before the tallest wouldn't catch input for all height. + // In your custom header row you may omit this all together and just call TableNextRow() without a height... + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float row_height = g.FontSize; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if ((table->Columns[column_n].Flags & ImGuiTableColumnFlags_NoHeaderLabel) == 0) + row_height = ImMax(row_height, CalcTextSize(TableGetColumnName(table, column_n)).y); + return row_height + g.Style.CellPadding.y * 2.0f; +} + +float ImGui::TableGetHeaderAngledMaxLabelWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if (table->Columns[column_n].Flags & ImGuiTableColumnFlags_AngledHeader) + width = ImMax(width, CalcTextSize(TableGetColumnName(table, column_n), NULL, true).x); + return width + g.Style.CellPadding.y * 2.0f; // Swap padding +} + +// [Public] This is a helper to output TableHeader() calls based on the column names declared in TableSetupColumn(). +// The intent is that advanced users willing to create customized headers would not need to use this helper +// and can create their own! For example: TableHeader() may be preceded by Checkbox() or other custom widgets. +// See 'Demo->Tables->Custom headers' for a demonstration of implementing a custom version of this. +// This code is intentionally written to not make much use of internal functions, to give you better direction +// if you need to write your own. +// FIXME-TABLE: TableOpenContextMenu() and TableGetHeaderRowHeight() are not public. +void ImGui::TableHeadersRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + + // Call layout if not already done. This is automatically done by TableNextRow: we do it here _only_ to make + // it easier to debug-step in TableUpdateLayout(). Your own version of this function doesn't need this. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + // Open row + const float row_height = TableGetHeaderRowHeight(); + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + const float row_y1 = GetCursorScreenPos().y; + if (table->HostSkipItems) // Merely an optimization, you may skip in your own code. + return; + + const int columns_count = TableGetColumnCount(); + for (int column_n = 0; column_n < columns_count; column_n++) + { + if (!TableSetColumnIndex(column_n)) + continue; + + // Push an id to allow empty/unnamed headers. This is also idiomatic as it ensure there is a consistent ID path to access columns (for e.g. automation) + const char* name = (TableGetColumnFlags(column_n) & ImGuiTableColumnFlags_NoHeaderLabel) ? "" : TableGetColumnName(column_n); + PushID(column_n); + TableHeader(name); + PopID(); + } + + // Allow opening popup from the right-most section after the last column. + ImVec2 mouse_pos = ImGui::GetMousePos(); + if (IsMouseReleased(1) && TableGetHoveredColumn() == columns_count) + if (mouse_pos.y >= row_y1 && mouse_pos.y < row_y1 + row_height) + TableOpenContextMenu(columns_count); // Will open a non-column-specific popup. +} + +// Emit a column header (text + optional sort order) +// We cpu-clip text here so that all columns headers can be merged into a same draw call. +// Note that because of how we cpu-clip and display sorting indicators, you _cannot_ use SameLine() after a TableHeader() +void ImGui::TableHeader(const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + + IM_ASSERT(table->CurrentColumn != -1); + const int column_n = table->CurrentColumn; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Label + if (label == NULL) + label = ""; + const char* label_end = FindRenderedTextEnd(label); + ImVec2 label_size = CalcTextSize(label, label_end, true); + ImVec2 label_pos = window->DC.CursorPos; + + // If we already got a row height, there's use that. + // FIXME-TABLE: Padding problem if the correct outer-padding CellBgRect strays off our ClipRect? + ImRect cell_r = TableGetCellBgRect(table, column_n); + float label_height = ImMax(label_size.y, table->RowMinHeight - table->RowCellPaddingY * 2.0f); + + // Calculate ideal size for sort order arrow + float w_arrow = 0.0f; + float w_sort_text = 0.0f; + bool sort_arrow = false; + char sort_order_suf[4] = ""; + const float ARROW_SCALE = 0.65f; + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + w_arrow = ImTrunc(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x); + if (column->SortOrder != -1) + sort_arrow = true; + if (column->SortOrder > 0) + { + ImFormatString(sort_order_suf, IM_ARRAYSIZE(sort_order_suf), "%d", column->SortOrder + 1); + w_sort_text = g.Style.ItemInnerSpacing.x + CalcTextSize(sort_order_suf).x; + } + } + + // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considered for merging. + float max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow; + column->ContentMaxXHeadersUsed = ImMax(column->ContentMaxXHeadersUsed, sort_arrow ? cell_r.Max.x : ImMin(max_pos_x, cell_r.Max.x)); + column->ContentMaxXHeadersIdeal = ImMax(column->ContentMaxXHeadersIdeal, max_pos_x); + + // Keep header highlighted when context menu is open. + ImGuiID id = window->GetID(label); + ImRect bb(cell_r.Min.x, cell_r.Min.y, cell_r.Max.x, ImMax(cell_r.Max.y, cell_r.Min.y + label_height + g.Style.CellPadding.y * 2.0f)); + ItemSize(ImVec2(0.0f, label_height)); // Don't declare unclipped width, it'll be fed ContentMaxPosHeadersIdeal + if (!ItemAdd(bb, id)) + return; + + //GetForegroundDrawList()->AddRect(cell_r.Min, cell_r.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + //GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + + // Using AllowOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items. + const bool highlight = (table->HighlightColumnHeader == column_n); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_AllowOverlap); + if (held || hovered || highlight) + { + const ImU32 col = GetColorU32(held ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + //RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + TableSetBgColor(ImGuiTableBgTarget_CellBg, col, table->CurrentColumn); + } + else + { + // Submit single cell bg color in the case we didn't submit a full header row + if ((table->RowFlags & ImGuiTableRowFlags_Headers) == 0) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_TableHeaderBg), table->CurrentColumn); + } + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding); + if (held) + table->HeldHeaderColumn = (ImGuiTableColumnIdx)column_n; + window->DC.CursorPos.y -= g.Style.ItemSpacing.y * 0.5f; + + // Drag and drop to re-order columns. + // FIXME-TABLE: Scroll request while reordering a column and it lands out of the scrolling zone. + if (held && (table->Flags & ImGuiTableFlags_Reorderable) && IsMouseDragging(0) && !g.DragDropActive) + { + // While moving a column it will jump on the other side of the mouse, so we also test for MouseDelta.x + table->ReorderColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + + // We don't reorder: through the frozen<>unfrozen line, or through a column that is marked with ImGuiTableColumnFlags_NoReorder. + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) + if (ImGuiTableColumn* prev_column = (column->PrevEnabledColumn != -1) ? &table->Columns[column->PrevEnabledColumn] : NULL) + if (!((column->Flags | prev_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (prev_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = -1; + if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) + if (ImGuiTableColumn* next_column = (column->NextEnabledColumn != -1) ? &table->Columns[column->NextEnabledColumn] : NULL) + if (!((column->Flags | next_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (next_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = +1; + } + + // Sort order arrow + const float ellipsis_max = ImMax(cell_r.Max.x - w_arrow - w_sort_text, label_pos.x); + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + if (column->SortOrder != -1) + { + float x = ImMax(cell_r.Min.x, cell_r.Max.x - w_arrow - w_sort_text); + float y = label_pos.y; + if (column->SortOrder > 0) + { + PushStyleColor(ImGuiCol_Text, GetColorU32(ImGuiCol_Text, 0.70f)); + RenderText(ImVec2(x + g.Style.ItemInnerSpacing.x, y), sort_order_suf); + PopStyleColor(); + x += w_sort_text; + } + RenderArrow(window->DrawList, ImVec2(x, y), GetColorU32(ImGuiCol_Text), column->SortDirection == ImGuiSortDirection_Ascending ? ImGuiDir_Up : ImGuiDir_Down, ARROW_SCALE); + } + + // Handle clicking on column header to adjust Sort Order + if (pressed && table->ReorderColumn != column_n) + { + ImGuiSortDirection sort_direction = TableGetColumnNextSortDirection(column); + TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift); + } + } + + // Render clipped label. Clipping here ensure that in the majority of situations, all our header cells will + // be merged into a single draw call. + //window->DrawList->AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + label_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size); + + const bool text_clipped = label_size.x > (ellipsis_max - label_pos.x); + if (text_clipped && hovered && g.ActiveId == 0) + SetItemTooltip("%.*s", (int)(label_end - label), label); + + // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden + if (IsMouseReleased(1) && IsItemHovered()) + TableOpenContextMenu(column_n); +} + +// Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. +// FIXME: No hit-testing/button on the angled header. +void ImGui::TableAngledHeadersRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiTableTempData* temp_data = table->TempData; + temp_data->AngledHeadersRequests.resize(0); + temp_data->AngledHeadersRequests.reserve(table->ColumnsEnabledCount); + + // Which column needs highlight? + const ImGuiID row_id = GetID("##AngledHeaders"); + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + int highlight_column_n = table->HighlightColumnHeader; + if (highlight_column_n == -1 && table->HoveredColumnBody != -1) + if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) + highlight_column_n = table->HoveredColumnBody; + + // Build up request + ImU32 col_header_bg = GetColorU32(ImGuiCol_TableHeaderBg); + ImU32 col_text = GetColorU32(ImGuiCol_Text); + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if ((column->Flags & ImGuiTableColumnFlags_AngledHeader) == 0) // Note: can't rely on ImGuiTableColumnFlags_IsVisible test here. + continue; + ImGuiTableHeaderData request = { (ImGuiTableColumnIdx)column_n, col_text, col_header_bg, (column_n == highlight_column_n) ? GetColorU32(ImGuiCol_Header) : 0 }; + temp_data->AngledHeadersRequests.push_back(request); + } + + // Render row + TableAngledHeadersRowEx(row_id, g.Style.TableAngledHeadersAngle, 0.0f, temp_data->AngledHeadersRequests.Data, temp_data->AngledHeadersRequests.Size); +} + +// Important: data must be fed left to right +void ImGui::TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiWindow* window = g.CurrentWindow; + ImDrawList* draw_list = window->DrawList; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->CurrentRow == -1 && "Must be first row"); + + if (max_label_width == 0.0f) + max_label_width = TableGetHeaderAngledMaxLabelWidth(); + + // Angle argument expressed in (-IM_PI/2 .. +IM_PI/2) as it is easier to think about for user. + const bool flip_label = (angle < 0.0f); + angle -= IM_PI * 0.5f; + const float cos_a = ImCos(angle); + const float sin_a = ImSin(angle); + const float label_cos_a = flip_label ? ImCos(angle + IM_PI) : cos_a; + const float label_sin_a = flip_label ? ImSin(angle + IM_PI) : sin_a; + const ImVec2 unit_right = ImVec2(cos_a, sin_a); + + // Calculate our base metrics and set angled headers data _before_ the first call to TableNextRow() + // FIXME-STYLE: Would it be better for user to submit 'max_label_width' or 'row_height' ? One can be derived from the other. + const float header_height = g.FontSize + g.Style.CellPadding.x * 2.0f; + const float row_height = ImTrunc(ImFabs(ImRotate(ImVec2(max_label_width, flip_label ? +header_height : -header_height), cos_a, sin_a).y)); + table->AngledHeadersHeight = row_height; + table->AngledHeadersSlope = (sin_a != 0.0f) ? (cos_a / sin_a) : 0.0f; + const ImVec2 header_angled_vector = unit_right * (row_height / -sin_a); // vector from bottom-left to top-left, and from bottom-right to top-right + + // Declare row, override and draw our own background + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + TableNextColumn(); + const ImRect row_r(table->WorkRect.Min.x, table->BgClipRect.Min.y, table->WorkRect.Max.x, table->RowPosY2); + table->DrawSplitter->SetCurrentChannel(draw_list, TABLE_DRAW_CHANNEL_BG0); + float clip_rect_min_x = table->BgClipRect.Min.x; + if (table->FreezeColumnsCount > 0) + clip_rect_min_x = ImMax(clip_rect_min_x, table->Columns[table->FreezeColumnsCount - 1].MaxX); + TableSetBgColor(ImGuiTableBgTarget_RowBg0, 0); // Cancel + PushClipRect(table->BgClipRect.Min, table->BgClipRect.Max, false); // Span all columns + draw_list->AddRectFilled(ImVec2(table->BgClipRect.Min.x, row_r.Min.y), ImVec2(table->BgClipRect.Max.x, row_r.Max.y), GetColorU32(ImGuiCol_TableHeaderBg, 0.25f)); // FIXME-STYLE: Change row background with an arbitrary color. + PushClipRect(ImVec2(clip_rect_min_x, table->BgClipRect.Min.y), table->BgClipRect.Max, true); // Span all columns + + ButtonBehavior(row_r, row_id, NULL, NULL); + KeepAliveID(row_id); + + const float ascent_scaled = g.Font->Ascent * g.FontScale; // FIXME: Standardize those scaling factors better + const float line_off_for_ascent_x = (ImMax((g.FontSize - ascent_scaled) * 0.5f, 0.0f) / -sin_a) * (flip_label ? -1.0f : 1.0f); + const ImVec2 padding = g.Style.CellPadding; // We will always use swapped component + const ImVec2 align = g.Style.TableAngledHeadersTextAlign; + + // Draw background and labels in first pass, then all borders. + float max_x = -FLT_MAX; + for (int pass = 0; pass < 2; pass++) + for (int order_n = 0; order_n < data_count; order_n++) + { + const ImGuiTableHeaderData* request = &data[order_n]; + const int column_n = request->Index; + ImGuiTableColumn* column = &table->Columns[column_n]; + + ImVec2 bg_shape[4]; + bg_shape[0] = ImVec2(column->MaxX, row_r.Max.y); + bg_shape[1] = ImVec2(column->MinX, row_r.Max.y); + bg_shape[2] = bg_shape[1] + header_angled_vector; + bg_shape[3] = bg_shape[0] + header_angled_vector; + if (pass == 0) + { + // Draw shape + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor0); + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor1); // Optional highlight + max_x = ImMax(max_x, bg_shape[3].x); + + // Draw label + // - First draw at an offset where RenderTextXXX() function won't meddle with applying current ClipRect, then transform to final offset. + // - Handle multiple lines manually, as we want each lines to follow on the horizontal border, rather than see a whole block rotated. + const char* label_name = TableGetColumnName(table, column_n); + const char* label_name_end = FindRenderedTextEnd(label_name); + const float line_off_step_x = (g.FontSize / -sin_a); + const int label_lines = ImTextCountLines(label_name, label_name_end); + + // Left<>Right alignment + float line_off_curr_x = flip_label ? (label_lines - 1) * line_off_step_x : 0.0f; + float line_off_for_align_x = ImMax((((column->MaxX - column->MinX) - padding.x * 2.0f) - (label_lines * line_off_step_x)), 0.0f) * align.x; + line_off_curr_x += line_off_for_align_x - line_off_for_ascent_x; + + // Register header width + column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX + ImCeil(label_lines * line_off_step_x - line_off_for_align_x); + + while (label_name < label_name_end) + { + const char* label_name_eol = strchr(label_name, '\n'); + if (label_name_eol == NULL) + label_name_eol = label_name_end; + + // FIXME: Individual line clipping for right-most column is broken for negative angles. + ImVec2 label_size = CalcTextSize(label_name, label_name_eol); + float clip_width = max_label_width - padding.y; // Using padding.y*2.0f would be symmetrical but hide more text. + float clip_height = ImMin(label_size.y, column->ClipRect.Max.x - column->WorkMinX - line_off_curr_x); + ImRect clip_r(window->ClipRect.Min, window->ClipRect.Min + ImVec2(clip_width, clip_height)); + int vtx_idx_begin = draw_list->_VtxCurrentIdx; + PushStyleColor(ImGuiCol_Text, request->TextColor); + RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, clip_r.Max.x, label_name, label_name_eol, &label_size); + PopStyleColor(); + int vtx_idx_end = draw_list->_VtxCurrentIdx; + + // Up<>Down alignment + const float available_space = ImMax(clip_width - label_size.x + ImAbs(padding.x * cos_a) * 2.0f - ImAbs(padding.y * sin_a) * 2.0f, 0.0f); + const float vertical_offset = available_space * align.y * (flip_label ? -1.0f : 1.0f); + + // Rotate and offset label + ImVec2 pivot_in = ImVec2(window->ClipRect.Min.x - vertical_offset, window->ClipRect.Min.y + label_size.y); + ImVec2 pivot_out = ImVec2(column->WorkMinX, row_r.Max.y); + line_off_curr_x += flip_label ? -line_off_step_x : line_off_step_x; + pivot_out += unit_right * padding.y; + if (flip_label) + pivot_out += unit_right * (clip_width - ImMax(0.0f, clip_width - label_size.x)); + pivot_out.x += flip_label ? line_off_curr_x + line_off_step_x : line_off_curr_x; + ShadeVertsTransformPos(draw_list, vtx_idx_begin, vtx_idx_end, pivot_in, label_cos_a, label_sin_a, pivot_out); // Rotate and offset + //if (g.IO.KeyShift) { ImDrawList* fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl->_VtxCurrentIdx; fg_dl->AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0f, 0, 1.0f); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl->_VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out); } + + label_name = label_name_eol + 1; + } + } + if (pass == 1) + { + // Draw border + draw_list->AddLine(bg_shape[0], bg_shape[3], TableGetColumnBorderCol(table, order_n, column_n)); + } + } + PopClipRect(); + PopClipRect(); + table->TempData->AngledHeadersExtraWidth = ImMax(0.0f, max_x - table->Columns[table->RightMostEnabledColumn].MaxX); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Context Menu +//------------------------------------------------------------------------- +// - TableOpenContextMenu() [Internal] +// - TableBeginContextMenuPopup() [Internal] +// - TableDrawDefaultContextMenu() [Internal] +//------------------------------------------------------------------------- + +// Use -1 to open menu not specific to a given column. +void ImGui::TableOpenContextMenu(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (column_n == -1 && table->CurrentColumn != -1) // When called within a column automatically use this one (for consistency) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) // To facilitate using with TableGetHoveredColumn() + column_n = -1; + IM_ASSERT(column_n >= -1 && column_n < table->ColumnsCount); + if (table->Flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + table->IsContextPopupOpen = true; + table->ContextPopupColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + OpenPopupEx(context_menu_id, ImGuiPopupFlags_None); + } +} + +bool ImGui::TableBeginContextMenuPopup(ImGuiTable* table) +{ + if (!table->IsContextPopupOpen || table->InstanceCurrent != table->InstanceInteracted) + return false; + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + if (BeginPopupEx(context_menu_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) + return true; + table->IsContextPopupOpen = false; + return false; +} + +// Output context menu into current window (generally a popup) +// FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? +// Sections to display are pulled from 'flags_for_section_to_display', which is typically == table->Flags. +// - ImGuiTableFlags_Resizable -> display Sizing menu items +// - ImGuiTableFlags_Reorderable -> display "Reset Order" +////- ImGuiTableFlags_Sortable -> display sorting options (disabled) +// - ImGuiTableFlags_Hideable -> display columns visibility menu items +// It means if you have a custom context menus you can call this section and omit some sections, and add your own. +void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + bool want_separator = false; + const int column_n = (table->ContextPopupColumn >= 0 && table->ContextPopupColumn < table->ColumnsCount) ? table->ContextPopupColumn : -1; + ImGuiTableColumn* column = (column_n != -1) ? &table->Columns[column_n] : NULL; + + // Sizing + if (flags_for_section_to_display & ImGuiTableFlags_Resizable) + { + if (column != NULL) + { + const bool can_resize = !(column->Flags & ImGuiTableColumnFlags_NoResize) && column->IsEnabled; + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableSizeOne), NULL, false, can_resize)) // "###SizeOne" + TableSetColumnWidthAutoSingle(table, column_n); + } + + const char* size_all_desc; + if (table->ColumnsEnabledFixedCount == table->ColumnsEnabledCount && (table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame) + size_all_desc = LocalizeGetMsg(ImGuiLocKey_TableSizeAllFit); // "###SizeAll" All fixed + else + size_all_desc = LocalizeGetMsg(ImGuiLocKey_TableSizeAllDefault); // "###SizeAll" All stretch or mixed + if (MenuItem(size_all_desc, NULL)) + TableSetColumnWidthAutoAll(table); + want_separator = true; + } + + // Ordering + if (flags_for_section_to_display & ImGuiTableFlags_Reorderable) + { + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableResetOrder), NULL, false, !table->IsDefaultDisplayOrder)) + table->IsResetDisplayOrderRequest = true; + want_separator = true; + } + + // Reset all (should work but seems unnecessary/noisy to expose?) + //if (MenuItem("Reset all")) + // table->IsResetAllRequest = true; + + // Sorting + // (modify TableOpenContextMenu() to add _Sortable flag if enabling this) +#if 0 + if ((flags_for_section_to_display & ImGuiTableFlags_Sortable) && column != NULL && (column->Flags & ImGuiTableColumnFlags_NoSort) == 0) + { + if (want_separator) + Separator(); + want_separator = true; + + bool append_to_sort_specs = g.IO.KeyShift; + if (MenuItem("Sort in Ascending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Ascending, (column->Flags & ImGuiTableColumnFlags_NoSortAscending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Ascending, append_to_sort_specs); + if (MenuItem("Sort in Descending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Descending, (column->Flags & ImGuiTableColumnFlags_NoSortDescending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Descending, append_to_sort_specs); + } +#endif + + // Hiding / Visibility + if (flags_for_section_to_display & ImGuiTableFlags_Hideable) + { + if (want_separator) + Separator(); + want_separator = true; + + PushItemFlag(ImGuiItemFlags_AutoClosePopups, false); + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column->Flags & ImGuiTableColumnFlags_Disabled) + continue; + + const char* name = TableGetColumnName(table, other_column_n); + if (name == NULL || name[0] == 0) + name = ""; + + // Make sure we can't hide the last active column + bool menu_item_active = (other_column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + if (other_column->IsUserEnabled && table->ColumnsEnabledCount <= 1) + menu_item_active = false; + if (MenuItem(name, NULL, other_column->IsUserEnabled, menu_item_active)) + other_column->IsUserEnabledNextFrame = !other_column->IsUserEnabled; + } + PopItemFlag(); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Settings (.ini data) +//------------------------------------------------------------------------- +// FIXME: The binding/finding/creating flow are too confusing. +//------------------------------------------------------------------------- +// - TableSettingsInit() [Internal] +// - TableSettingsCalcChunkSize() [Internal] +// - TableSettingsCreate() [Internal] +// - TableSettingsFindByID() [Internal] +// - TableGetBoundSettings() [Internal] +// - TableResetSettings() +// - TableSaveSettings() [Internal] +// - TableLoadSettings() [Internal] +// - TableSettingsHandler_ClearAll() [Internal] +// - TableSettingsHandler_ApplyAll() [Internal] +// - TableSettingsHandler_ReadOpen() [Internal] +// - TableSettingsHandler_ReadLine() [Internal] +// - TableSettingsHandler_WriteAll() [Internal] +// - TableSettingsInstallHandler() [Internal] +//------------------------------------------------------------------------- +// [Init] 1: TableSettingsHandler_ReadXXXX() Load and parse .ini file into TableSettings. +// [Main] 2: TableLoadSettings() When table is created, bind Table to TableSettings, serialize TableSettings data into Table. +// [Main] 3: TableSaveSettings() When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty. +// [Main] 4: TableSettingsHandler_WriteAll() When .ini file is dirty (which can come from other source), save TableSettings into .ini file. +//------------------------------------------------------------------------- + +// Clear and initialize empty settings instance +static void TableSettingsInit(ImGuiTableSettings* settings, ImGuiID id, int columns_count, int columns_count_max) +{ + IM_PLACEMENT_NEW(settings) ImGuiTableSettings(); + ImGuiTableColumnSettings* settings_column = settings->GetColumnSettings(); + for (int n = 0; n < columns_count_max; n++, settings_column++) + IM_PLACEMENT_NEW(settings_column) ImGuiTableColumnSettings(); + settings->ID = id; + settings->ColumnsCount = (ImGuiTableColumnIdx)columns_count; + settings->ColumnsCountMax = (ImGuiTableColumnIdx)columns_count_max; + settings->WantApply = true; +} + +static size_t TableSettingsCalcChunkSize(int columns_count) +{ + return sizeof(ImGuiTableSettings) + (size_t)columns_count * sizeof(ImGuiTableColumnSettings); +} + +ImGuiTableSettings* ImGui::TableSettingsCreate(ImGuiID id, int columns_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.alloc_chunk(TableSettingsCalcChunkSize(columns_count)); + TableSettingsInit(settings, id, columns_count, columns_count); + return settings; +} + +// Find existing settings +ImGuiTableSettings* ImGui::TableSettingsFindByID(ImGuiID id) +{ + // FIXME-OPT: Might want to store a lookup map for this? + ImGuiContext& g = *GImGui; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID == id) + return settings; + return NULL; +} + +// Get settings for a given table, NULL if none +ImGuiTableSettings* ImGui::TableGetBoundSettings(ImGuiTable* table) +{ + if (table->SettingsOffset != -1) + { + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + IM_ASSERT(settings->ID == table->ID); + if (settings->ColumnsCountMax >= table->ColumnsCount) + return settings; // OK + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return NULL; +} + +// Restore initial state of table (with or without saved settings) +void ImGui::TableResetSettings(ImGuiTable* table) +{ + table->IsInitializing = table->IsSettingsDirty = true; + table->IsResetAllRequest = false; + table->IsSettingsRequestLoad = false; // Don't reload from ini + table->SettingsLoadedFlags = ImGuiTableFlags_None; // Mark as nothing loaded so our initialized data becomes authoritative +} + +void ImGui::TableSaveSettings(ImGuiTable* table) +{ + table->IsSettingsDirty = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind or create settings data + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = TableGetBoundSettings(table); + if (settings == NULL) + { + settings = TableSettingsCreate(table->ID, table->ColumnsCount); + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + settings->ColumnsCount = (ImGuiTableColumnIdx)table->ColumnsCount; + + // Serialize ImGuiTable/ImGuiTableColumn into ImGuiTableSettings/ImGuiTableColumnSettings + IM_ASSERT(settings->ID == table->ID); + IM_ASSERT(settings->ColumnsCount == table->ColumnsCount && settings->ColumnsCountMax >= settings->ColumnsCount); + ImGuiTableColumn* column = table->Columns.Data; + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + + bool save_ref_scale = false; + settings->SaveFlags = ImGuiTableFlags_None; + for (int n = 0; n < table->ColumnsCount; n++, column++, column_settings++) + { + const float width_or_weight = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? column->StretchWeight : column->WidthRequest; + column_settings->WidthOrWeight = width_or_weight; + column_settings->Index = (ImGuiTableColumnIdx)n; + column_settings->DisplayOrder = column->DisplayOrder; + column_settings->SortOrder = column->SortOrder; + column_settings->SortDirection = column->SortDirection; + column_settings->IsEnabled = column->IsUserEnabled; + column_settings->IsStretch = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? 1 : 0; + if ((column->Flags & ImGuiTableColumnFlags_WidthStretch) == 0) + save_ref_scale = true; + + // We skip saving some data in the .ini file when they are unnecessary to restore our state. + // Note that fixed width where initial width was derived from auto-fit will always be saved as InitStretchWeightOrWidth will be 0.0f. + // FIXME-TABLE: We don't have logic to easily compare SortOrder to DefaultSortOrder yet so it's always saved when present. + if (width_or_weight != column->InitStretchWeightOrWidth) + settings->SaveFlags |= ImGuiTableFlags_Resizable; + if (column->DisplayOrder != n) + settings->SaveFlags |= ImGuiTableFlags_Reorderable; + if (column->SortOrder != -1) + settings->SaveFlags |= ImGuiTableFlags_Sortable; + if (column->IsUserEnabled != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) == 0)) + settings->SaveFlags |= ImGuiTableFlags_Hideable; + } + settings->SaveFlags &= table->Flags; + settings->RefScale = save_ref_scale ? table->RefScale : 0.0f; + + MarkIniSettingsDirty(); +} + +void ImGui::TableLoadSettings(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + table->IsSettingsRequestLoad = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind settings + ImGuiTableSettings* settings; + if (table->SettingsOffset == -1) + { + settings = TableSettingsFindByID(table->ID); + if (settings == NULL) + return; + if (settings->ColumnsCount != table->ColumnsCount) // Allow settings if columns count changed. We could otherwise decide to return... + table->IsSettingsDirty = true; + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + else + { + settings = TableGetBoundSettings(table); + } + + table->SettingsLoadedFlags = settings->SaveFlags; + table->RefScale = settings->RefScale; + + // Initialize default columns settings + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + TableInitColumnDefaults(table, column, ~0); + column->AutoFitQueue = 0x00; + } + + // Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + ImU64 display_order_mask = 0; + for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + { + int column_n = column_settings->Index; + if (column_n < 0 || column_n >= table->ColumnsCount) + continue; + + ImGuiTableColumn* column = &table->Columns[column_n]; + if (settings->SaveFlags & ImGuiTableFlags_Resizable) + { + if (column_settings->IsStretch) + column->StretchWeight = column_settings->WidthOrWeight; + else + column->WidthRequest = column_settings->WidthOrWeight; + } + if (settings->SaveFlags & ImGuiTableFlags_Reorderable) + column->DisplayOrder = column_settings->DisplayOrder; + display_order_mask |= (ImU64)1 << column->DisplayOrder; + if ((settings->SaveFlags & ImGuiTableFlags_Hideable) && column_settings->IsEnabled != -1) + column->IsUserEnabled = column->IsUserEnabledNextFrame = column_settings->IsEnabled == 1; + column->SortOrder = column_settings->SortOrder; + column->SortDirection = column_settings->SortDirection; + } + + // Validate and fix invalid display order data + const ImU64 expected_display_order_mask = (settings->ColumnsCount == 64) ? ~0 : ((ImU64)1 << settings->ColumnsCount) - 1; + if (display_order_mask != expected_display_order_mask) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->Columns[column_n].DisplayOrder = (ImGuiTableColumnIdx)column_n; + + // Rebuild index + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; +} + +static void TableSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + table->SettingsOffset = -1; + g.SettingsTables.clear(); +} + +// Apply to existing windows (if any) +static void TableSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + { + table->IsSettingsRequestLoad = true; + table->SettingsOffset = -1; + } +} + +static void* TableSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = 0; + int columns_count = 0; + if (sscanf(name, "0x%08X,%d", &id, &columns_count) < 2) + return NULL; + + if (ImGuiTableSettings* settings = ImGui::TableSettingsFindByID(id)) + { + if (settings->ColumnsCountMax >= columns_count) + { + TableSettingsInit(settings, id, columns_count, settings->ColumnsCountMax); // Recycle + return settings; + } + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return ImGui::TableSettingsCreate(id, columns_count); +} + +static void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + ImGuiTableSettings* settings = (ImGuiTableSettings*)entry; + float f = 0.0f; + int column_n = 0, r = 0, n = 0; + + if (sscanf(line, "RefScale=%f", &f) == 1) { settings->RefScale = f; return; } + + if (sscanf(line, "Column %d%n", &column_n, &r) == 1) + { + if (column_n < 0 || column_n >= settings->ColumnsCount) + return; + line = ImStrSkipBlank(line + r); + char c = 0; + ImGuiTableColumnSettings* column = settings->GetColumnSettings() + column_n; + column->Index = (ImGuiTableColumnIdx)column_n; + if (sscanf(line, "UserID=0x%08X%n", (ImU32*)&n, &r)==1) { line = ImStrSkipBlank(line + r); column->UserID = (ImGuiID)n; } + if (sscanf(line, "Width=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = (float)n; column->IsStretch = 0; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Weight=%f%n", &f, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = f; column->IsStretch = 1; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->IsEnabled = (ImU8)n; settings->SaveFlags |= ImGuiTableFlags_Hideable; } + if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (ImGuiTableColumnIdx)n; settings->SaveFlags |= ImGuiTableFlags_Reorderable; } + if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (ImGuiTableColumnIdx)n; column->SortDirection = (c == '^') ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending; settings->SaveFlags |= ImGuiTableFlags_Sortable; } + } +} + +static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + if (settings->ID == 0) // Skip ditched settings + continue; + + // TableSaveSettings() may clear some of those flags when we establish that the data can be stripped + // (e.g. Order was unchanged) + const bool save_size = (settings->SaveFlags & ImGuiTableFlags_Resizable) != 0; + const bool save_visible = (settings->SaveFlags & ImGuiTableFlags_Hideable) != 0; + const bool save_order = (settings->SaveFlags & ImGuiTableFlags_Reorderable) != 0; + const bool save_sort = (settings->SaveFlags & ImGuiTableFlags_Sortable) != 0; + // We need to save the [Table] entry even if all the bools are false, since this records a table with "default settings". + + buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve + buf->appendf("[%s][0x%08X,%d]\n", handler->TypeName, settings->ID, settings->ColumnsCount); + if (settings->RefScale != 0.0f) + buf->appendf("RefScale=%g\n", settings->RefScale); + ImGuiTableColumnSettings* column = settings->GetColumnSettings(); + for (int column_n = 0; column_n < settings->ColumnsCount; column_n++, column++) + { + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + bool save_column = column->UserID != 0 || save_size || save_visible || save_order || (save_sort && column->SortOrder != -1); + if (!save_column) + continue; + buf->appendf("Column %-2d", column_n); + if (column->UserID != 0) { buf->appendf(" UserID=%08X", column->UserID); } + if (save_size && column->IsStretch) { buf->appendf(" Weight=%.4f", column->WidthOrWeight); } + if (save_size && !column->IsStretch) { buf->appendf(" Width=%d", (int)column->WidthOrWeight); } + if (save_visible) { buf->appendf(" Visible=%d", column->IsEnabled); } + if (save_order) { buf->appendf(" Order=%d", column->DisplayOrder); } + if (save_sort && column->SortOrder != -1) { buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? 'v' : '^'); } + buf->append("\n"); + } + buf->append("\n"); + } +} + +void ImGui::TableSettingsAddSettingsHandler() +{ + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Table"; + ini_handler.TypeHash = ImHashStr("Table"); + ini_handler.ClearAllFn = TableSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = TableSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = TableSettingsHandler_WriteAll; + AddSettingsHandler(&ini_handler); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Garbage Collection +//------------------------------------------------------------------------- +// - TableRemove() [Internal] +// - TableGcCompactTransientBuffers() [Internal] +// - TableGcCompactSettings() [Internal] +//------------------------------------------------------------------------- + +// Remove Table (currently only used by TestEngine) +void ImGui::TableRemove(ImGuiTable* table) +{ + //IMGUI_DEBUG_PRINT("TableRemove() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + int table_idx = g.Tables.GetIndex(table); + //memset(table->RawData.Data, 0, table->RawData.size_in_bytes()); + //memset(table, 0, sizeof(ImGuiTable)); + g.Tables.Remove(table->ID, table); + g.TablesLastTimeActive[table_idx] = -1.0f; +} + +// Free up/compact internal Table buffers for when it gets unused +void ImGui::TableGcCompactTransientBuffers(ImGuiTable* table) +{ + //IMGUI_DEBUG_PRINT("TableGcCompactTransientBuffers() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + IM_ASSERT(table->MemoryCompacted == false); + table->SortSpecs.Specs = NULL; + table->SortSpecsMulti.clear(); + table->IsSortSpecsDirty = true; // FIXME: In theory shouldn't have to leak into user performing a sort on resume. + table->ColumnsNames.clear(); + table->MemoryCompacted = true; + for (int n = 0; n < table->ColumnsCount; n++) + table->Columns[n].NameOffset = -1; + g.TablesLastTimeActive[g.Tables.GetIndex(table)] = -1.0f; +} + +void ImGui::TableGcCompactTransientBuffers(ImGuiTableTempData* temp_data) +{ + temp_data->DrawSplitter.ClearFreeMemory(); + temp_data->LastTimeActive = -1.0f; +} + +// Compact and remove unused settings data (currently only used by TestEngine) +void ImGui::TableGcCompactSettings() +{ + ImGuiContext& g = *GImGui; + int required_memory = 0; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + required_memory += (int)TableSettingsCalcChunkSize(settings->ColumnsCount); + if (required_memory == g.SettingsTables.Buf.Size) + return; + ImChunkStream new_chunk_stream; + new_chunk_stream.Buf.reserve(required_memory); + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + memcpy(new_chunk_stream.alloc_chunk(TableSettingsCalcChunkSize(settings->ColumnsCount)), settings, TableSettingsCalcChunkSize(settings->ColumnsCount)); + g.SettingsTables.swap(new_chunk_stream); +} + + +//------------------------------------------------------------------------- +// [SECTION] Tables: Debugging +//------------------------------------------------------------------------- +// - DebugNodeTable() [Internal] +//------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + +static const char* DebugNodeTableGetSizingPolicyDesc(ImGuiTableFlags sizing_policy) +{ + sizing_policy &= ImGuiTableFlags_SizingMask_; + if (sizing_policy == ImGuiTableFlags_SizingFixedFit) { return "FixedFit"; } + if (sizing_policy == ImGuiTableFlags_SizingFixedSame) { return "FixedSame"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchProp) { return "StretchProp"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchSame) { return "StretchSame"; } + return "N/A"; +} + +void ImGui::DebugNodeTable(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + const bool is_active = (table->LastFrameActive >= g.FrameCount - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(table, "Table 0x%08X (%d columns, in '%s')%s", table->ID, table->ColumnsCount, table->OuterWindow->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min, table->OuterRect.Max, IM_COL32(255, 255, 0, 255)); + if (IsItemVisible() && table->HoveredColumnBody != -1) + GetForegroundDrawList()->AddRect(GetItemRectMin(), GetItemRectMax(), IM_COL32(255, 255, 0, 255)); + if (!open) + return; + if (table->InstanceCurrent > 0) + Text("** %d instances of same table! Some data below will refer to last instance.", table->InstanceCurrent + 1); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + if (DebugBreakButton("**DebugBreak**", "in BeginTable()")) + g.DebugBreakInTable = table->ID; + SameLine(); + } + + bool clear_settings = SmallButton("Clear settings"); + BulletText("OuterRect: Pos: (%.1f,%.1f) Size: (%.1f,%.1f) Sizing: '%s'", table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.GetWidth(), table->OuterRect.GetHeight(), DebugNodeTableGetSizingPolicyDesc(table->Flags)); + BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); + BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); + BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); + BulletText("ResizedColumn: %d, ReorderColumn: %d, HeldHeaderColumn: %d", table->ResizedColumn, table->ReorderColumn, table->HeldHeaderColumn); + for (int n = 0; n < table->InstanceCurrent + 1; n++) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); + BulletText("Instance %d: HoveredRow: %d, LastOuterHeight: %.2f", n, table_instance->HoveredRowLast, table_instance->LastOuterHeight); + } + //BulletText("BgDrawChannels: %d/%d", 0, table->BgDrawChannelUnfrozen); + float sum_weights = 0.0f; + for (int n = 0; n < table->ColumnsCount; n++) + if (table->Columns[n].Flags & ImGuiTableColumnFlags_WidthStretch) + sum_weights += table->Columns[n].StretchWeight; + for (int n = 0; n < table->ColumnsCount; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + const char* name = TableGetColumnName(table, n); + char buf[512]; + ImFormatString(buf, IM_ARRAYSIZE(buf), + "Column %d order %d '%s': offset %+.2f to %+.2f%s\n" + "Enabled: %d, VisibleX/Y: %d/%d, RequestOutput: %d, SkipItems: %d, DrawChannels: %d,%d\n" + "WidthGiven: %.1f, Request/Auto: %.1f/%.1f, StretchWeight: %.3f (%.1f%%)\n" + "MinX: %.1f, MaxX: %.1f (%+.1f), ClipRect: %.1f to %.1f (+%.1f)\n" + "ContentWidth: %.1f,%.1f, HeadersUsed/Ideal %.1f/%.1f\n" + "Sort: %d%s, UserID: 0x%08X, Flags: 0x%04X: %s%s%s..", + n, column->DisplayOrder, name, column->MinX - table->WorkRect.Min.x, column->MaxX - table->WorkRect.Min.x, (n < table->FreezeColumnsRequest) ? " (Frozen)" : "", + column->IsEnabled, column->IsVisibleX, column->IsVisibleY, column->IsRequestOutput, column->IsSkipItems, column->DrawChannelFrozen, column->DrawChannelUnfrozen, + column->WidthGiven, column->WidthRequest, column->WidthAuto, column->StretchWeight, column->StretchWeight > 0.0f ? (column->StretchWeight / sum_weights) * 100.0f : 0.0f, + column->MinX, column->MaxX, column->MaxX - column->MinX, column->ClipRect.Min.x, column->ClipRect.Max.x, column->ClipRect.Max.x - column->ClipRect.Min.x, + column->ContentMaxXFrozen - column->WorkMinX, column->ContentMaxXUnfrozen - column->WorkMinX, column->ContentMaxXHeadersUsed - column->WorkMinX, column->ContentMaxXHeadersIdeal - column->WorkMinX, + column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? " (Asc)" : (column->SortDirection == ImGuiSortDirection_Descending) ? " (Des)" : "", column->UserID, column->Flags, + (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? "WidthStretch " : "", + (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? "WidthFixed " : "", + (column->Flags & ImGuiTableColumnFlags_NoResize) ? "NoResize " : ""); + Bullet(); + Selectable(buf); + if (IsItemHovered()) + { + ImRect r(column->MinX, table->OuterRect.Min.y, column->MaxX, table->OuterRect.Max.y); + GetForegroundDrawList()->AddRect(r.Min, r.Max, IM_COL32(255, 255, 0, 255)); + } + } + if (ImGuiTableSettings* settings = TableGetBoundSettings(table)) + DebugNodeTableSettings(settings); + if (clear_settings) + table->IsResetAllRequest = true; + TreePop(); +} + +void ImGui::DebugNodeTableSettings(ImGuiTableSettings* settings) +{ + if (!TreeNode((void*)(intptr_t)settings->ID, "Settings 0x%08X (%d columns)", settings->ID, settings->ColumnsCount)) + return; + BulletText("SaveFlags: 0x%08X", settings->SaveFlags); + BulletText("ColumnsCount: %d (max %d)", settings->ColumnsCount, settings->ColumnsCountMax); + for (int n = 0; n < settings->ColumnsCount; n++) + { + ImGuiTableColumnSettings* column_settings = &settings->GetColumnSettings()[n]; + ImGuiSortDirection sort_dir = (column_settings->SortOrder != -1) ? (ImGuiSortDirection)column_settings->SortDirection : ImGuiSortDirection_None; + BulletText("Column %d Order %d SortOrder %d %s Vis %d %s %7.3f UserID 0x%08X", + n, column_settings->DisplayOrder, column_settings->SortOrder, + (sort_dir == ImGuiSortDirection_Ascending) ? "Asc" : (sort_dir == ImGuiSortDirection_Descending) ? "Des" : "---", + column_settings->IsEnabled, column_settings->IsStretch ? "Weight" : "Width ", column_settings->WidthOrWeight, column_settings->UserID); + } + TreePop(); +} + +#else // #ifndef IMGUI_DISABLE_DEBUG_TOOLS + +void ImGui::DebugNodeTable(ImGuiTable*) {} +void ImGui::DebugNodeTableSettings(ImGuiTableSettings*) {} + +#endif + + +//------------------------------------------------------------------------- +// [SECTION] Columns, BeginColumns, EndColumns, etc. +// (This is a legacy API, prefer using BeginTable/EndTable!) +//------------------------------------------------------------------------- +// FIXME: sizing is lossy when columns width is very small (default width may turn negative etc.) +//------------------------------------------------------------------------- +// - SetWindowClipRectBeforeSetChannel() [Internal] +// - GetColumnIndex() +// - GetColumnsCount() +// - GetColumnOffset() +// - GetColumnWidth() +// - SetColumnOffset() +// - SetColumnWidth() +// - PushColumnClipRect() [Internal] +// - PushColumnsBackground() [Internal] +// - PopColumnsBackground() [Internal] +// - FindOrCreateColumns() [Internal] +// - GetColumnsID() [Internal] +// - BeginColumns() +// - NextColumn() +// - EndColumns() +// - Columns() +//------------------------------------------------------------------------- + +// [Internal] Small optimization to avoid calls to PopClipRect/SetCurrentChannel/PushClipRect in sequences, +// they would meddle many times with the underlying ImDrawCmd. +// Instead, we do a preemptive overwrite of clipping rectangle _without_ altering the command-buffer and let +// the subsequent single call to SetCurrentChannel() does it things once. +void ImGui::SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect) +{ + ImVec4 clip_rect_vec4 = clip_rect.ToVec4(); + window->ClipRect = clip_rect; + window->DrawList->_CmdHeader.ClipRect = clip_rect_vec4; + window->DrawList->_ClipRectStack.Data[window->DrawList->_ClipRectStack.Size - 1] = clip_rect_vec4; +} + +int ImGui::GetColumnIndex() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Current : 0; +} + +int ImGui::GetColumnsCount() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Count : 1; +} + +float ImGui::GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm) +{ + return offset_norm * (columns->OffMaxX - columns->OffMinX); +} + +float ImGui::GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset) +{ + return offset / (columns->OffMaxX - columns->OffMinX); +} + +static const float COLUMNS_HIT_RECT_HALF_THICKNESS = 4.0f; + +static float GetDraggedColumnOffset(ImGuiOldColumns* columns, int column_index) +{ + // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing + // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(column_index > 0); // We are not supposed to drag column 0. + IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); + + float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale) - window->Pos.x; + x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); + if ((columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths)) + x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); + + return x; +} + +float ImGui::GetColumnOffset(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return 0.0f; + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const float t = columns->Columns[column_index].OffsetNorm; + const float x_offset = ImLerp(columns->OffMinX, columns->OffMaxX, t); + return x_offset; +} + +static float GetColumnWidthEx(ImGuiOldColumns* columns, int column_index, bool before_resize = false) +{ + if (column_index < 0) + column_index = columns->Current; + + float offset_norm; + if (before_resize) + offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; + else + offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; + return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); +} + +float ImGui::GetColumnWidth(int column_index) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return GetContentRegionAvail().x; + + if (column_index < 0) + column_index = columns->Current; + return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); +} + +void ImGui::SetColumnOffset(int column_index, float offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const bool preserve_width = !(columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths) && (column_index < columns->Count - 1); + const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; + + if (!(columns->Flags & ImGuiOldColumnFlags_NoForceWithinWindow)) + offset = ImMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); + columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX); + + if (preserve_width) + SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); +} + +void ImGui::SetColumnWidth(int column_index, float width) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); +} + +void ImGui::PushColumnClipRect(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (column_index < 0) + column_index = columns->Current; + + ImGuiOldColumnData* column = &columns->Columns[column_index]; + PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false); +} + +// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns) +void ImGui::PushColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + columns->HostBackupClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, columns->HostInitialClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, 0); +} + +void ImGui::PopColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, columns->HostBackupClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); +} + +ImGuiOldColumns* ImGui::FindOrCreateColumns(ImGuiWindow* window, ImGuiID id) +{ + // We have few columns per window so for now we don't need bother much with turning this into a faster lookup. + for (int n = 0; n < window->ColumnsStorage.Size; n++) + if (window->ColumnsStorage[n].ID == id) + return &window->ColumnsStorage[n]; + + window->ColumnsStorage.push_back(ImGuiOldColumns()); + ImGuiOldColumns* columns = &window->ColumnsStorage.back(); + columns->ID = id; + return columns; +} + +ImGuiID ImGui::GetColumnsID(const char* str_id, int columns_count) +{ + ImGuiWindow* window = GetCurrentWindow(); + + // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. + // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. + PushID(0x11223347 + (str_id ? 0 : columns_count)); + ImGuiID id = window->GetID(str_id ? str_id : "columns"); + PopID(); + + return id; +} + +void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiOldColumnFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(columns_count >= 1); + IM_ASSERT(window->DC.CurrentColumns == NULL); // Nested columns are currently not supported + + // Acquire storage for the columns set + ImGuiID id = GetColumnsID(str_id, columns_count); + ImGuiOldColumns* columns = FindOrCreateColumns(window, id); + IM_ASSERT(columns->ID == id); + columns->Current = 0; + columns->Count = columns_count; + columns->Flags = flags; + window->DC.CurrentColumns = columns; + window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX(); + + columns->HostCursorPosY = window->DC.CursorPos.y; + columns->HostCursorMaxPosX = window->DC.CursorMaxPos.x; + columns->HostInitialClipRect = window->ClipRect; + columns->HostBackupParentWorkRect = window->ParentWorkRect; + window->ParentWorkRect = window->WorkRect; + + // Set state for first column + // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect + const float column_padding = g.Style.ItemSpacing.x; + const float half_clip_extend_x = ImTrunc(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); + const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f); + const float max_2 = window->WorkRect.Max.x + half_clip_extend_x; + columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f); + columns->OffMaxX = ImMax(ImMin(max_1, max_2) - window->Pos.x, columns->OffMinX + 1.0f); + columns->LineMinY = columns->LineMaxY = window->DC.CursorPos.y; + + // Clear data if columns count changed + if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) + columns->Columns.resize(0); + + // Initialize default widths + columns->IsFirstFrame = (columns->Columns.Size == 0); + if (columns->Columns.Size == 0) + { + columns->Columns.reserve(columns_count + 1); + for (int n = 0; n < columns_count + 1; n++) + { + ImGuiOldColumnData column; + column.OffsetNorm = n / (float)columns_count; + columns->Columns.push_back(column); + } + } + + for (int n = 0; n < columns_count; n++) + { + // Compute clipping rectangle + ImGuiOldColumnData* column = &columns->Columns[n]; + float clip_x1 = IM_ROUND(window->Pos.x + GetColumnOffset(n)); + float clip_x2 = IM_ROUND(window->Pos.x + GetColumnOffset(n + 1) - 1.0f); + column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); + column->ClipRect.ClipWithFull(window->ClipRect); + } + + if (columns->Count > 1) + { + columns->Splitter.Split(window->DrawList, 1 + columns->Count); + columns->Splitter.SetCurrentChannel(window->DrawList, 1); + PushColumnClipRect(0); + } + + // We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; + window->WorkRect.Max.y = window->ContentRegionRect.Max.y; +} + +void ImGui::NextColumn() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems || window->DC.CurrentColumns == NULL) + return; + + ImGuiContext& g = *GImGui; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + + if (columns->Count == 1) + { + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + IM_ASSERT(columns->Current == 0); + return; + } + + // Next column + if (++columns->Current == columns->Count) + columns->Current = 0; + + PopItemWidth(); + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + PushClipRect() + // (which would needlessly attempt to update commands in the wrong channel, then pop or overwrite them), + ImGuiOldColumnData* column = &columns->Columns[columns->Current]; + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); + + const float column_padding = g.Style.ItemSpacing.x; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + if (columns->Current > 0) + { + // Columns 1+ ignore IndentX (by canceling it out) + // FIXME-COLUMNS: Unnecessary, could be locked? + window->DC.ColumnsOffset.x = GetColumnOffset(columns->Current) - window->DC.Indent.x + column_padding; + } + else + { + // New row/line: column 0 honor IndentX. + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + window->DC.IsSameLine = false; + columns->LineMinY = columns->LineMaxY; + } + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.y = columns->LineMinY; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = 0.0f; + + // FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; +} + +void ImGui::EndColumns() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + PopItemWidth(); + if (columns->Count > 1) + { + PopClipRect(); + columns->Splitter.Merge(window->DrawList); + } + + const ImGuiOldColumnFlags flags = columns->Flags; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + window->DC.CursorPos.y = columns->LineMaxY; + if (!(flags & ImGuiOldColumnFlags_GrowParentContentsSize)) + window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX; // Restore cursor max pos, as columns don't grow parent + + // Draw columns borders and handle resize + // The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy + bool is_being_resized = false; + if (!(flags & ImGuiOldColumnFlags_NoBorder) && !window->SkipItems) + { + // We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers. + const float y1 = ImMax(columns->HostCursorPosY, window->ClipRect.Min.y); + const float y2 = ImMin(window->DC.CursorPos.y, window->ClipRect.Max.y); + int dragging_column = -1; + for (int n = 1; n < columns->Count; n++) + { + ImGuiOldColumnData* column = &columns->Columns[n]; + float x = window->Pos.x + GetColumnOffset(n); + const ImGuiID column_id = columns->ID + ImGuiID(n); + const float column_hit_hw = ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale); + const ImRect column_hit_rect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2)); + if (!ItemAdd(column_hit_rect, column_id, NULL, ImGuiItemFlags_NoNav)) + continue; + + bool hovered = false, held = false; + if (!(flags & ImGuiOldColumnFlags_NoResize)) + { + ButtonBehavior(column_hit_rect, column_id, &hovered, &held); + if (hovered || held) + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (held && !(column->Flags & ImGuiOldColumnFlags_NoResize)) + dragging_column = n; + } + + // Draw column + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + const float xi = IM_TRUNC(x); + window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); + } + + // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. + if (dragging_column != -1) + { + if (!columns->IsBeingResized) + for (int n = 0; n < columns->Count + 1; n++) + columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; + columns->IsBeingResized = is_being_resized = true; + float x = GetDraggedColumnOffset(columns, dragging_column); + SetColumnOffset(dragging_column, x); + } + } + columns->IsBeingResized = is_being_resized; + + window->WorkRect = window->ParentWorkRect; + window->ParentWorkRect = columns->HostBackupParentWorkRect; + window->DC.CurrentColumns = NULL; + window->DC.ColumnsOffset.x = 0.0f; + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + NavUpdateCurrentWindowIsScrollPushableX(); +} + +void ImGui::Columns(int columns_count, const char* id, bool borders) +{ + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(columns_count >= 1); + + ImGuiOldColumnFlags flags = (borders ? 0 : ImGuiOldColumnFlags_NoBorder); + //flags |= ImGuiOldColumnFlags_NoPreserveWidths; // NB: Legacy behavior + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns != NULL && columns->Count == columns_count && columns->Flags == flags) + return; + + if (columns != NULL) + EndColumns(); + + if (columns_count != 1) + BeginColumns(id, columns_count, flags); +} + +//------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imgui_widgets.cpp b/thirdparty/imgui/imgui_widgets.cpp new file mode 100644 index 00000000..c997e17b --- /dev/null +++ b/thirdparty/imgui/imgui_widgets.cpp @@ -0,0 +1,10451 @@ +// dear imgui, v1.91b +// (widgets code) + +/* + +Index of this file: + +// [SECTION] Forward Declarations +// [SECTION] Widgets: Text, etc. +// [SECTION] Widgets: Main (Button, Image, Checkbox, RadioButton, ProgressBar, Bullet, etc.) +// [SECTION] Widgets: Low-level Layout helpers (Spacing, Dummy, NewLine, Separator, etc.) +// [SECTION] Widgets: ComboBox +// [SECTION] Data Type and Data Formatting Helpers +// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. +// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. +// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. +// [SECTION] Widgets: InputText, InputTextMultiline +// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. +// [SECTION] Widgets: TreeNode, CollapsingHeader, etc. +// [SECTION] Widgets: Selectable +// [SECTION] Widgets: Typing-Select support +// [SECTION] Widgets: Box-Select support +// [SECTION] Widgets: Multi-Select support +// [SECTION] Widgets: Multi-Select helpers +// [SECTION] Widgets: ListBox +// [SECTION] Widgets: PlotLines, PlotHistogram +// [SECTION] Widgets: Value helpers +// [SECTION] Widgets: MenuItem, BeginMenu, EndMenu, etc. +// [SECTION] Widgets: BeginTabBar, EndTabBar, etc. +// [SECTION] Widgets: BeginTabItem, EndTabItem, etc. +// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc. + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" + +// System includes +#include // intptr_t + +//------------------------------------------------------------------------- +// Warnings +//------------------------------------------------------------------------- + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +//------------------------------------------------------------------------- +// Data +//------------------------------------------------------------------------- + +// Widgets +static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior. +static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags. + +// Those MIN/MAX values are not define because we need to point to them +static const signed char IM_S8_MIN = -128; +static const signed char IM_S8_MAX = 127; +static const unsigned char IM_U8_MIN = 0; +static const unsigned char IM_U8_MAX = 0xFF; +static const signed short IM_S16_MIN = -32768; +static const signed short IM_S16_MAX = 32767; +static const unsigned short IM_U16_MIN = 0; +static const unsigned short IM_U16_MAX = 0xFFFF; +static const ImS32 IM_S32_MIN = INT_MIN; // (-2147483647 - 1), (0x80000000); +static const ImS32 IM_S32_MAX = INT_MAX; // (2147483647), (0x7FFFFFFF) +static const ImU32 IM_U32_MIN = 0; +static const ImU32 IM_U32_MAX = UINT_MAX; // (0xFFFFFFFF) +#ifdef LLONG_MIN +static const ImS64 IM_S64_MIN = LLONG_MIN; // (-9223372036854775807ll - 1ll); +static const ImS64 IM_S64_MAX = LLONG_MAX; // (9223372036854775807ll); +#else +static const ImS64 IM_S64_MIN = -9223372036854775807LL - 1; +static const ImS64 IM_S64_MAX = 9223372036854775807LL; +#endif +static const ImU64 IM_U64_MIN = 0; +#ifdef ULLONG_MAX +static const ImU64 IM_U64_MAX = ULLONG_MAX; // (0xFFFFFFFFFFFFFFFFull); +#else +static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); +#endif + +//------------------------------------------------------------------------- +// [SECTION] Forward Declarations +//------------------------------------------------------------------------- + +// For InputTextEx() +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Text, etc. +//------------------------------------------------------------------------- +// - TextEx() [Internal] +// - TextUnformatted() +// - Text() +// - TextV() +// - TextColored() +// - TextColoredV() +// - TextDisabled() +// - TextDisabledV() +// - TextWrapped() +// - TextWrappedV() +// - LabelText() +// - LabelTextV() +// - BulletText() +// - BulletTextV() +//------------------------------------------------------------------------- + +void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + // Accept null ranges + if (text == text_end) + text = text_end = ""; + + // Calculate length + const char* text_begin = text; + if (text_end == NULL) + text_end = text + ImStrlen(text); // FIXME-OPT + + const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + const float wrap_pos_x = window->DC.TextWrapPos; + const bool wrap_enabled = (wrap_pos_x >= 0.0f); + if (text_end - text <= 2000 || wrap_enabled) + { + // Common case + const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; + const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size, 0.0f); + if (!ItemAdd(bb, 0)) + return; + + // Render (we don't hide text after ## in this end-user function) + RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); + } + else + { + // Long text! + // Perform manual coarse clipping to optimize for long multi-line text + // - From this point we will only compute the width of lines that are visible. Optimization only available when word-wrapping is disabled. + // - We also don't vertically center the text within the line full height, which is unlikely to matter because we are likely the biggest and only item on the line. + // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop. + const char* line = text; + const float line_height = GetTextLineHeight(); + ImVec2 text_size(0, 0); + + // Lines to skip (can't skip when logging text) + ImVec2 pos = text_pos; + if (!g.LogEnabled) + { + int lines_skippable = (int)((window->ClipRect.Min.y - text_pos.y) / line_height); + if (lines_skippable > 0) + { + int lines_skipped = 0; + while (line < text_end && lines_skipped < lines_skippable) + { + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + } + + // Lines to render + if (line < text_end) + { + ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); + while (line < text_end) + { + if (IsClippedEx(line_rect, 0)) + break; + + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + RenderText(pos, line, line_end, false); + line = line_end + 1; + line_rect.Min.y += line_height; + line_rect.Max.y += line_height; + pos.y += line_height; + } + + // Count remaining lines + int lines_skipped = 0; + while (line < text_end) + { + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); + if (!line_end) + line_end = text_end; + if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) + text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); + line = line_end + 1; + lines_skipped++; + } + pos.y += lines_skipped * line_height; + } + text_size.y = (pos - text_pos).y; + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size, 0.0f); + ItemAdd(bb, 0); + } +} + +void ImGui::TextUnformatted(const char* text, const char* text_end) +{ + TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); +} + +void ImGui::Text(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::TextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const char* text, *text_end; + ImFormatStringToTempBufferV(&text, &text_end, fmt, args); + TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); +} + +void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextColoredV(col, fmt, args); + va_end(args); +} + +void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, col); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextDisabled(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextDisabledV(fmt, args); + va_end(args); +} + +void ImGui::TextDisabledV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextWrapped(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextWrappedV(fmt, args); + va_end(args); +} + +void ImGui::TextWrappedV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set + if (need_backup) + PushTextWrapPos(0.0f); + TextV(fmt, args); + if (need_backup) + PopTextWrapPos(); +} + +void ImGui::LabelText(const char* label, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LabelTextV(label, fmt, args); + va_end(args); +} + +// Add a label+text combo aligned to other label+value widgets +void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float w = CalcItemWidth(); + + const char* value_text_begin, *value_text_end; + ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args); + const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const ImVec2 pos = window->DC.CursorPos; + const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2)); + const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0)) + return; + + // Render + RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); +} + +void ImGui::BulletText(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + BulletTextV(fmt, args); + va_end(args); +} + +// Text with a little bullet aligned to the typical tree node. +void ImGui::BulletTextV(const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const char* text_begin, *text_end; + ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args); + const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); + const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(total_size, 0.0f); + const ImRect bb(pos, pos + total_size); + if (!ItemAdd(bb, 0)) + return; + + // Render + ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col); + RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Main +//------------------------------------------------------------------------- +// - ButtonBehavior() [Internal] +// - Button() +// - SmallButton() +// - InvisibleButton() +// - ArrowButton() +// - CloseButton() [Internal] +// - CollapseButton() [Internal] +// - GetWindowScrollbarID() [Internal] +// - GetWindowScrollbarRect() [Internal] +// - Scrollbar() [Internal] +// - ScrollbarEx() [Internal] +// - Image() +// - ImageButton() +// - Checkbox() +// - CheckboxFlagsT() [Internal] +// - CheckboxFlags() +// - RadioButton() +// - ProgressBar() +// - Bullet() +// - Hyperlink() +//------------------------------------------------------------------------- + +// The ButtonBehavior() function is key to many interactions and used by many/most widgets. +// Because we handle so many cases (keyboard/gamepad navigation, drag and drop) and many specific behavior (via ImGuiButtonFlags_), +// this code is a little complex. +// By far the most common path is interacting with the Mouse using the default ImGuiButtonFlags_PressedOnClickRelease button behavior. +// See the series of events below and the corresponding state reported by dear imgui: +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnClickRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+0 (mouse is outside bb) - - - - - - +// Frame N+1 (mouse moves inside bb) - true - - - - +// Frame N+2 (mouse button is down) - true true true - true +// Frame N+3 (mouse button is down) - true true - - - +// Frame N+4 (mouse moves outside bb) - - true - - - +// Frame N+5 (mouse moves inside bb) - true true - - - +// Frame N+6 (mouse button is released) true true - - true - +// Frame N+7 (mouse button is released) - true - - - - +// Frame N+8 (mouse moves outside bb) - - - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+2 (mouse button is down) true true true true - true +// Frame N+3 (mouse button is down) - true true - - - +// Frame N+6 (mouse button is released) - true - - true - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnRelease: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+2 (mouse button is down) - true - - - true +// Frame N+3 (mouse button is down) - true - - - - +// Frame N+6 (mouse button is released) true true - - - - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// with PressedOnDoubleClick: return-value IsItemHovered() IsItemActive() IsItemActivated() IsItemDeactivated() IsItemClicked() +// Frame N+0 (mouse button is down) - true - - - true +// Frame N+1 (mouse button is down) - true - - - - +// Frame N+2 (mouse button is released) - true - - - - +// Frame N+3 (mouse button is released) - true - - - - +// Frame N+4 (mouse button is down) true true true true - true +// Frame N+5 (mouse button is down) - true true - - - +// Frame N+6 (mouse button is released) - true - - true - +// Frame N+7 (mouse button is released) - true - - - - +//------------------------------------------------------------------------------------------------------------------------------------------------ +// Note that some combinations are supported, +// - PressedOnDragDropHold can generally be associated with any flag. +// - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported. +//------------------------------------------------------------------------------------------------------------------------------------------------ +// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set: +// Repeat+ Repeat+ Repeat+ Repeat+ +// PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick +//------------------------------------------------------------------------------------------------------------------------------------------------- +// Frame N+0 (mouse button is down) - true - true +// ... - - - - +// Frame N + RepeatDelay true true - true +// ... - - - - +// Frame N + RepeatDelay + RepeatRate*N true true - true +//------------------------------------------------------------------------------------------------------------------------------------------------- + +// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc. +// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);' +// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading. +// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature. +// One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior() +// with same ID and different MouseButton (see #8030). You can fix it by: +// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags. +// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag() +bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + // Default behavior inherited from item flags + // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that. + ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags); + if (flags & ImGuiButtonFlags_AllowOverlap) + item_flags |= ImGuiItemFlags_AllowOverlap; + + // Default only reacts to left mouse button + if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0) + flags |= ImGuiButtonFlags_MouseButtonLeft; + + // Default behavior requires click + release inside bounding box + if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0) + flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_; + + ImGuiWindow* backup_hovered_window = g.HoveredWindow; + const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindow == window; + if (flatten_hovered_children) + g.HoveredWindow = window; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + // Alternate registration spot, for when caller didn't use ItemAdd() + if (g.LastItemData.ID != id) + IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL); +#endif + + bool pressed = false; + bool hovered = ItemHoverable(bb, id, item_flags); + + // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button + if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) + if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + { + hovered = true; + SetHoveredID(id); + if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER) + { + pressed = true; + g.DragDropHoldJustPressedId = id; + FocusWindow(window); + } + } + + if (flatten_hovered_children) + g.HoveredWindow = backup_hovered_window; + + // Mouse handling + const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id; + if (hovered) + { + IM_ASSERT(id != 0); // Lazily check inside rare path. + + // Poll mouse buttons + // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId. + // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine. + int mouse_button_clicked = -1; + int mouse_button_released = -1; + for (int button = 0; button < 3; button++) + if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here. + { + if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; } + if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; } + } + + // Process initial action + const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt); + if (mods_ok) + { + if (mouse_button_clicked != -1 && g.ActiveId != id) + { + if (!(flags & ImGuiButtonFlags_NoSetKeyOwner)) + SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id); + if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere)) + { + SetActiveID(id, window); + g.ActiveIdMouseButton = mouse_button_clicked; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + { + SetFocusID(id, window); + FocusWindow(window); + } + else + { + FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child + } + } + if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2)) + { + pressed = true; + if (flags & ImGuiButtonFlags_NoHoldingActiveId) + ClearActiveID(); + else + SetActiveID(id, window); // Hold on ID + g.ActiveIdMouseButton = mouse_button_clicked; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + { + SetFocusID(id, window); + FocusWindow(window); + } + else + { + FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child + } + } + } + if (flags & ImGuiButtonFlags_PressedOnRelease) + { + if (mouse_button_released != -1) + { + const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior + if (!has_repeated_at_least_once) + pressed = true; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why. + ClearActiveID(); + } + } + + // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). + // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. + if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat)) + if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id)) + pressed = true; + } + + if (pressed && g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + } + + // Keyboard/Gamepad navigation handling + // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav) + if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus)) + hovered = true; + if (g.NavActivateDownId == id) + { + bool nav_activated_by_code = (g.NavActivateId == id); + bool nav_activated_by_inputs = (g.NavActivatePressedId == id); + if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat)) + { + // Avoid pressing multiple keys from triggering excessive amount of repeat events + const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space); + const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter); + const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate); + const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration); + nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; + } + if (nav_activated_by_code || nav_activated_by_inputs) + { + // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. + pressed = true; + SetActiveID(id, window); + g.ActiveIdSource = g.NavInputSource; + if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)) + SetFocusID(id, window); + if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut) + g.ActiveIdFromShortcut = true; + } + } + + // Process while held + bool held = false; + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (g.ActiveIdIsJustActivated) + g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; + + const int mouse_button = g.ActiveIdMouseButton; + if (mouse_button == -1) + { + // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304). + ClearActiveID(); + } + else if (IsMouseDown(mouse_button, test_owner_id)) + { + held = true; + } + else + { + bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0; + bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0; + if ((release_in || release_anywhere) && !g.DragDropActive) + { + // Report as pressed when releasing the mouse (this is the most common path) + bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2; + bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps + bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id); + if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned) + pressed = true; + } + ClearActiveID(); + } + if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + } + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + { + // When activated using Nav, we hold on the ActiveID until activation button is released + if (g.NavActivateDownId == id) + held = true; // hovered == true not true as we are already likely hovered on direct activation. + else + ClearActiveID(); + } + if (pressed) + g.ActiveIdHasBeenPressedBefore = true; + } + + // Activation highlight (this may be a remote activation) + if (g.NavHighlightActivatedId == id) + hovered = true; + + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + + return pressed; +} + +bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + ImVec2 pos = window->DC.CursorPos; + if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) + pos.y += window->DC.CurrLineTextBaseOffset - style.FramePadding.y; + ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + + if (g.LogEnabled) + LogSetNextTextDecoration("[", "]"); + RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); + + // Automatically close popups + //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) + // CloseCurrentPopup(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::Button(const char* label, const ImVec2& size_arg) +{ + return ButtonEx(label, size_arg, ImGuiButtonFlags_None); +} + +// Small buttons fits within text without additional vertical spacing. +bool ImGui::SmallButton(const char* label) +{ + ImGuiContext& g = *GImGui; + float backup_padding_y = g.Style.FramePadding.y; + g.Style.FramePadding.y = 0.0f; + bool pressed = ButtonEx(label, ImVec2(0, 0), ImGuiButtonFlags_AlignTextBaseLine); + g.Style.FramePadding.y = backup_padding_y; + return pressed; +} + +// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. +// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) +bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + // Cannot use zero-size for InvisibleButton(). Unlike Button() there is not way to fallback using the label size. + IM_ASSERT(size_arg.x != 0.0f && size_arg.y != 0.0f); + + const ImGuiID id = window->GetID(str_id); + ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(size); + if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + RenderNavCursor(bb, id); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiID id = window->GetID(str_id); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + const float default_size = GetFrameHeight(); + ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + const ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); + RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); + return pressed; +} + +bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir) +{ + float sz = GetFrameHeight(); + return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None); +} + +// Button to close a window +bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825) + // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible? + const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); + ImRect bb_interact = bb; + const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea(); + if (area_to_visible_ratio < 1.5f) + bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f)); + + // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window. + // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer). + bool is_clipped = !ItemAdd(bb_interact, id); + + bool hovered, held; + bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held); + if (is_clipped) + return pressed; + + // Render + ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); + if (hovered) + window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); + ImU32 cross_col = GetColorU32(ImGuiCol_Text); + ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f); + float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; + window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f); + window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f); + + return pressed; +} + +bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); + bool is_clipped = !ItemAdd(bb, id); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); + if (is_clipped) + return pressed; + + // Render + ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImU32 text_col = GetColorU32(ImGuiCol_Text); + if (hovered || held) + window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); + RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + + // Switch to moving the window after mouse is moved beyond the initial drag threshold + if (IsItemActive() && IsMouseDragging(0)) + StartMouseMovingWindow(window); + + return pressed; +} + +ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) +{ + return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY"); +} + +// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set. +ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + const ImRect outer_rect = window->Rect(); + const ImRect inner_rect = window->InnerRect; + const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) + IM_ASSERT(scrollbar_size >= 0.0f); + const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f); + const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f; + if (axis == ImGuiAxis_X) + return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size); + else + return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y + border_top, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size); +} + +void ImGui::Scrollbar(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = GetWindowScrollbarID(window, axis); + + // Calculate scrollbar bounding box + ImRect bb = GetWindowScrollbarRect(window, axis); + ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone; + if (axis == ImGuiAxis_X) + { + rounding_corners |= ImDrawFlags_RoundCornersBottomLeft; + if (!window->ScrollbarY) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + else + { + if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) + rounding_corners |= ImDrawFlags_RoundCornersTopRight; + if (!window->ScrollbarX) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis]; + float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f; + ImS64 scroll = (ImS64)window->Scroll[axis]; + ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners); + window->Scroll[axis] = (float)scroll; +} + +// Vertical/Horizontal scrollbar +// The entire piece of code below is rather confusing because: +// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) +// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar +// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. +// Still, the code should probably be made simpler.. +bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags draw_rounding_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const float bb_frame_width = bb_frame.GetWidth(); + const float bb_frame_height = bb_frame.GetHeight(); + if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f) + return false; + + // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab) + float alpha = 1.0f; + if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width) + alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f)); + if (alpha <= 0.0f) + return false; + + const ImGuiStyle& style = g.Style; + const bool allow_interaction = (alpha >= 1.0f); + + ImRect bb = bb_frame; + bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); + + // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) + const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight(); + + // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) + // But we maintain a minimum size in pixel to allow for the user to still aim inside. + IM_ASSERT(ImMax(size_contents_v, size_visible_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. + const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1); + const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize); + const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v); + const float grab_h_norm = grab_h_pixels / scrollbar_size_v; + + // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). + bool held = false; + bool hovered = false; + ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); + + const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v); + float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); + float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space + if (held && allow_interaction && grab_h_norm < 1.0f) + { + const float scrollbar_pos_v = bb.Min[axis]; + const float mouse_pos_v = g.IO.MousePos[axis]; + + // Click position in scrollbar normalized space (0.0f->1.0f) + const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); + + const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0; + if (g.ActiveIdIsJustActivated) + { + // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab + const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0); + g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir; + g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f; + } + + // Apply scroll (p_scroll_v will generally point on one member of window->Scroll) + // It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position + if (g.ScrollbarSeekMode == 0) + { + // Absolute seeking + const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); + *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max); + } + else + { + // Page by page + if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode) + { + float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f; + *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max); + } + } + + // Update values for rendering + scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); + grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + + // Update distance to grab now that we have seek'ed and saturated + //if (seek_absolute) + // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + } + + // Render + const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg); + const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha); + window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags); + ImRect grab_rect; + if (axis == ImGuiAxis_X) + grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y); + else + grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels); + window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); + + return held; +} + +// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples +// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. +void ImGui::ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + return; + + // Render + if (g.Style.ImageBorderSize > 0.0f) + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, ImDrawFlags_None, g.Style.ImageBorderSize); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); +} + +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1) +{ + ImageWithBg(user_texture_id, image_size, uv0, uv1); +} + +// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +{ + ImGuiContext& g = *GImGui; + PushStyleVar(ImGuiStyleVar_ImageBorderSize, (border_col.w > 0.0f) ? ImMax(1.0f, g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f + PushStyleColor(ImGuiCol_Border, border_col); + ImageWithBg(user_texture_id, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col); + PopStyleColor(); + PopStyleVar(); +} +#endif + +bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImVec2 padding = g.Style.FramePadding; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding)); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); + + return pressed; +} + +// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button. +// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design? +bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col); +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// Legacy API obsoleted in 1.89. Two differences with new ImageButton() +// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID) +// - new ImageButton() requires an explicit 'const char* str_id' +// - old ImageButton() had frame_padding' override argument. +// - new ImageButton() always use style.FramePadding. +/* +bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + // Default to using texture ID as ID. User can still push string/integer prefixes. + PushID((ImTextureID)(intptr_t)user_texture_id); + if (frame_padding >= 0) + PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding)); + bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col); + if (frame_padding >= 0) + PopStyleVar(); + PopID(); + return ret; +} +*/ +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +bool ImGui::Checkbox(const char* label, bool* v) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const float square_sz = GetFrameHeight(); + const ImVec2 pos = window->DC.CursorPos; + const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); + ItemSize(total_bb, style.FramePadding.y); + const bool is_visible = ItemAdd(total_bb, id); + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (!is_visible) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support + { + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return false; + } + + // Range-Selection/Multi-selection support (header) + bool checked = *v; + if (is_multi_select) + MultiSelectItemHeader(id, &checked, NULL); + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + + // Range-Selection/Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &checked, &pressed); + else if (pressed) + checked = !checked; + + if (*v != checked) + { + *v = checked; + pressed = true; // return value + MarkItemEdited(id); + } + + const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); + const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; + if (is_visible) + { + RenderNavCursor(total_bb, id); + RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); + ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); + if (mixed_value) + { + // Undocumented tristate/mixed/indeterminate checkbox (#2644) + // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox) + ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); + window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); + } + else if (*v) + { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); + } + } + const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); + if (g.LogEnabled) + LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); + if (is_visible && label_size.x > 0.0f) + RenderText(label_pos, label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return pressed; +} + +template +bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value) +{ + bool all_on = (*flags & flags_value) == flags_value; + bool any_on = (*flags & flags_value) != 0; + bool pressed; + if (!all_on && any_on) + { + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue; + pressed = Checkbox(label, &all_on); + } + else + { + pressed = Checkbox(label, &all_on); + + } + if (pressed) + { + if (all_on) + *flags |= flags_value; + else + *flags &= ~flags_value; + } + return pressed; +} + +bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::RadioButton(const char* label, bool active) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + const float square_sz = GetFrameHeight(); + const ImVec2 pos = window->DC.CursorPos; + const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); + const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id)) + return false; + + ImVec2 center = check_bb.GetCenter(); + center.x = IM_ROUND(center.x); + center.y = IM_ROUND(center.y); + const float radius = (square_sz - 1.0f) * 0.5f; + + bool hovered, held; + bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); + if (pressed) + MarkItemEdited(id); + + RenderNavCursor(total_bb, id); + const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); + window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); + if (active) + { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark)); + } + + if (style.FrameBorderSize > 0.0f) + { + window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize); + window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize); + } + + ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); + if (g.LogEnabled) + LogRenderedText(&label_pos, active ? "(x)" : "( )"); + if (label_size.x > 0.0f) + RenderText(label_pos, label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +// FIXME: This would work nicely if it was a public template, e.g. 'template RadioButton(const char* label, T* v, T v_button)', but I'm not sure how we would expose it.. +bool ImGui::RadioButton(const char* label, int* v, int v_button) +{ + const bool pressed = RadioButton(label, *v == v_button); + if (pressed) + *v = v_button; + return pressed; +} + +// size_arg (for each axis) < 0.0f: align to end, 0.0f: auto, > 0.0f: specified size +void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* overlay) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + ImVec2 pos = window->DC.CursorPos; + ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); + ImRect bb(pos, pos + size); + ItemSize(size, style.FramePadding.y); + if (!ItemAdd(bb, 0)) + return; + + // Fraction < 0.0f will display an indeterminate progress bar animation + // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works. + const bool is_indeterminate = (fraction < 0.0f); + if (!is_indeterminate) + fraction = ImSaturate(fraction); + + // Out of courtesy we accept a NaN fraction without crashing + float fill_n0 = 0.0f; + float fill_n1 = (fraction == fraction) ? fraction : 0.0f; + + if (is_indeterminate) + { + const float fill_width_n = 0.2f; + fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n; + fill_n1 = ImSaturate(fill_n0 + fill_width_n); + fill_n0 = ImSaturate(fill_n0); + } + + // Render + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); + RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding); + + // Default displaying the fraction as percentage string, but user can override it + // Don't display text for indeterminate bars by default + char overlay_buf[32]; + if (!is_indeterminate || overlay != NULL) + { + if (!overlay) + { + ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); + overlay = overlay_buf; + } + + ImVec2 overlay_size = CalcTextSize(overlay, NULL); + if (overlay_size.x > 0.0f) + { + float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x; + RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); + } + } +} + +void ImGui::Bullet() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + { + SameLine(0, style.FramePadding.x * 2); + return; + } + + // Render and stay on same line + ImU32 text_col = GetColorU32(ImGuiCol_Text); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col); + SameLine(0, style.FramePadding.x * 2.0f); +} + +// This is provided as a convenience for being an often requested feature. +// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system. +// Because of this we currently don't provide many styling options for this widget +// (e.g. hovered/active colors are automatically inferred from a single color). +bool ImGui::TextLink(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const char* label_end = FindRenderedTextEnd(label); + + ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + ImVec2 size = CalcTextSize(label, label_end, true); + ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + RenderNavCursor(bb, id); + + if (hovered) + SetMouseCursor(ImGuiMouseCursor_Hand); + + ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink]; + ImVec4 line_colf = text_colf; + { + // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets, + // as we are currently experimenting/planning a different styling system. + float h, s, v; + ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v); + if (held || hovered) + { + v = ImSaturate(v + (held ? 0.4f : 0.3f)); + h = ImFmod(h + 0.02f, 1.0f); + } + ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z); + v = ImSaturate(v - 0.20f); + ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z); + } + + float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f); + window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode. + + PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf)); + RenderText(bb.Min, label, label_end); + PopStyleColor(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +void ImGui::TextLinkOpenURL(const char* label, const char* url) +{ + ImGuiContext& g = *GImGui; + if (url == NULL) + url = label; + if (TextLink(label)) + if (g.PlatformIO.Platform_OpenInShellFn != NULL) + g.PlatformIO.Platform_OpenInShellFn(&g, url); + SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label + if (BeginPopupContextItem()) + { + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink))) + SetClipboardText(url); + EndPopup(); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Low-level Layout helpers +//------------------------------------------------------------------------- +// - Spacing() +// - Dummy() +// - NewLine() +// - AlignTextToFramePadding() +// - SeparatorEx() [Internal] +// - Separator() +// - SplitterBehavior() [Internal] +// - ShrinkWidths() [Internal] +//------------------------------------------------------------------------- + +void ImGui::Spacing() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ItemSize(ImVec2(0, 0)); +} + +void ImGui::Dummy(const ImVec2& size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(size); + ItemAdd(bb, 0); +} + +void ImGui::NewLine() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.IsSameLine = false; + if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. + ItemSize(ImVec2(0, 0)); + else + ItemSize(ImVec2(0.0f, g.FontSize)); + window->DC.LayoutType = backup_layout_type; +} + +void ImGui::AlignTextToFramePadding() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + window->DC.CurrLineSize.y = ImMax(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y * 2); + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, g.Style.FramePadding.y); +} + +// Horizontal/vertical separating line +// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues. +// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are. +void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected + IM_ASSERT(thickness > 0.0f); + + if (flags & ImGuiSeparatorFlags_Vertical) + { + // Vertical separator, for menu bars (use current line height). + float y1 = window->DC.CursorPos.y; + float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y; + const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2)); + ItemSize(ImVec2(thickness, 0.0f)); + if (!ItemAdd(bb, 0)) + return; + + // Draw + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogText(" |"); + } + else if (flags & ImGuiSeparatorFlags_Horizontal) + { + // Horizontal Separator + float x1 = window->DC.CursorPos.x; + float x2 = window->WorkRect.Max.x; + + // Preserve legacy behavior inside Columns() + // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set. + // We currently don't need to provide the same feature for tables because tables naturally have border features. + ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; + if (columns) + { + x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03 + x2 = window->Pos.x + window->Size.x; + PushColumnsBackground(); + } + + // We don't provide our width to the layout so that it doesn't get feed back into AutoFit + // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell) + const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change. + const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness)); + ItemSize(ImVec2(0.0f, thickness_for_layout)); + + if (ItemAdd(bb, 0)) + { + // Draw + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogRenderedText(&bb.Min, "--------------------------------\n"); + + } + if (columns) + { + PopColumnsBackground(); + columns->LineMinY = window->DC.CursorPos.y; + } + } +} + +void ImGui::Separator() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // Those flags should eventually be configurable by the user + // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f. + ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; + + // Only applies to legacy Columns() api as they relied on Separator() a lot. + if (window->DC.CurrentColumns) + flags |= ImGuiSeparatorFlags_SpanAllColumns; + + SeparatorEx(flags, 1.0f); +} + +void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = g.Style; + + const ImVec2 label_size = CalcTextSize(label, label_end, false); + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 padding = style.SeparatorTextPadding; + + const float separator_thickness = style.SeparatorTextBorderSize; + const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness)); + const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y)); + const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImFloor((style.SeparatorTextSize - label_size.y) * 0.5f)); + ItemSize(min_size, text_baseline_y); + if (!ItemAdd(bb, id)) + return; + + const float sep1_x1 = pos.x; + const float sep2_x2 = bb.Max.x; + const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); + + const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f); + const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN + + // This allows using SameLine() to position something in the 'extra_w' + window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x; + + const ImU32 separator_col = GetColorU32(ImGuiCol_Separator); + if (label_size.x > 0.0f) + { + const float sep1_x2 = label_pos.x - style.ItemSpacing.x; + const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x; + if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness); + if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); + if (g.LogEnabled) + LogSetNextTextDecoration("---", NULL); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size); + } + else + { + if (g.LogEnabled) + LogText("---"); + if (separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); + } +} + +void ImGui::SeparatorText(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want: + // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight) + // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string) + // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...' + // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item, + // and then we can turn this into a format function. + SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f); +} + +// Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise. +bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay, ImU32 bg_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav)) + return false; + + // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is + // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item. + // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item. + ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren; +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + button_flags |= ImGuiButtonFlags_AllowOverlap; +#endif + + bool hovered, held; + ImRect bb_interact = bb; + bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); + ButtonBehavior(bb_interact, id, &hovered, &held, button_flags); + if (hovered) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb + + if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) + SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + + ImRect bb_render = bb; + if (held) + { + float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis]; + + // Minimum pane size + float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1); + float size_2_maximum_delta = ImMax(0.0f, *size2 - min_size2); + if (mouse_delta < -size_1_maximum_delta) + mouse_delta = -size_1_maximum_delta; + if (mouse_delta > size_2_maximum_delta) + mouse_delta = size_2_maximum_delta; + + // Apply resize + if (mouse_delta != 0.0f) + { + *size1 = ImMax(*size1 + mouse_delta, min_size1); + *size2 = ImMax(*size2 - mouse_delta, min_size2); + bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); + MarkItemEdited(id); + } + } + + // Render at new position + if (bg_col & IM_COL32_A_MASK) + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f); + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f); + + return held; +} + +static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs) +{ + const ImGuiShrinkWidthItem* a = (const ImGuiShrinkWidthItem*)lhs; + const ImGuiShrinkWidthItem* b = (const ImGuiShrinkWidthItem*)rhs; + if (int d = (int)(b->Width - a->Width)) + return d; + return (b->Index - a->Index); +} + +// Shrink excess width from a set of item, by removing width from the larger items first. +// Set items Width to -1.0f to disable shrinking this item. +void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess) +{ + if (count == 1) + { + if (items[0].Width >= 0.0f) + items[0].Width = ImMax(items[0].Width - width_excess, 1.0f); + return; + } + ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); + int count_same_width = 1; + while (width_excess > 0.0f && count_same_width < count) + { + while (count_same_width < count && items[0].Width <= items[count_same_width].Width) + count_same_width++; + float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f); + if (max_width_to_remove_per_item <= 0.0f) + break; + float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item); + for (int item_n = 0; item_n < count_same_width; item_n++) + items[item_n].Width -= width_to_remove_per_item; + width_excess -= width_to_remove_per_item * count_same_width; + } + + // Round width and redistribute remainder + // Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator. + width_excess = 0.0f; + for (int n = 0; n < count; n++) + { + float width_rounded = ImTrunc(items[n].Width); + width_excess += items[n].Width - width_rounded; + items[n].Width = width_rounded; + } + while (width_excess > 0.0f) + for (int n = 0; n < count && width_excess > 0.0f; n++) + { + float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f); + items[n].Width += width_to_add; + width_excess -= width_to_add; + } +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ComboBox +//------------------------------------------------------------------------- +// - CalcMaxPopupHeightFromItemCount() [Internal] +// - BeginCombo() +// - BeginComboPopup() [Internal] +// - EndCombo() +// - BeginComboPreview() [Internal] +// - EndComboPreview() [Internal] +// - Combo() +//------------------------------------------------------------------------- + +static float CalcMaxPopupHeightFromItemCount(int items_count) +{ + ImGuiContext& g = *GImGui; + if (items_count <= 0) + return FLT_MAX; + return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2); +} + +bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags; + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together + if (flags & ImGuiComboFlags_WidthFitPreview) + IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); + + const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; + const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &bb)) + return false; + + // Open on click + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id); + bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None); + if (pressed && !popup_open) + { + OpenPopupEx(popup_id, ImGuiPopupFlags_None); + popup_open = true; + } + + // Render shape + const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size); + RenderNavCursor(bb, id); + if (!(flags & ImGuiComboFlags_NoPreview)) + window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft); + if (!(flags & ImGuiComboFlags_NoArrowButton)) + { + ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImU32 text_col = GetColorU32(ImGuiCol_Text); + window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight); + if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x) + RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); + } + RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding); + + // Custom preview + if (flags & ImGuiComboFlags_CustomPreview) + { + g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y); + IM_ASSERT(preview_value == NULL || preview_value[0] == 0); + preview_value = NULL; + } + + // Render preview and label + if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) + { + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); + } + if (label_size.x > 0) + RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); + + if (!popup_open) + return false; + + g.NextWindowData.HasFlags = backup_next_window_data_flags; + return BeginComboPopup(popup_id, bb, flags); +} + +bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); + return false; + } + + // Set popup size + float w = bb.GetWidth(); + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint) + { + g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); + } + else + { + if ((flags & ImGuiComboFlags_HeightMask_) == 0) + flags |= ImGuiComboFlags_HeightRegular; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one + int popup_max_height_in_items = -1; + if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; + else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; + else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; + ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX); + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size + constraint_min.x = w; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f) + constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); + SetNextWindowSizeConstraints(constraint_min, constraint_max); + } + + // This is essentially a specialized version of BeginPopupEx() + char name[16]; + ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth + + // Set position given a custom constraint (peak into expected window size so we can position it) + // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function? + // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()? + if (ImGuiWindow* popup_window = FindWindowByName(name)) + if (popup_window->WasActive) + { + // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us. + ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window); + popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)" + ImRect r_outer = GetPopupAllowedExtentRect(popup_window); + ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox); + SetNextWindowPos(pos); + } + + // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx() + ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove; + PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text + bool ret = Begin(name, NULL, window_flags); + PopStyleVar(); + if (!ret) + { + EndPopup(); + IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above + return false; + } + g.BeginComboDepth++; + return true; +} + +void ImGui::EndCombo() +{ + ImGuiContext& g = *GImGui; + EndPopup(); + g.BeginComboDepth--; +} + +// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements +// (Experimental, see GitHub issues: #1658, #4168) +bool ImGui::BeginComboPreview() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)) + return false; + IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag? + if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional) + return false; + + // FIXME: This could be contained in a PushWorkRect() api + preview_data->BackupCursorPos = window->DC.CursorPos; + preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos; + preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + preview_data->BackupLayout = window->DC.LayoutType; + window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.IsSameLine = false; + PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true); + + return true; +} + +void ImGui::EndComboPreview() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future + ImDrawList* draw_list = window->DrawList; + if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y) + if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command + { + draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect; + draw_list->_TryMergeDrawCmds(); + } + PopClipRect(); + window->DC.CursorPos = preview_data->BackupCursorPos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos); + window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine; + window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset; + window->DC.LayoutType = preview_data->BackupLayout; + window->DC.IsSameLine = false; + preview_data->PreviewRect = ImRect(); +} + +// Getter for the old Combo() API: const char*[] +static const char* Items_ArrayGetter(void* data, int idx) +{ + const char* const* items = (const char* const*)data; + return items[idx]; +} + +// Getter for the old Combo() API: "item1\0item2\0item3\0" +static const char* Items_SingleStringGetter(void* data, int idx) +{ + const char* items_separated_by_zeros = (const char*)data; + int items_count = 0; + const char* p = items_separated_by_zeros; + while (*p) + { + if (idx == items_count) + break; + p += ImStrlen(p) + 1; + items_count++; + } + return *p ? p : NULL; +} + +// Old API, prefer using BeginCombo() nowadays if you can. +bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items) +{ + ImGuiContext& g = *GImGui; + + // Call the getter to obtain the preview string which is a parameter to BeginCombo() + const char* preview_value = NULL; + if (*current_item >= 0 && *current_item < items_count) + preview_value = getter(user_data, *current_item); + + // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. + if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)) + SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + + if (!BeginCombo(label, preview_value, ImGuiComboFlags_None)) + return false; + + // Display items + bool value_changed = false; + ImGuiListClipper clipper; + clipper.Begin(items_count); + clipper.IncludeItemByIndex(*current_item); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + const char* item_text = getter(user_data, i); + if (item_text == NULL) + item_text = "*Unknown item*"; + + PushID(i); + const bool item_selected = (i == *current_item); + if (Selectable(item_text, item_selected) && *current_item != i) + { + value_changed = true; + *current_item = i; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + + EndCombo(); + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +// Combo box helper allowing to pass an array of strings. +bool ImGui::Combo(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items) +{ + const bool value_changed = Combo(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_in_items); + return value_changed; +} + +// Combo box helper allowing to pass all items in a single string literal holding multiple zero-terminated items "item1\0item2\0" +bool ImGui::Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int height_in_items) +{ + int items_count = 0; + const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open + while (*p) + { + p += ImStrlen(p) + 1; + items_count++; + } + bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); + return value_changed; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); }; +static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx) +{ + ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data; + const char* s = NULL; + data->OldCallback(data->UserData, idx, &s); + return s; +} + +bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items); +} +bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items); +} + +#endif + +//------------------------------------------------------------------------- +// [SECTION] Data Type and Data Formatting Helpers [Internal] +//------------------------------------------------------------------------- +// - DataTypeGetInfo() +// - DataTypeFormatString() +// - DataTypeApplyOp() +// - DataTypeApplyFromText() +// - DataTypeCompare() +// - DataTypeClamp() +// - GetMinimumStepAtDecimalPrecision +// - RoundScalarWithFormat<>() +//------------------------------------------------------------------------- + +static const ImGuiDataTypeInfo GDataTypeInfo[] = +{ + { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8 + { sizeof(unsigned char), "U8", "%u", "%u" }, + { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16 + { sizeof(unsigned short), "U16", "%u", "%u" }, + { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32 + { sizeof(unsigned int), "U32", "%u", "%u" }, +#ifdef _MSC_VER + { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%I64u","%I64u" }, +#else + { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%llu", "%llu" }, +#endif + { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg) + { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double + { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool + { 0, "char*","%s", "%s" }, // ImGuiDataType_String +}; +IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT); + +const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type) +{ + IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT); + return &GDataTypeInfo[data_type]; +} + +int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format) +{ + // Signedness doesn't matter when pushing integer arguments + if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32) + return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data); + if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64) + return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data); + if (data_type == ImGuiDataType_Float) + return ImFormatString(buf, buf_size, format, *(const float*)p_data); + if (data_type == ImGuiDataType_Double) + return ImFormatString(buf, buf_size, format, *(const double*)p_data); + if (data_type == ImGuiDataType_S8) + return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data); + if (data_type == ImGuiDataType_U8) + return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data); + if (data_type == ImGuiDataType_S16) + return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data); + if (data_type == ImGuiDataType_U16) + return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data); + IM_ASSERT(0); + return 0; +} + +void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2) +{ + IM_ASSERT(op == '+' || op == '-'); + switch (data_type) + { + case ImGuiDataType_S8: + if (op == '+') { *(ImS8*)output = ImAddClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } + if (op == '-') { *(ImS8*)output = ImSubClampOverflow(*(const ImS8*)arg1, *(const ImS8*)arg2, IM_S8_MIN, IM_S8_MAX); } + return; + case ImGuiDataType_U8: + if (op == '+') { *(ImU8*)output = ImAddClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } + if (op == '-') { *(ImU8*)output = ImSubClampOverflow(*(const ImU8*)arg1, *(const ImU8*)arg2, IM_U8_MIN, IM_U8_MAX); } + return; + case ImGuiDataType_S16: + if (op == '+') { *(ImS16*)output = ImAddClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } + if (op == '-') { *(ImS16*)output = ImSubClampOverflow(*(const ImS16*)arg1, *(const ImS16*)arg2, IM_S16_MIN, IM_S16_MAX); } + return; + case ImGuiDataType_U16: + if (op == '+') { *(ImU16*)output = ImAddClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } + if (op == '-') { *(ImU16*)output = ImSubClampOverflow(*(const ImU16*)arg1, *(const ImU16*)arg2, IM_U16_MIN, IM_U16_MAX); } + return; + case ImGuiDataType_S32: + if (op == '+') { *(ImS32*)output = ImAddClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } + if (op == '-') { *(ImS32*)output = ImSubClampOverflow(*(const ImS32*)arg1, *(const ImS32*)arg2, IM_S32_MIN, IM_S32_MAX); } + return; + case ImGuiDataType_U32: + if (op == '+') { *(ImU32*)output = ImAddClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } + if (op == '-') { *(ImU32*)output = ImSubClampOverflow(*(const ImU32*)arg1, *(const ImU32*)arg2, IM_U32_MIN, IM_U32_MAX); } + return; + case ImGuiDataType_S64: + if (op == '+') { *(ImS64*)output = ImAddClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } + if (op == '-') { *(ImS64*)output = ImSubClampOverflow(*(const ImS64*)arg1, *(const ImS64*)arg2, IM_S64_MIN, IM_S64_MAX); } + return; + case ImGuiDataType_U64: + if (op == '+') { *(ImU64*)output = ImAddClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } + if (op == '-') { *(ImU64*)output = ImSubClampOverflow(*(const ImU64*)arg1, *(const ImU64*)arg2, IM_U64_MIN, IM_U64_MAX); } + return; + case ImGuiDataType_Float: + if (op == '+') { *(float*)output = *(const float*)arg1 + *(const float*)arg2; } + if (op == '-') { *(float*)output = *(const float*)arg1 - *(const float*)arg2; } + return; + case ImGuiDataType_Double: + if (op == '+') { *(double*)output = *(const double*)arg1 + *(const double*)arg2; } + if (op == '-') { *(double*)output = *(const double*)arg1 - *(const double*)arg2; } + return; + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); +} + +// User can input math operators (e.g. +100) to edit a numerical values. +// NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess.. +bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty) +{ + // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all. + const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); + ImGuiDataTypeStorage data_backup; + memcpy(&data_backup, p_data, type_info->Size); + + while (ImCharIsBlankA(*buf)) + buf++; + if (!buf[0]) + { + if (p_data_when_empty != NULL) + { + memcpy(p_data, p_data_when_empty, type_info->Size); + return memcmp(&data_backup, p_data, type_info->Size) != 0; + } + return false; + } + + // Sanitize format + // - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf + // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %. + char format_sanitized[32]; + if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) + format = type_info->ScanFmt; + else + format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized)); + + // Small types need a 32-bit buffer to receive the result from scanf() + int v32 = 0; + if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1) + return false; + if (type_info->Size < 4) + { + if (data_type == ImGuiDataType_S8) + *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX); + else if (data_type == ImGuiDataType_U8) + *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX); + else if (data_type == ImGuiDataType_S16) + *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX); + else if (data_type == ImGuiDataType_U16) + *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX); + else + IM_ASSERT(0); + } + + return memcmp(&data_backup, p_data, type_info->Size) != 0; +} + +template +static int DataTypeCompareT(const T* lhs, const T* rhs) +{ + if (*lhs < *rhs) return -1; + if (*lhs > *rhs) return +1; + return 0; +} + +int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeCompareT((const ImS8* )arg_1, (const ImS8* )arg_2); + case ImGuiDataType_U8: return DataTypeCompareT((const ImU8* )arg_1, (const ImU8* )arg_2); + case ImGuiDataType_S16: return DataTypeCompareT((const ImS16* )arg_1, (const ImS16* )arg_2); + case ImGuiDataType_U16: return DataTypeCompareT((const ImU16* )arg_1, (const ImU16* )arg_2); + case ImGuiDataType_S32: return DataTypeCompareT((const ImS32* )arg_1, (const ImS32* )arg_2); + case ImGuiDataType_U32: return DataTypeCompareT((const ImU32* )arg_1, (const ImU32* )arg_2); + case ImGuiDataType_S64: return DataTypeCompareT((const ImS64* )arg_1, (const ImS64* )arg_2); + case ImGuiDataType_U64: return DataTypeCompareT((const ImU64* )arg_1, (const ImU64* )arg_2); + case ImGuiDataType_Float: return DataTypeCompareT((const float* )arg_1, (const float* )arg_2); + case ImGuiDataType_Double: return DataTypeCompareT((const double*)arg_1, (const double*)arg_2); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return 0; +} + +template +static bool DataTypeClampT(T* v, const T* v_min, const T* v_max) +{ + // Clamp, both sides are optional, return true if modified + if (v_min && *v < *v_min) { *v = *v_min; return true; } + if (v_max && *v > *v_max) { *v = *v_max; return true; } + return false; +} + +bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeClampT((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max); + case ImGuiDataType_U8: return DataTypeClampT((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max); + case ImGuiDataType_S16: return DataTypeClampT((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max); + case ImGuiDataType_U16: return DataTypeClampT((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max); + case ImGuiDataType_S32: return DataTypeClampT((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max); + case ImGuiDataType_U32: return DataTypeClampT((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max); + case ImGuiDataType_S64: return DataTypeClampT((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max); + case ImGuiDataType_U64: return DataTypeClampT((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max); + case ImGuiDataType_Float: return DataTypeClampT((float* )p_data, (const float* )p_min, (const float* )p_max); + case ImGuiDataType_Double: return DataTypeClampT((double*)p_data, (const double*)p_min, (const double*)p_max); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data) +{ + ImGuiContext& g = *GImGui; + return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0; +} + +static float GetMinimumStepAtDecimalPrecision(int decimal_precision) +{ + static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f }; + if (decimal_precision < 0) + return FLT_MIN; + return (decimal_precision < IM_ARRAYSIZE(min_steps)) ? min_steps[decimal_precision] : ImPow(10.0f, (float)-decimal_precision); +} + +template +TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v) +{ + IM_UNUSED(data_type); + IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double); + const char* fmt_start = ImParseFormatFindStart(format); + if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string + return v; + + // Sanitize format + char fmt_sanitized[32]; + ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized)); + fmt_start = fmt_sanitized; + + // Format value with our rounding, and read back + char v_str[64]; + ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v); + const char* p = v_str; + while (*p == ' ') + p++; + v = (TYPE)ImAtof(p); + + return v; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: DragScalar, DragFloat, DragInt, etc. +//------------------------------------------------------------------------- +// - DragBehaviorT<>() [Internal] +// - DragBehavior() [Internal] +// - DragScalar() +// - DragScalarN() +// - DragFloat() +// - DragFloat2() +// - DragFloat3() +// - DragFloat4() +// - DragFloatRange2() +// - DragInt() +// - DragInt2() +// - DragInt3() +// - DragInt4() +// - DragIntRange2() +//------------------------------------------------------------------------- + +// This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls) +template +bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; + const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange))); + const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround); + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + + // Default tweak speed + if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX)) + v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio); + + // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings + float adjust_delta = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + { + adjust_delta = g.IO.MouseDelta[axis]; + if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) + adjust_delta *= 1.0f / 100.0f; + if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) + adjust_delta *= 10.0f; + } + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + { + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); + const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); + const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f; + adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor; + v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); + } + adjust_delta *= v_speed; + + // For vertical drag we currently assume that Up=higher value (like we do with vertical sliders). This may become a parameter. + if (axis == ImGuiAxis_Y) + adjust_delta = -adjust_delta; + + // For logarithmic use our range is effectively 0..1 so scale the delta into that range + if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0 + adjust_delta /= (float)(v_max - v_min); + + // Clear current value on activation + // Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300. + const bool is_just_activated = g.ActiveIdIsJustActivated; + const bool is_already_past_limits_and_pushing_outward = is_bounded && !is_wrapped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f)); + if (is_just_activated || is_already_past_limits_and_pushing_outward) + { + g.DragCurrentAccum = 0.0f; + g.DragCurrentAccumDirty = false; + } + else if (adjust_delta != 0.0f) + { + g.DragCurrentAccum += adjust_delta; + g.DragCurrentAccumDirty = true; + } + + if (!g.DragCurrentAccumDirty) + return false; + + TYPE v_cur = *v; + FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f; + + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense) + if (is_logarithmic) + { + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + + // Convert to parametric space, apply delta, convert back + float v_old_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + float v_new_parametric = v_old_parametric + g.DragCurrentAccum; + v_cur = ScaleValueFromRatioT(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + v_old_ref_for_accum_remainder = v_old_parametric; + } + else + { + v_cur += (SIGNEDTYPE)g.DragCurrentAccum; + } + + // Round to user desired precision based on format string + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_cur = RoundScalarWithFormatT(format, data_type, v_cur); + + // Preserve remainder after rounding has been applied. This also allow slow tweaking of values. + g.DragCurrentAccumDirty = false; + if (is_logarithmic) + { + // Convert to parametric space, apply delta, convert back + float v_new_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder); + } + else + { + g.DragCurrentAccum -= (float)((SIGNEDTYPE)v_cur - (SIGNEDTYPE)*v); + } + + // Lose zero sign for float/double + if (v_cur == (TYPE)-0) + v_cur = (TYPE)0; + + if (*v != v_cur && is_bounded) + { + if (is_wrapped) + { + // Wrap values + if (v_cur < v_min) + v_cur += v_max - v_min + (is_floating_point ? 0 : 1); + if (v_cur > v_max) + v_cur -= v_max - v_min + (is_floating_point ? 0 : 1); + } + else + { + // Clamp values + handle overflow/wrap-around for integer types. + if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point)) + v_cur = v_min; + if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point)) + v_cur = v_max; + } + } + + // Apply result + if (*v == v_cur) + return false; + *v = v_cur; + return true; +} + +bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + { + // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation. + if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) + ClearActiveID(); + else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + ClearActiveID(); + } + if (g.ActiveId != id) + return false; + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + return false; + + switch (data_type) + { + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } + case ImGuiDataType_S32: return DragBehaviorT(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags); + case ImGuiDataType_U32: return DragBehaviorT(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags); + case ImGuiDataType_S64: return DragBehaviorT(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags); + case ImGuiDataType_U64: return DragBehaviorT(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags); + case ImGuiDataType_Float: return DragBehaviorT(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags); + case ImGuiDataType_Double: return DragBehaviorT(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional. +// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); + if (!temp_input_is_active) + { + // Tabbing or CTRL+click on Drag turns it into an InputText + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id)); + const bool make_active = (clicked || double_clicked || g.NavActivateId == id); + if (make_active && (clicked || double_clicked)) + SetKeyOwner(ImGuiKey_MouseLeft, id); + if (make_active && temp_input_allowed) + if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + temp_input_is_active = true; + + // (Optional) simple click (without moving) turns Drag into an InputText + if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active) + if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + { + g.NavActivateId = id; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + temp_input_is_active = true; + } + + // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert) + if (make_active) + memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size); + + if (make_active && !temp_input_is_active) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + } + } + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp) + bool clamp_enabled = false; + if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL)) + { + const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max + if (p_min == NULL || p_max == NULL || clamp_range_dir < 0) + clamp_enabled = true; + else if (clamp_range_dir == 0) + clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true; + } + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + // Drag behavior + const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags); + if (value_changed) + MarkItemEdited(id); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); + return value_changed; +} + +bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags); + PopID(); + PopItemWidth(); + p_data = (void*)((char*)p_data + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags); +} + +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2, CalcItemWidth()); + + float min_min = (v_min >= v_max) ? -FLT_MAX : v_min; + float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + float max_max = (v_min >= v_max) ? FLT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextEx(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +// NB: v_speed is float to allow adjusting the drag speed with more precision +bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags); +} + +bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags); +} + +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2, CalcItemWidth()); + + int min_min = (v_min >= v_max) ? INT_MIN : v_min; + int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + int max_max = (v_min >= v_max) ? INT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextEx(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. +//------------------------------------------------------------------------- +// - ScaleRatioFromValueT<> [Internal] +// - ScaleValueFromRatioT<> [Internal] +// - SliderBehaviorT<>() [Internal] +// - SliderBehavior() [Internal] +// - SliderScalar() +// - SliderScalarN() +// - SliderFloat() +// - SliderFloat2() +// - SliderFloat3() +// - SliderFloat4() +// - SliderAngle() +// - SliderInt() +// - SliderInt2() +// - SliderInt3() +// - SliderInt4() +// - VSliderScalar() +// - VSliderFloat() +// - VSliderInt() +//------------------------------------------------------------------------- + +// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT) +template +float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) +{ + if (v_min == v_max) + return 0.0f; + IM_UNUSED(data_type); + + const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); + if (is_logarithmic) + { + bool flipped = v_max < v_min; + + if (flipped) // Handle the case where the range is backwards + ImSwap(v_min, v_max); + + // Fudge min/max to avoid getting close to log(0) + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_min == 0.0f) && (v_max < 0.0f)) + v_min_fudged = -logarithmic_zero_epsilon; + else if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float result; + if (v_clamped <= v_min_fudged) + result = 0.0f; // Workaround for values that are in-range but below our fudge + else if (v_clamped >= v_max_fudged) + result = 1.0f; // Workaround for values that are in-range but above our fudge + else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions + { + float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine) + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (v == 0.0f) + result = zero_point_center; // Special case for exactly zero + else if (v < 0.0f) + result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L; + else + result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R)); + } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged)); + else + result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged)); + + return flipped ? (1.0f - result) : result; + } + else + { + // Linear slider + return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min)); + } +} + +// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT) +template +TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) +{ + // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct" + // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler. + if (t <= 0.0f || v_min == v_max) + return v_min; + if (t >= 1.0f) + return v_max; + + TYPE result = (TYPE)0; + if (is_logarithmic) + { + // Fudge min/max to avoid getting silly results close to zero + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + const bool flipped = v_max < v_min; // Check if range is "backwards" + if (flipped) + ImSwap(v_min_fudged, v_max_fudged); + + // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range + + if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts + { + float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R) + result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise) + else if (t_with_flip < zero_point_center) + result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L)))); + else + result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R)))); + } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip))); + else + result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip)); + } + else + { + // Linear slider + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + if (is_floating_point) + { + result = ImLerp(v_min, v_max, t); + } + else if (t < 1.0) + { + // - For integer values we want the clicking position to match the grab box so we round above + // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property.. + // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64 + // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits. + FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t; + result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5))); + } + } + + return result; +} + +// FIXME: Try to move more of the code into shared SliderBehavior() +template +bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it. + + // Calculate bounds + const float grab_padding = 2.0f; // FIXME: Should be part of style. + const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f; + float grab_sz = style.GrabMinSize; + if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows + grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit + grab_sz = ImMin(grab_sz, slider_sz); + const float slider_usable_sz = slider_sz - grab_sz; + const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f; + const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f; + + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true + if (is_logarithmic) + { + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f); + } + + // Process interacting with the slider + bool value_changed = false; + if (g.ActiveId == id) + { + bool set_new_value = false; + float clicked_t = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (!g.IO.MouseDown[0]) + { + ClearActiveID(); + } + else + { + const float mouse_abs_pos = g.IO.MousePos[axis]; + if (g.ActiveIdIsJustActivated) + { + float grab_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (axis == ImGuiAxis_Y) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + const bool clicked_around_grab = (mouse_abs_pos >= grab_pos - grab_sz * 0.5f - 1.0f) && (mouse_abs_pos <= grab_pos + grab_sz * 0.5f + 1.0f); // No harm being extra generous here. + g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f; + } + if (slider_usable_sz > 0.0f) + clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz); + if (axis == ImGuiAxis_Y) + clicked_t = 1.0f - clicked_t; + set_new_value = true; + } + } + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + { + if (g.ActiveIdIsJustActivated) + { + g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation + g.SliderCurrentAccumDirty = false; + } + + float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis); + if (input_delta != 0.0f) + { + const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); + const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + if (decimal_precision > 0) + { + input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds + if (tweak_slow) + input_delta /= 10.0f; + } + else + { + if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow) + input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps + else + input_delta /= 100.0f; + } + if (tweak_fast) + input_delta *= 10.0f; + + g.SliderCurrentAccum += input_delta; + g.SliderCurrentAccumDirty = true; + } + + float delta = g.SliderCurrentAccum; + if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + { + ClearActiveID(); + } + else if (g.SliderCurrentAccumDirty) + { + clicked_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits + { + set_new_value = false; + g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate + } + else + { + set_new_value = true; + float old_clicked_t = clicked_t; + clicked_t = ImSaturate(clicked_t + delta); + + // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); + float new_clicked_t = ScaleRatioFromValueT(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + if (delta > 0) + g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta); + else + g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta); + } + + g.SliderCurrentAccumDirty = false; + } + } + + if (set_new_value) + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + set_new_value = false; + + if (set_new_value) + { + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + // Round to user desired precision based on format string + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); + + // Apply result + if (*v != v_new) + { + *v = v_new; + value_changed = true; + } + } + } + + if (slider_sz < 1.0f) + { + *out_grab_bb = ImRect(bb.Min, bb.Min); + } + else + { + // Output grab position so it can be displayed by the caller + float grab_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (axis == ImGuiAxis_Y) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + if (axis == ImGuiAxis_X) + *out_grab_bb = ImRect(grab_pos - grab_sz * 0.5f, bb.Min.y + grab_padding, grab_pos + grab_sz * 0.5f, bb.Max.y - grab_padding); + else + *out_grab_bb = ImRect(bb.Min.x + grab_padding, grab_pos - grab_sz * 0.5f, bb.Max.x - grab_padding, grab_pos + grab_sz * 0.5f); + } + + return value_changed; +} + +// For 32-bit and larger types, slider bounds are limited to half the natural type range. +// So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok. +// It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders. +bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) +{ + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX() + + switch (data_type) + { + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } + case ImGuiDataType_S32: + IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_U32: + IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_S64: + IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_U64: + IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_Float: + IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_Double: + IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required. +// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); + if (!temp_input_is_active) + { + // Tabbing or CTRL+click on Slider turns it into an input box + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + const bool make_active = (clicked || g.NavActivateId == id); + if (make_active && clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); + if (make_active && temp_input_allowed) + if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + temp_input_is_active = true; + + // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert) + if (make_active) + memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size); + + if (make_active && !temp_input_is_active) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + } + } + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp) + const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0; + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); + + // Slider behavior + ImRect grab_bb; + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb); + if (value_changed) + MarkItemEdited(id); + + // Render grab + if (grab_bb.Max.x > grab_bb.Min.x) + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); + return value_changed; +} + +// Add multiple sliders on 1 line for compact edition of multiple components +bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags); + PopID(); + PopItemWidth(); + v = (void*)((char*)v + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags) +{ + if (format == NULL) + format = "%.0f deg"; + float v_deg = (*v_rad) * 360.0f / (2 * IM_PI); + bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags); + if (value_changed) + *v_rad = v_deg * (2 * IM_PI) / 360.0f; + return value_changed; +} + +bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags); +} + +bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags); +} + +bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); + const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(frame_bb, id)) + return false; + + // Default format string when passing NULL + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + if (clicked || g.NavActivateId == id) + { + if (clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + } + + // Draw frame + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); + + // Slider behavior + ImRect grab_bb; + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb); + if (value_changed) + MarkItemEdited(id); + + // Render grab + if (grab_bb.Max.y > grab_bb.Min.y) + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + // For the vertical slider we allow centered text to overlap the frame padding + char value_buf[64]; + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + return value_changed; +} + +bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) +{ + return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); +} + +bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) +{ + return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: InputScalar, InputFloat, InputInt, etc. +//------------------------------------------------------------------------- +// - ImParseFormatFindStart() [Internal] +// - ImParseFormatFindEnd() [Internal] +// - ImParseFormatTrimDecorations() [Internal] +// - ImParseFormatSanitizeForPrinting() [Internal] +// - ImParseFormatSanitizeForScanning() [Internal] +// - ImParseFormatPrecision() [Internal] +// - TempInputTextScalar() [Internal] +// - InputScalar() +// - InputScalarN() +// - InputFloat() +// - InputFloat2() +// - InputFloat3() +// - InputFloat4() +// - InputInt() +// - InputInt2() +// - InputInt3() +// - InputInt4() +// - InputDouble() +//------------------------------------------------------------------------- + +// We don't use strchr() because our strings are usually very short and often start with '%' +const char* ImParseFormatFindStart(const char* fmt) +{ + while (char c = fmt[0]) + { + if (c == '%' && fmt[1] != '%') + return fmt; + else if (c == '%') + fmt++; + fmt++; + } + return fmt; +} + +const char* ImParseFormatFindEnd(const char* fmt) +{ + // Printf/scanf types modifiers: I/L/h/j/l/t/w/z. Other uppercase letters qualify as types aka end of the format. + if (fmt[0] != '%') + return fmt; + const unsigned int ignored_uppercase_mask = (1 << ('I'-'A')) | (1 << ('L'-'A')); + const unsigned int ignored_lowercase_mask = (1 << ('h'-'a')) | (1 << ('j'-'a')) | (1 << ('l'-'a')) | (1 << ('t'-'a')) | (1 << ('w'-'a')) | (1 << ('z'-'a')); + for (char c; (c = *fmt) != 0; fmt++) + { + if (c >= 'A' && c <= 'Z' && ((1 << (c - 'A')) & ignored_uppercase_mask) == 0) + return fmt + 1; + if (c >= 'a' && c <= 'z' && ((1 << (c - 'a')) & ignored_lowercase_mask) == 0) + return fmt + 1; + } + return fmt; +} + +// Extract the format out of a format string with leading or trailing decorations +// fmt = "blah blah" -> return "" +// fmt = "%.3f" -> return fmt +// fmt = "hello %.3f" -> return fmt + 6 +// fmt = "%.3f hello" -> return buf written with "%.3f" +const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_size) +{ + const char* fmt_start = ImParseFormatFindStart(fmt); + if (fmt_start[0] != '%') + return ""; + const char* fmt_end = ImParseFormatFindEnd(fmt_start); + if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data. + return fmt_start; + ImStrncpy(buf, fmt_start, ImMin((size_t)(fmt_end - fmt_start) + 1, buf_size)); + return buf; +} + +// Sanitize format +// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi +// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi. +void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size) +{ + const char* fmt_end = ImParseFormatFindEnd(fmt_in); + IM_UNUSED(fmt_out_size); + IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! + while (fmt_in < fmt_end) + { + char c = *fmt_in++; + if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. + *(fmt_out++) = c; + } + *fmt_out = 0; // Zero-terminate +} + +// - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d" +const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size) +{ + const char* fmt_end = ImParseFormatFindEnd(fmt_in); + const char* fmt_out_begin = fmt_out; + IM_UNUSED(fmt_out_size); + IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! + bool has_type = false; + while (fmt_in < fmt_end) + { + char c = *fmt_in++; + if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#')) + continue; + has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits + if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. + *(fmt_out++) = c; + } + *fmt_out = 0; // Zero-terminate + return fmt_out_begin; +} + +template +static const char* ImAtoi(const char* src, TYPE* output) +{ + int negative = 0; + if (*src == '-') { negative = 1; src++; } + if (*src == '+') { src++; } + TYPE v = 0; + while (*src >= '0' && *src <= '9') + v = (v * 10) + (*src++ - '0'); + *output = negative ? -v : v; + return src; +} + +// Parse display precision back from the display format string +// FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed. +int ImParseFormatPrecision(const char* fmt, int default_precision) +{ + fmt = ImParseFormatFindStart(fmt); + if (fmt[0] != '%') + return default_precision; + fmt++; + while (*fmt >= '0' && *fmt <= '9') + fmt++; + int precision = INT_MAX; + if (*fmt == '.') + { + fmt = ImAtoi(fmt + 1, &precision); + if (precision < 0 || precision > 99) + precision = default_precision; + } + if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation + precision = -1; + if ((*fmt == 'g' || *fmt == 'G') && precision == INT_MAX) + precision = -1; + return (precision == INT_MAX) ? default_precision : precision; +} + +// Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets) +// FIXME: Facilitate using this in variety of other situations. +// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but +// the expected relationship between TempInputXXX functions and LastItemData is a little fishy. +bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags) +{ + // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id. + // We clear ActiveID on the first frame to allow the InputText() taking it back. + ImGuiContext& g = *GImGui; + const bool init = (g.TempInputId != id); + if (init) + ClearActiveID(); + + g.CurrentWindow->DC.CursorPos = bb.Min; + g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; + bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem); + if (init) + { + // First frame we started displaying the InputText widget, we expect it to take the active id. + IM_ASSERT(g.ActiveId == id); + g.TempInputId = g.ActiveId; + } + return value_changed; +} + +// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set! +// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility. +// However this may not be ideal for all uses, as some user code may break on out of bound values. +bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max) +{ + // FIXME: May need to clarify display behavior if format doesn't contain %. + // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405 + ImGuiContext& g = *GImGui; + const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); + char fmt_buf[32]; + char data_buf[32]; + format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf)); + if (format[0] == 0) + format = type_info->PrintFmt; + DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); + ImStrTrimBlanks(data_buf); + + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; + g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData. + bool value_changed = false; + if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) + { + // Backup old value + size_t data_type_size = type_info->Size; + ImGuiDataTypeStorage data_backup; + memcpy(&data_backup, p_data, data_type_size); + + // Apply new value (or operations) then clamp + DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL); + if (p_clamp_min || p_clamp_max) + { + if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0) + ImSwap(p_clamp_min, p_clamp_max); + DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max); + } + + // Only mark as edited if new value is different + g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited; + value_changed = memcmp(&data_backup, p_data, data_type_size) != 0; + if (value_changed) + MarkItemEdited(id); + } + return value_changed; +} + +void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal; + memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size); +} + +// Note: p_data, p_step, p_step_fast are _pointers_ to a memory address holding the data. For an Input widget, p_step and p_step_fast are optional. +// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()! + + if (format == NULL) + format = DataTypeGetInfo(data_type)->PrintFmt; + + void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue; + + char buf[64]; + if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0) + buf[0] = 0; + else + DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); + + // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited. + // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. + g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; + flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; + + bool value_changed = false; + if (p_step == NULL) + { + if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + } + else + { + const float button_size = GetFrameHeight(); + + BeginGroup(); // The only purpose of the group here is to allow the caller to query item data e.g. IsItemActive() + PushID(label); + SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); + if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); + + // Step buttons + const ImVec2 backup_frame_padding = style.FramePadding; + style.FramePadding.x = style.FramePadding.y; + if (flags & ImGuiInputTextFlags_ReadOnly) + BeginDisabled(); + PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("-", ImVec2(button_size, button_size))) + { + DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + SameLine(0, style.ItemInnerSpacing.x); + if (ButtonEx("+", ImVec2(button_size, button_size))) + { + DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); + value_changed = true; + } + PopItemFlag(); + if (flags & ImGuiInputTextFlags_ReadOnly) + EndDisabled(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0, style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + style.FramePadding = backup_frame_padding; + + PopID(); + EndGroup(); + } + + g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited; + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + bool value_changed = false; + BeginGroup(); + PushID(label); + PushMultiItemsWidths(components, CalcItemWidth()); + size_t type_size = GDataTypeInfo[data_type].Size; + for (int i = 0; i < components; i++) + { + PushID(i); + if (i > 0) + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags); + PopID(); + PopItemWidth(); + p_data = (void*)((char*)p_data + type_size); + } + PopID(); + + const char* label_end = FindRenderedTextEnd(label); + if (label != label_end) + { + SameLine(0.0f, g.Style.ItemInnerSpacing.x); + TextEx(label, label_end); + } + + EndGroup(); + return value_changed; +} + +bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) +{ + return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags); +} + +bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags); +} + +bool ImGui::InputFloat3(const char* label, float v[3], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags); +} + +bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags); +} + +bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags) +{ + // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. + const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; + return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags); +} + +bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 2, NULL, NULL, "%d", flags); +} + +bool ImGui::InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 3, NULL, NULL, "%d", flags); +} + +bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) +{ + return InputScalarN(label, ImGuiDataType_S32, v, 4, NULL, NULL, "%d", flags); +} + +bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) +{ + return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint +//------------------------------------------------------------------------- +// - imstb_textedit.h include +// - InputText() +// - InputTextWithHint() +// - InputTextMultiline() +// - InputTextGetCharInfo() [Internal] +// - InputTextReindexLines() [Internal] +// - InputTextReindexLinesRange() [Internal] +// - InputTextEx() [Internal] +// - DebugNodeInputTextState() [Internal] +//------------------------------------------------------------------------- + +namespace ImStb +{ +#include "imstb_textedit.h" +} + +bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() + return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); +} + +bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + return InputTextEx(label, NULL, buf, (int)buf_size, size, flags | ImGuiInputTextFlags_Multiline, callback, user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint. + return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); +} + +// This is only used in the path where the multiline widget is inactivate. +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) +{ + int line_count = 0; + const char* s = text_begin; + while (true) + { + const char* s_eol = strchr(s, '\n'); + line_count++; + if (s_eol == NULL) + { + s = s + ImStrlen(s); + break; + } + s = s_eol + 1; + } + *out_text_end = s; + return line_count; +} + +// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA() +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line) +{ + ImGuiContext& g = *ctx; + ImFont* font = g.Font; + const float line_height = g.FontSize; + const float scale = line_height / font->FontSize; + + ImVec2 text_size = ImVec2(0, 0); + float line_width = 0.0f; + + const char* s = text_begin; + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + if (stop_on_new_line) + break; + continue; + } + if (c == '\r') + continue; + + const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale; + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (out_offset) + *out_offset = ImVec2(line_width, text_size.y + line_height); // offset allow for the possibility of sitting after a trailing \n + + if (line_width > 0 || text_size.y == 0.0f) // whereas size.y will ignore the trailing \n + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +// Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) +// With our UTF-8 use of stb_textedit: +// - STB_TEXTEDIT_GETCHAR is nothing more than a a "GETBYTE". It's only used to compare to ascii or to copy blocks of text so we are fine. +// - One exception is the STB_TEXTEDIT_IS_SPACE feature which would expect a full char in order to handle full-width space such as 0x3000 (see ImCharIsBlankW). +// - ...but we don't use that feature. +namespace ImStb +{ +static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; } +static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; } +static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; } +static char STB_TEXTEDIT_NEWLINE = '\n'; +static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) +{ + const char* text = obj->TextSrc; + const char* text_remaining = NULL; + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true); + r->x0 = 0.0f; + r->x1 = size.x; + r->baseline_y_delta = size.y; + r->ymin = 0.0f; + r->ymax = size.y; + r->num_chars = (int)(text_remaining - (text + line_start_idx)); +} + +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL + +static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + if (idx >= obj->TextLen) + return obj->TextLen + 1; + unsigned int c; + return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen); +} + +static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) +{ + if (idx <= 0) + return -1; + const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx); + return (int)(p - obj->TextSrc); +} + +static bool ImCharIsSeparatorW(unsigned int c) +{ + static const unsigned int separator_list[] = + { + ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D, + '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F, + '\n', '\r', + }; + for (unsigned int separator : separator_list) + if (c == separator) + return true; + return false; +} + +static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) +{ + // When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators. + if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) + return 0; + + const char* curr_p = obj->TextSrc + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen); + + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); + return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); +} +static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) +{ + if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) + return 0; + + const char* curr_p = obj->TextSrc + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen); + + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); + return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); +} +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + return idx < 0 ? 0 : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) +{ + int len = obj->TextLen; + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + while (idx < len && !is_word_boundary_from_left(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + int len = obj->TextLen; + while (idx < len && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } +#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL + +static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) +{ + // Offset remaining text (+ copy zero terminator) + IM_ASSERT(obj->TextSrc == obj->TextA.Data); + char* dst = obj->TextA.Data + pos; + char* src = obj->TextA.Data + pos + n; + memmove(dst, src, obj->TextLen - n - pos + 1); + obj->Edited = true; + obj->TextLen -= n; +} + +static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len) +{ + const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0; + const int text_len = obj->TextLen; + IM_ASSERT(pos <= text_len); + + if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity)) + return false; + + // Grow internal buffer if needed + IM_ASSERT(obj->TextSrc == obj->TextA.Data); + if (new_text_len + text_len + 1 > obj->TextA.Size) + { + if (!is_resizable) + return false; + obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1); + obj->TextSrc = obj->TextA.Data; + } + + char* text = obj->TextA.Data; + if (pos != text_len) + memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos)); + memcpy(text + pos, new_text, (size_t)new_text_len); + + obj->Edited = true; + obj->TextLen += new_text_len; + obj->TextA[obj->TextLen] = '\0'; + + return true; +} + +// We don't use an enum so we can build even with conflicting symbols (if another user of stb_textedit.h leak their STB_TEXTEDIT_K_* symbols) +#define STB_TEXTEDIT_K_LEFT 0x200000 // keyboard input to move cursor left +#define STB_TEXTEDIT_K_RIGHT 0x200001 // keyboard input to move cursor right +#define STB_TEXTEDIT_K_UP 0x200002 // keyboard input to move cursor up +#define STB_TEXTEDIT_K_DOWN 0x200003 // keyboard input to move cursor down +#define STB_TEXTEDIT_K_LINESTART 0x200004 // keyboard input to move cursor to start of line +#define STB_TEXTEDIT_K_LINEEND 0x200005 // keyboard input to move cursor to end of line +#define STB_TEXTEDIT_K_TEXTSTART 0x200006 // keyboard input to move cursor to start of text +#define STB_TEXTEDIT_K_TEXTEND 0x200007 // keyboard input to move cursor to end of text +#define STB_TEXTEDIT_K_DELETE 0x200008 // keyboard input to delete selection or character under cursor +#define STB_TEXTEDIT_K_BACKSPACE 0x200009 // keyboard input to delete selection or character left of cursor +#define STB_TEXTEDIT_K_UNDO 0x20000A // keyboard input to perform undo +#define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo +#define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word +#define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word +#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page +#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page +#define STB_TEXTEDIT_K_SHIFT 0x400000 + +#define IMSTB_TEXTEDIT_IMPLEMENTATION +#define IMSTB_TEXTEDIT_memmove memmove +#include "imstb_textedit.h" + +// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling +// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) +static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len); + ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen); + state->cursor = state->select_start = state->select_end = 0; + if (text_len <= 0) + return; + if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len)) + { + state->cursor = state->select_start = state->select_end = text_len; + state->has_preferred_x = 0; + return; + } + IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace() +} + +} // namespace ImStb + +// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators. +ImGuiInputTextState::ImGuiInputTextState() +{ + memset(this, 0, sizeof(*this)); + Stb = IM_NEW(ImStbTexteditState); + memset(Stb, 0, sizeof(*Stb)); +} + +ImGuiInputTextState::~ImGuiInputTextState() +{ + IM_DELETE(Stb); +} + +void ImGuiInputTextState::OnKeyPressed(int key) +{ + stb_textedit_key(this, Stb, key); + CursorFollow = true; + CursorAnimReset(); +} + +void ImGuiInputTextState::OnCharPressed(unsigned int c) +{ + // Convert the key to a UTF8 byte sequence. + // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great. + char utf8[5]; + ImTextCharToUtf8(utf8, c); + stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8)); + CursorFollow = true; + CursorAnimReset(); +} + +// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header. +void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking +void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); } +bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; } +void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; } +int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; } +int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; } +int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; } +void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; } +void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } +void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; } +void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; } + +ImGuiInputTextCallbackData::ImGuiInputTextCallbackData() +{ + memset(this, 0, sizeof(*this)); +} + +// Public API to manipulate UTF-8 text from within a callback. +// FIXME: The existence of this rarely exercised code path is a bit of a nuisance. +// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar +// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both. +void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) +{ + IM_ASSERT(pos + bytes_count <= BufTextLen); + char* dst = Buf + pos; + const char* src = Buf + pos + bytes_count; + memmove(dst, src, BufTextLen - bytes_count - pos + 1); + + if (CursorPos >= pos + bytes_count) + CursorPos -= bytes_count; + else if (CursorPos >= pos) + CursorPos = pos; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen -= bytes_count; +} + +void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) +{ + // Accept null ranges + if (new_text == new_text_end) + return; + + // Grow internal buffer if needed + const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; + const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text); + if (new_text_len + BufTextLen >= BufSize) + { + if (!is_resizable) + return; + + ImGuiContext& g = *Ctx; + ImGuiInputTextState* edit_state = &g.InputTextState; + IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID); + IM_ASSERT(Buf == edit_state->TextA.Data); + int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1; + edit_state->TextA.resize(new_buf_size + 1); + edit_state->TextSrc = edit_state->TextA.Data; + Buf = edit_state->TextA.Data; + BufSize = edit_state->BufCapacity = new_buf_size; + } + + if (BufTextLen != pos) + memmove(Buf + pos + new_text_len, Buf + pos, (size_t)(BufTextLen - pos)); + memcpy(Buf + pos, new_text, (size_t)new_text_len * sizeof(char)); + Buf[BufTextLen + new_text_len] = '\0'; + + if (CursorPos >= pos) + CursorPos += new_text_len; + SelectionStart = SelectionEnd = CursorPos; + BufDirty = true; + BufTextLen += new_text_len; +} + +void ImGui::PushPasswordFont() +{ + ImGuiContext& g = *GImGui; + ImFont* in_font = g.Font; + ImFont* out_font = &g.InputTextPasswordFont; + ImFontGlyph* glyph = in_font->FindGlyph('*'); + out_font->FontSize = in_font->FontSize; + out_font->Scale = in_font->Scale; + out_font->Ascent = in_font->Ascent; + out_font->Descent = in_font->Descent; + out_font->ContainerAtlas = in_font->ContainerAtlas; + out_font->FallbackGlyph = glyph; + out_font->FallbackAdvanceX = glyph->AdvanceX; + IM_ASSERT(out_font->Glyphs.Size == 0 && out_font->IndexAdvanceX.Size == 0 && out_font->IndexLookup.Size == 0); + PushFont(out_font); +} + +// Return false to discard a character. +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard) +{ + unsigned int c = *p_char; + + // Filter non-printable (NB: isprint is unreliable! see #2467) + bool apply_named_filters = true; + if (c < 0x20) + { + bool pass = false; + pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code) + if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space + { + c = *p_char = ' '; + pass = true; + } + pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; + pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0; + if (!pass) + return false; + apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. + } + + if (input_source_is_clipboard == false) + { + // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817) + if (c == 127) + return false; + + // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME) + if (c >= 0xE000 && c <= 0xF8FF) + return false; + } + + // Filter Unicode ranges we are not handling in this build + if (c > IM_UNICODE_CODEPOINT_MAX) + return false; + + // Generic named filters + if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))) + { + // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'. + // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. + // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point. + // Change the default decimal_point with: + // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point; + // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. + ImGuiContext& g = *ctx; + const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint; + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)) + if (c == '.' || c == ',') + c = c_decimal_point; + + // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) + // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may + // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font. + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal)) + if (c >= 0xFF01 && c <= 0xFF5E) + c = c - 0xFF01 + 0x21; + + // Allow 0-9 . - + * / + if (flags & ImGuiInputTextFlags_CharsDecimal) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/')) + return false; + + // Allow 0-9 . - + * / e E + if (flags & ImGuiInputTextFlags_CharsScientific) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) + return false; + + // Allow 0-9 a-F A-F + if (flags & ImGuiInputTextFlags_CharsHexadecimal) + if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) + return false; + + // Turn a-z into A-Z + if (flags & ImGuiInputTextFlags_CharsUppercase) + if (c >= 'a' && c <= 'z') + c += (unsigned int)('A' - 'a'); + + if (flags & ImGuiInputTextFlags_CharsNoBlank) + if (ImCharIsBlankW(c)) + return false; + + *p_char = c; + } + + // Custom callback filter + if (flags & ImGuiInputTextFlags_CallbackCharFilter) + { + ImGuiContext& g = *GImGui; + ImGuiInputTextCallbackData callback_data; + callback_data.Ctx = &g; + callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; + callback_data.EventChar = (ImWchar)c; + callback_data.Flags = flags; + callback_data.UserData = user_data; + if (callback(&callback_data) != 0) + return false; + *p_char = callback_data.EventChar; + if (!callback_data.EventChar) + return false; + } + + return true; +} + +// Find the shortest single replacement we can make to get from old_buf to new_buf +// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately. +// FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly. +static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length) +{ + const int shorter_length = ImMin(old_length, new_length); + int first_diff; + for (first_diff = 0; first_diff < shorter_length; first_diff++) + if (old_buf[first_diff] != new_buf[first_diff]) + break; + if (first_diff == old_length && first_diff == new_length) + return; + + int old_last_diff = old_length - 1; + int new_last_diff = new_length - 1; + for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--) + if (old_buf[old_last_diff] != new_buf[new_last_diff]) + break; + + const int insert_len = new_last_diff - first_diff + 1; + const int delete_len = old_last_diff - first_diff + 1; + if (insert_len > 0 || delete_len > 0) + if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len)) + for (int i = 0; i < delete_len; i++) + p[i] = old_buf[first_diff + i]; +} + +// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables) +// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714) +// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick, +// but that more likely be attractive when we do have _NoLiveEdit flag available. +void ImGui::InputTextDeactivateHook(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiInputTextState* state = &g.InputTextState; + if (id == 0 || state->ID != id) + return; + g.InputTextDeactivatedState.ID = state->ID; + if (state->Flags & ImGuiInputTextFlags_ReadOnly) + { + g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat. + } + else + { + IM_ASSERT(state->TextA.Data != 0); + IM_ASSERT(state->TextA[state->TextLen] == 0); + g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1); + memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1); + } +} + +// Edit a string of text +// - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". +// This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match +// Note that in std::string world, capacity() would omit 1 byte used by the zero-terminator. +// - When active, hold on a privately held copy of the text (and apply back to 'buf'). So changing 'buf' while the InputText is active has no effect. +// - If you want to use ImGui::InputText() with std::string, see misc/cpp/imgui_stdlib.h +// (FIXME: Rather confusing and messy function, among the worse part of our codebase, expecting to rewrite a V2 at some point.. Partly because we are +// doing UTF8 > U16 > UTF8 conversions on the go to easily interface with stb_textedit. Ideally should stay in UTF-8 all the time. See https://github.com/nothings/stb/issues/188) +bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* callback_user_data) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + IM_ASSERT(buf != NULL && buf_size >= 0); + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) + IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) + IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming + + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + const ImGuiStyle& style = g.Style; + + const bool RENDER_SELECTION_WHEN_INACTIVE = false; + const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; + + if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) + BeginGroup(); + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line + const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size); + + ImGuiWindow* draw_window = window; + ImVec2 inner_size = frame_size; + ImGuiLastItemData item_data_backup; + if (is_multiline) + { + ImVec2 backup_pos = window->DC.CursorPos; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + { + EndGroup(); + return false; + } + item_data_backup = g.LastItemData; + window->DC.CursorPos = backup_pos; + + // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping. + if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput)) + g.NavActivateId = 0; + + // Prevent NavActivate reactivating in BeginChild() when we are already active. + const ImGuiID backup_activate_id = g.NavActivateId; + if (g.ActiveId == id) // Prevent reactivation + g.NavActivateId = 0; + + // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug. + PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges + bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove); + g.NavActivateId = backup_activate_id; + PopStyleVar(3); + PopStyleColor(); + if (!child_visible) + { + EndChild(); + EndGroup(); + return false; + } + draw_window = g.CurrentWindow; // Child window + draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it. + draw_window->DC.CursorPos += style.FramePadding; + inner_size.x -= draw_window->ScrollbarSizes.x; + } + else + { + // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd) + ItemSize(total_bb, style.FramePadding.y); + if (!(flags & ImGuiInputTextFlags_MergedItem)) + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + return false; + } + + // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417) + bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover); + if (hovered) + SetMouseCursor(ImGuiMouseCursor_TextInput); + if (hovered && g.NavHighlightItemUnderNav) + hovered = false; + + // We are only allowed to access the state if we are already the active widget. + ImGuiInputTextState* state = GetInputTextState(id); + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) + flags |= ImGuiInputTextFlags_ReadOnly; + const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; + const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; + const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; + const bool is_resizable = (flags & ImGuiInputTextFlags_CallbackResize) != 0; + if (is_resizable) + IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! + + const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard))); + + const bool user_clicked = hovered && io.MouseClicked[0]; + const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); + const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); + bool clear_active_id = false; + bool select_all = false; + + float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; + + const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf); + const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state. + const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav); + const bool init_state = (init_make_active || user_scroll_active); + if (init_reload_from_user_buf) + { + int new_len = (int)ImStrlen(buf); + IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?"); + state->WantReloadUserBuf = false; + InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len); + state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. + state->TextLen = new_len; + memcpy(state->TextA.Data, buf, state->TextLen + 1); + state->Stb->select_start = state->ReloadSelectionStart; + state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; + state->CursorClamp(); + } + else if ((init_state && g.ActiveId != id) || init_changed_specs) + { + // Access state even if we don't own it yet. + state = &g.InputTextState; + state->CursorAnimReset(); + + // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714) + InputTextDeactivateHook(state->ID); + + // Take a copy of the initial buffer value. + // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) + const int buf_len = (int)ImStrlen(buf); + IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?"); + state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->TextToRevertTo.Data, buf, buf_len + 1); + + // Preserve cursor position and undo/redo stack if we come back to same widget + // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? + bool recycle_state = (state->ID == id && !init_changed_specs); + if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0))) + recycle_state = false; + + // Start edition + state->ID = id; + state->TextLen = buf_len; + if (!is_readonly) + { + state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->TextA.Data, buf, state->TextLen + 1); + } + + // Find initial scroll position for right alignment + state->Scroll = ImVec2(0.0f, 0.0f); + if (flags & ImGuiInputTextFlags_ElideLeft) + state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f); + + // Recycle existing cursor/selection/undo stack but clamp position + // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. + if (recycle_state) + state->CursorClamp(); + else + stb_textedit_initialize_state(state->Stb, !is_multiline); + + if (!is_multiline) + { + if (flags & ImGuiInputTextFlags_AutoSelectAll) + select_all = true; + if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState))) + select_all = true; + if (user_clicked && io.KeyCtrl) + select_all = true; + } + + if (flags & ImGuiInputTextFlags_AlwaysOverwrite) + state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863) + } + + const bool is_osx = io.ConfigMacOSXBehaviors; + if (g.ActiveId != id && init_make_active) + { + IM_ASSERT(state && state->ID == id); + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + } + if (g.ActiveId == id) + { + // Declare some inputs, the other are registered and polled via Shortcut() routing system. + // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combinaison into individual shortcuts. + const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End }; + for (ImGuiKey key : always_owned_keys) + SetKeyOwner(key, id); + if (user_clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory)) + { + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + SetKeyOwner(ImGuiKey_UpArrow, id); + SetKeyOwner(ImGuiKey_DownArrow, id); + } + if (is_multiline) + { + SetKeyOwner(ImGuiKey_PageUp, id); + SetKeyOwner(ImGuiKey_PageDown, id); + } + // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu + if (is_osx) + SetKeyOwner(ImGuiMod_Alt, id); + + // Expose scroll in a manner that is agnostic to us using a child window + if (is_multiline && state != NULL) + state->Scroll.y = draw_window->Scroll.y; + + // Read-only mode always ever read from source buffer. Refresh TextLen when active. + if (is_readonly && state != NULL) + state->TextLen = (int)ImStrlen(buf); + //if (is_readonly && state != NULL) + // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation. + } + if (state != NULL) + state->TextSrc = is_readonly ? buf : state->TextA.Data; + + // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function) + if (g.ActiveId == id && state == NULL) + ClearActiveID(); + + // Release focus when we click outside + if (g.ActiveId == id && io.MouseClicked[0] && !init_state && !init_make_active) //-V560 + clear_active_id = true; + + // Lock the decision of whether we are going to take the path displaying the cursor or selection + bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active); + bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); + bool value_changed = false; + bool validated = false; + + // Select the buffer to render. + const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state; + bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + + // Password pushes a temporary font with only a fallback glyph + if (is_password && !is_displaying_hint) + PushPasswordFont(); + + // Process mouse inputs and character inputs + if (g.ActiveId == id) + { + IM_ASSERT(state != NULL); + state->Edited = false; + state->BufCapacity = buf_size; + state->Flags = flags; + + // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. + // Down the line we should have a cleaner library-wide concept of Selected vs Active. + g.ActiveIdAllowOverlap = !io.MouseDown[0]; + + // Edit in progress + const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x; + const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f)); + + if (select_all) + { + state->SelectAll(); + state->SelectedAllMouseLock = true; + } + else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift) + { + stb_textedit_click(state, state->Stb, mouse_x, mouse_y); + const int multiclick_count = (io.MouseClickedCount[0] - 2); + if ((multiclick_count % 2) == 0) + { + // Double-click: Select word + // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant: + // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS) + const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n'; + if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); + //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + if (!STB_TEXT_HAS_SELECTION(state->Stb)) + ImStb::stb_textedit_prep_selection_at_cursor(state->Stb); + state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor); + state->Stb->select_end = state->Stb->cursor; + ImStb::stb_textedit_clamp(state, state->Stb); + } + else + { + // Triple-click: Select line + const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n'; + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART); + state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT); + state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT); + if (!is_eol && is_multiline) + { + ImSwap(state->Stb->select_start, state->Stb->select_end); + state->Stb->cursor = state->Stb->select_end; + } + state->CursorFollow = false; + } + state->CursorAnimReset(); + } + else if (io.MouseClicked[0] && !state->SelectedAllMouseLock) + { + if (hovered) + { + if (io.KeyShift) + stb_textedit_drag(state, state->Stb, mouse_x, mouse_y); + else + stb_textedit_click(state, state->Stb, mouse_x, mouse_y); + state->CursorAnimReset(); + } + } + else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) + { + stb_textedit_drag(state, state->Stb, mouse_x, mouse_y); + state->CursorAnimReset(); + state->CursorFollow = true; + } + if (state->SelectedAllMouseLock && !io.MouseDown[0]) + state->SelectedAllMouseLock = false; + + // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336) + // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes) + if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly) + { + if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id)) + { + unsigned int c = '\t'; // Insert TAB + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); + } + // FIXME: Implement Shift+Tab + /* + if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id)) + { + } + */ + } + + // Process regular text input (before we check for Return because using some IME will effectively send a Return?) + // We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters. + const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl); + if (io.InputQueueCharacters.Size > 0) + { + if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav) + for (int n = 0; n < io.InputQueueCharacters.Size; n++) + { + // Insert character if they pass filtering + unsigned int c = (unsigned int)io.InputQueueCharacters[n]; + if (c == '\t') // Skip Tab, see above. + continue; + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); + } + + // Consume characters + io.InputQueueCharacters.resize(0); + } + } + + // Process other shortcuts/key-presses + bool revert_edit = false; + if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) + { + IM_ASSERT(state != NULL); + + const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1); + state->Stb->row_count_per_page = row_count_per_page; + + const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); + const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl + const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End + + // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: former would be handled by InputText) + // Otherwise we could simply assume that we own the keys as we are active. + const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat; + const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection()); + const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection()); + const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly; + const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable; + const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable; + const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id); + + // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful. + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true); + const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false)); + const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id)); + + // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of. + // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line. + if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } + else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } + else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } + else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } + else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut) + { + if (!state->HasSelection()) + { + // OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace) + if (is_wordmove_key_down) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + } + state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); + } + else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly) + { + if (!state->HasSelection()) + { + if (is_wordmove_key_down) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT); + else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper) + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT); + } + state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); + } + else if (is_enter_pressed || is_gamepad_validate) + { + // Determine if we turn Enter into a \n character + bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; + if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) + { + validated = true; + if (io.ConfigInputTextEnterKeepActive && !is_multiline) + state->SelectAll(); // No need to scroll + else + clear_active_id = true; + } + else if (!is_readonly) + { + unsigned int c = '\n'; // Insert new line + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); + } + } + else if (is_cancel) + { + if (flags & ImGuiInputTextFlags_EscapeClearsAll) + { + if (buf[0] != 0) + { + revert_edit = true; + } + else + { + render_cursor = render_selection = false; + clear_active_id = true; + } + } + else + { + clear_active_id = revert_edit = true; + render_cursor = render_selection = false; + } + } + else if (is_undo || is_redo) + { + state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO); + state->ClearSelection(); + } + else if (is_select_all) + { + state->SelectAll(); + state->CursorFollow = true; + } + else if (is_cut || is_copy) + { + // Cut, Copy + if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) + { + // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy. + const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0; + const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen; + g.TempBuffer.reserve(ie - ib + 1); + memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib); + g.TempBuffer.Data[ie - ib] = 0; + SetClipboardText(g.TempBuffer.Data); + } + if (is_cut) + { + if (!state->HasSelection()) + state->SelectAll(); + state->CursorFollow = true; + stb_textedit_cut(state, state->Stb); + } + } + else if (is_paste) + { + if (const char* clipboard = GetClipboardText()) + { + // Filter pasted buffer + const int clipboard_len = (int)ImStrlen(clipboard); + ImVector clipboard_filtered; + clipboard_filtered.reserve(clipboard_len + 1); + for (const char* s = clipboard; *s != 0; ) + { + unsigned int c; + int in_len = ImTextCharFromUtf8(&c, s, NULL); + s += in_len; + if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) + continue; + char c_utf8[5]; + ImTextCharToUtf8(c_utf8, c); + int out_len = (int)ImStrlen(c_utf8); + clipboard_filtered.resize(clipboard_filtered.Size + out_len); + memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len); + } + if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation + { + clipboard_filtered.push_back(0); + stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1); + state->CursorFollow = true; + } + } + } + + // Update render selection flag after events have been handled, so selection highlight can be displayed during the same frame. + render_selection |= state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); + } + + // Process callbacks and apply result back to user's buffer. + const char* apply_new_text = NULL; + int apply_new_text_length = 0; + if (g.ActiveId == id) + { + IM_ASSERT(state != NULL); + if (revert_edit && !is_readonly) + { + if (flags & ImGuiInputTextFlags_EscapeClearsAll) + { + // Clear input + IM_ASSERT(buf[0] != 0); + apply_new_text = ""; + apply_new_text_length = 0; + value_changed = true; + IMSTB_TEXTEDIT_CHARTYPE empty_string; + stb_textedit_replace(state, state->Stb, &empty_string, 0); + } + else if (strcmp(buf, state->TextToRevertTo.Data) != 0) + { + apply_new_text = state->TextToRevertTo.Data; + apply_new_text_length = state->TextToRevertTo.Size - 1; + + // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. + // Push records into the undo stack so we can CTRL+Z the revert operation itself + value_changed = true; + stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1); + } + } + + // FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId, + // even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks. + // If we do that, need to ensure that as special case, 'validated == true' also writes back. + // This also allows the user to use InputText() without maintaining any user-side storage. + // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object + // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). + const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); + if (apply_edit_back_to_user_buffer) + { + // Apply current edited text immediately. + // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer + + // User callback + if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0) + { + IM_ASSERT(callback != NULL); + + // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. + ImGuiInputTextFlags event_flag = 0; + ImGuiKey event_key = ImGuiKey_None; + if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id)) + { + event_flag = ImGuiInputTextFlags_CallbackCompletion; + event_key = ImGuiKey_Tab; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_UpArrow; + } + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow)) + { + event_flag = ImGuiInputTextFlags_CallbackHistory; + event_key = ImGuiKey_DownArrow; + } + else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited) + { + event_flag = ImGuiInputTextFlags_CallbackEdit; + } + else if (flags & ImGuiInputTextFlags_CallbackAlways) + { + event_flag = ImGuiInputTextFlags_CallbackAlways; + } + + if (event_flag) + { + ImGuiInputTextCallbackData callback_data; + callback_data.Ctx = &g; + callback_data.EventFlag = event_flag; + callback_data.Flags = flags; + callback_data.UserData = callback_user_data; + + // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925 + char* callback_buf = is_readonly ? buf : state->TextA.Data; + IM_ASSERT(callback_buf == state->TextSrc); + state->CallbackTextBackup.resize(state->TextLen + 1); + memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1); + + callback_data.EventKey = event_key; + callback_data.Buf = callback_buf; + callback_data.BufTextLen = state->TextLen; + callback_data.BufSize = state->BufCapacity; + callback_data.BufDirty = false; + + const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor; + const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start; + const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end; + + // Call user code + callback(&callback_data); + + // Read back what user may have modified + callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback + IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields + IM_ASSERT(callback_data.BufSize == state->BufCapacity); + IM_ASSERT(callback_data.Flags == flags); + const bool buf_dirty = callback_data.BufDirty; + if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; } + if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; } + if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; } + if (buf_dirty) + { + // Callback may update buffer and thus set buf_dirty even in read-only mode. + IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! + InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen); + state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() + state->CursorAnimReset(); + } + } + } + + // Will copy result string if modified + if (!is_readonly && strcmp(state->TextSrc, buf) != 0) + { + apply_new_text = state->TextSrc; + apply_new_text_length = state->TextLen; + value_changed = true; + } + } + } + + // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details) + if (g.InputTextDeactivatedState.ID == id) + { + if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) + { + apply_new_text = g.InputTextDeactivatedState.TextA.Data; + apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; + value_changed = true; + //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); + } + g.InputTextDeactivatedState.ID = 0; + } + + // Copy result to user buffer. This can currently only happen when (g.ActiveId == id) + if (apply_new_text != NULL) + { + //// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size + //// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used + //// without any storage on user's side. + IM_ASSERT(apply_new_text_length >= 0); + if (is_resizable) + { + ImGuiInputTextCallbackData callback_data; + callback_data.Ctx = &g; + callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; + callback_data.Flags = flags; + callback_data.Buf = buf; + callback_data.BufTextLen = apply_new_text_length; + callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); + callback_data.UserData = callback_user_data; + callback(&callback_data); + buf = callback_data.Buf; + buf_size = callback_data.BufSize; + apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); + IM_ASSERT(apply_new_text_length <= buf_size); + } + //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length); + + // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. + ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size)); + } + + // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) + // Otherwise request text input ahead for next frame. + if (g.ActiveId == id && clear_active_id) + ClearActiveID(); + else if (g.ActiveId == id) + g.WantTextInputNextFrame = 1; + + // Render frame + if (!is_multiline) + { + RenderNavCursor(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + } + + const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size + ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; + ImVec2 text_size(0.0f, 0.0f); + + // Set upper limit of single-line InputTextEx() at 2 million characters strings. The current pathological worst case is a long line + // without any carriage return, which would makes ImFont::RenderText() reserve too many vertices and probably crash. Avoid it altogether. + // Note that we only use this limit on single-line InputText(), so a pathologically large line on a InputTextMultiline() would still crash. + const int buf_display_max_length = 2 * 1024 * 1024; + const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595 + const char* buf_display_end = NULL; // We have specialized paths below for setting the length + + // Display hint when contents is empty + // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368) + const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + if (new_is_displaying_hint != is_displaying_hint) + { + if (is_password && !is_displaying_hint) + PopFont(); + is_displaying_hint = new_is_displaying_hint; + if (is_password && !is_displaying_hint) + PushPasswordFont(); + } + if (is_displaying_hint) + { + buf_display = hint; + buf_display_end = hint + ImStrlen(hint); + } + + // Render text. We currently only render selection when the widget is active or while scrolling. + // FIXME: We could remove the '&& render_cursor' to keep rendering selection when inactive. + if (render_cursor || render_selection) + { + IM_ASSERT(state != NULL); + if (!is_displaying_hint) + buf_display_end = buf_display + state->TextLen; + + // Render text (with cursor and selection) + // This is going to be messy. We need to: + // - Display the text (this alone can be more easily clipped) + // - Handle scrolling, highlight selection, display cursor (those all requires some form of 1d->2d cursor position calculation) + // - Measure text height (for scrollbar) + // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) + // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. + const char* text_begin = buf_display; + const char* text_end = text_begin + state->TextLen; + ImVec2 cursor_offset, select_start_offset; + + { + // Find lines numbers straddling cursor and selection min position + int cursor_line_no = render_cursor ? -1 : -1000; + int selmin_line_no = render_selection ? -1 : -1000; + const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL; + const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL; + + // Count lines and find line number for cursor and selection ends + int line_count = 1; + if (is_multiline) + { + for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++) + { + if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; } + if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; } + line_count++; + } + } + if (cursor_line_no == -1) + cursor_line_no = line_count; + if (selmin_line_no == -1) + selmin_line_no = line_count; + + // Calculate 2d position by finding the beginning of the line and measuring distance + cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x; + cursor_offset.y = cursor_line_no * g.FontSize; + if (selmin_line_no >= 0) + { + select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x; + select_start_offset.y = selmin_line_no * g.FontSize; + } + + // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) + if (is_multiline) + text_size = ImVec2(inner_size.x, line_count * g.FontSize); + } + + // Scroll + if (render_cursor && state->CursorFollow) + { + // Horizontal scroll in chunks of quarter width + if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) + { + const float scroll_increment_x = inner_size.x * 0.25f; + const float visible_width = inner_size.x - style.FramePadding.x; + if (cursor_offset.x < state->Scroll.x) + state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); + else if (cursor_offset.x - visible_width >= state->Scroll.x) + state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x); + } + else + { + state->Scroll.y = 0.0f; + } + + // Vertical scroll + if (is_multiline) + { + // Test if cursor is vertically visible + if (cursor_offset.y - g.FontSize < scroll_y) + scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); + else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) + scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; + const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); + scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); + draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag + draw_window->Scroll.y = scroll_y; + } + + state->CursorFollow = false; + } + + // Draw selection + const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f); + if (render_selection) + { + const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end); + const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end); + + ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. + float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. + float bg_offy_dn = is_multiline ? 0.0f : 2.0f; + ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; + for (const char* p = text_selected_begin; p < text_selected_end; ) + { + if (rect_pos.y > clip_rect.w + g.FontSize) + break; + if (rect_pos.y < clip_rect.y) + { + p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); + p = p ? p + 1 : text_selected_end; + } + else + { + ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true); + if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); + rect.ClipWith(clip_rect); + if (rect.Overlaps(clip_rect)) + draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + rect_pos.x = draw_pos.x - draw_scroll.x; + } + rect_pos.y += g.FontSize; + } + } + + // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. + // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it. + if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) + { + ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + } + + // Draw blinking cursor + if (render_cursor) + { + state->CursorAnim += io.DeltaTime; + bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; + ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); + ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); + if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) + draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); + + // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) + if (!is_readonly) + { + g.PlatformImeData.WantVisible = true; + g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); + g.PlatformImeData.InputLineHeight = g.FontSize; + } + } + } + else + { + // Render text only (no selection, no cursor) + if (is_multiline) + text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width + else if (!is_displaying_hint && g.ActiveId == id) + buf_display_end = buf_display + state->TextLen; + else if (!is_displaying_hint) + buf_display_end = buf_display + ImStrlen(buf_display); + + if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) + { + // Find render position for right alignment + if (flags & ImGuiInputTextFlags_ElideLeft) + draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); + + const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive? + ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + } + } + + if (is_password && !is_displaying_hint) + PopFont(); + + if (is_multiline) + { + // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)... + Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y)); + g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; + EndChild(); + item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow); + + // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active... + // FIXME: This quite messy/tricky, should attempt to get rid of the child window. + EndGroup(); + if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y)) + { + g.LastItemData.ID = id; + g.LastItemData.ItemFlags = item_data_backup.ItemFlags; + g.LastItemData.StatusFlags = item_data_backup.StatusFlags; + } + } + if (state) + state->TextSrc = NULL; + + // Log as text + if (g.LogEnabled && (!is_password || is_displaying_hint)) + { + LogSetNextTextDecoration("{", "}"); + LogRenderedText(&draw_pos, buf_display, buf_display_end); + } + + if (label_size.x > 0) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + if (value_changed) + MarkItemEdited(id); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); + if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) + return validated; + else + return value_changed; +} + +void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + ImStb::STB_TexteditState* stb_state = state->Stb; + ImStb::StbUndoState* undo_state = &stb_state->undostate; + Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId); + DebugLocateItemOnHover(state->ID); + Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end); + Text("BufCapacityA: %d", state->BufCapacity); + Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity); + Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); + Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point); + if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state + { + PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++) + { + ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n]; + const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; + if (undo_rec_type == ' ') + BeginDisabled(); + const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0; + const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage; + Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"", + undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf_preview_len, buf_preview_str); + if (undo_rec_type == ' ') + EndDisabled(); + } + PopStyleVar(); + } + EndChild(); +#else + IM_UNUSED(state); +#endif +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. +//------------------------------------------------------------------------- +// - ColorEdit3() +// - ColorEdit4() +// - ColorPicker3() +// - RenderColorRectWithAlphaCheckerboard() [Internal] +// - ColorPicker4() +// - ColorButton() +// - SetColorEditOptions() +// - ColorTooltip() [Internal] +// - ColorEditOptionsPopup() [Internal] +// - ColorPickerOptionsPopup() [Internal] +//------------------------------------------------------------------------- + +bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); +} + +static void ColorEditRestoreH(const float* col, float* H) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ColorEditCurrentID != 0); + if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + return; + *H = g.ColorEditSavedHue; +} + +// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation. +// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting. +static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ColorEditCurrentID != 0); + if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + return; + + // When S == 0, H is undefined. + // When H == 1 it wraps around to 0. + if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1)) + *H = g.ColorEditSavedHue; + + // When V == 0, S is undefined. + if (*V == 0.0f) + *S = g.ColorEditSavedSat; +} + +// Edit colors components (each component in 0.0f..1.0f range). +// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// With typical options: Left-click on color square to open color picker. Right-click to open option menu. CTRL+Click over input fields to edit them and TAB to go to next item. +bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float square_sz = GetFrameHeight(); + const char* label_display_end = FindRenderedTextEnd(label); + float w_full = CalcItemWidth(); + g.NextItemData.ClearFlags(); + + BeginGroup(); + PushID(label); + const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); + if (set_current_color_edit_id) + g.ColorEditCurrentID = window->IDStack.back(); + + // If we're not showing any slider there's no point in doing any HSV conversions + const ImGuiColorEditFlags flags_untouched = flags; + if (flags & ImGuiColorEditFlags_NoInputs) + flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions; + + // Context menu: display and modify options (before defaults are applied) + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorEditOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags_DisplayMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_); + if (!(flags & ImGuiColorEditFlags_DataTypeMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_); + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_); + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_); + flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_)); + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected + + const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; + const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; + const int components = alpha ? 4 : 3; + const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_inputs = ImMax(w_full - w_button, 1.0f); + w_full = w_inputs + w_button; + + // Convert to the formats we need + float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; + if ((flags & ImGuiColorEditFlags_InputHSV) && (flags & ImGuiColorEditFlags_DisplayRGB)) + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV)) + { + // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + ColorEditRestoreHS(col, &f[0], &f[1], &f[2]); + } + int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; + + bool value_changed = false; + bool value_changed_as_float = false; + + const ImVec2 pos = window->DC.CursorPos; + const float inputs_offset_x = (style.ColorButtonPosition == ImGuiDir_Left) ? w_button : 0.0f; + window->DC.CursorPos.x = pos.x + inputs_offset_x; + + if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB/HSV 0..255 Sliders + const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1); + + const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + static const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; + static const char* fmt_table_int[3][4] = + { + { "%3d", "%3d", "%3d", "%3d" }, // Short display + { "R:%3d", "G:%3d", "B:%3d", "A:%3d" }, // Long display for RGBA + { "H:%3d", "S:%3d", "V:%3d", "A:%3d" } // Long display for HSVA + }; + static const char* fmt_table_float[3][4] = + { + { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display + { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA + { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA + }; + const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1; + + float prev_split = 0.0f; + for (int n = 0; n < components; n++) + { + if (n > 0) + SameLine(0, style.ItemInnerSpacing.x); + float next_split = IM_TRUNC(w_items * (n + 1) / components); + SetNextItemWidth(ImMax(next_split - prev_split, 1.0f)); + prev_split = next_split; + + // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0. + if (flags & ImGuiColorEditFlags_Float) + { + value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); + value_changed_as_float |= value_changed; + } + else + { + value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + } + else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB Hexadecimal Input + char buf[64]; + if (alpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255)); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); + SetNextItemWidth(w_inputs); + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase)) + { + value_changed = true; + char* p = buf; + while (*p == '#' || ImCharIsBlankA(*p)) + p++; + i[0] = i[1] = i[2] = 0; + i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha) + int r; + if (alpha) + r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) + else + r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'. + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + + ImGuiWindow* picker_active_window = NULL; + if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) + { + const float button_offset_x = ((flags & ImGuiColorEditFlags_NoInputs) || (style.ColorButtonPosition == ImGuiDir_Left)) ? 0.0f : w_inputs + style.ItemInnerSpacing.x; + window->DC.CursorPos = ImVec2(pos.x + button_offset_x, pos.y); + + const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); + if (ColorButton("##ColorButton", col_v4, flags)) + { + if (!(flags & ImGuiColorEditFlags_NoPicker)) + { + // Store current color and open a picker + g.ColorPickerRef = col_v4; + OpenPopup("picker"); + SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y)); + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + + if (BeginPopup("picker")) + { + if (g.CurrentWindow->BeginCount == 1) + { + picker_active_window = g.CurrentWindow; + if (label != label_display_end) + { + TextEx(label, label_display_end); + Spacing(); + } + ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; + ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; + SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? + value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); + } + EndPopup(); + } + } + + if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) + { + // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left), + // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this. + SameLine(0.0f, style.ItemInnerSpacing.x); + window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x); + TextEx(label, label_display_end); + } + + // Convert back + if (value_changed && picker_active_window == NULL) + { + if (!value_changed_as_float) + for (int n = 0; n < 4; n++) + f[n] = i[n] / 255.0f; + if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB)) + { + g.ColorEditSavedHue = f[0]; + g.ColorEditSavedSat = f[1]; + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + g.ColorEditSavedID = g.ColorEditCurrentID; + g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0)); + } + if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV)) + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + + col[0] = f[0]; + col[1] = f[1]; + col[2] = f[2]; + if (alpha) + col[3] = f[3]; + } + + if (set_current_color_edit_id) + g.ColorEditCurrentID = 0; + PopID(); + EndGroup(); + + // Drag and Drop Target + // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) + { + bool accepted_drag_drop = false; + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086 + value_changed = accepted_drag_drop = true; + } + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * components); + value_changed = accepted_drag_drop = true; + } + + // Drag-drop payloads are always RGB + if (accepted_drag_drop && (flags & ImGuiColorEditFlags_InputHSV)) + ColorConvertRGBtoHSV(col[0], col[1], col[2], col[0], col[1], col[2]); + EndDragDropTarget(); + } + + // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). + if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) + g.LastItemData.ID = g.ActiveId; + + if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + float col4[4] = { col[0], col[1], col[2], 1.0f }; + if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) + return false; + col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; + return true; +} + +// Helper for ColorPicker4() +static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha) +{ + ImU32 alpha8 = IM_F32_TO_INT8_SAT(alpha); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32(0,0,0,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32(255,255,255,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32(0,0,0,alpha8)); + ImGui::RenderArrowPointingAt(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32(255,255,255,alpha8)); +} + +// Note: ColorPicker4() only accesses 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// (In C++ the 'float col[4]' notation for a function argument is equivalent to 'float* col', we only specify a size to facilitate understanding of the code.) +// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) +// FIXME: this is trying to be aware of style.Alpha but not fully correct. Also, the color wheel will have overlapping glitches with (style.Alpha < 1.0) +bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImDrawList* draw_list = window->DrawList; + ImGuiStyle& style = g.Style; + ImGuiIO& io = g.IO; + + const float width = CalcItemWidth(); + const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0; + g.NextItemData.ClearFlags(); + + PushID(label); + const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); + if (set_current_color_edit_id) + g.ColorEditCurrentID = window->IDStack.back(); + BeginGroup(); + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + flags |= ImGuiColorEditFlags_NoSmallPreview; + + // Context menu: display and store options. + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorPickerOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_; + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_NoOptions)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); + + // Setup + int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; + bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); + ImVec2 picker_pos = window->DC.CursorPos; + float square_sz = GetFrameHeight(); + float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars + float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box + float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; + float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; + float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f); + + float backup_initial_col[4]; + memcpy(backup_initial_col, col, components * sizeof(float)); + + float wheel_thickness = sv_picker_size * 0.08f; + float wheel_r_outer = sv_picker_size * 0.50f; + float wheel_r_inner = wheel_r_outer - wheel_thickness; + ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f); + + // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. + float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); + ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. + ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. + ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. + + float H = col[0], S = col[1], V = col[2]; + float R = col[0], G = col[1], B = col[2]; + if (flags & ImGuiColorEditFlags_InputRGB) + { + // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. + ColorConvertRGBtoHSV(R, G, B, H, S, V); + ColorEditRestoreHS(col, &H, &S, &V); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + ColorConvertHSVtoRGB(H, S, V, R, G, B); + } + + bool value_changed = false, value_changed_h = false, value_changed_sv = false; + + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Hue wheel + SV triangle logic + InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); + if (IsItemActive() && !is_readonly) + { + ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; + ImVec2 current_off = g.IO.MousePos - wheel_center; + float initial_dist2 = ImLengthSqr(initial_off); + if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1)) + { + // Interactive with Hue wheel + H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f; + if (H < 0.0f) + H += 1.0f; + value_changed = value_changed_h = true; + } + float cos_hue_angle = ImCos(-H * 2.0f * IM_PI); + float sin_hue_angle = ImSin(-H * 2.0f * IM_PI); + if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) + { + // Interacting with SV triangle + ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); + if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) + current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); + float uu, vv, ww; + ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); + V = ImClamp(1.0f - vv, 0.0001f, 1.0f); + S = ImClamp(uu / V, 0.0001f, 1.0f); + value_changed = value_changed_sv = true; + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // SV rectangle logic + InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); + if (IsItemActive() && !is_readonly) + { + S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); + V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square. + value_changed = value_changed_sv = true; + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + + // Hue bar logic + SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); + InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive() && !is_readonly) + { + H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + value_changed = value_changed_h = true; + } + } + + // Alpha bar logic + if (alpha_bar) + { + SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); + InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + value_changed = true; + } + } + PopItemFlag(); // ImGuiItemFlags_NoNav + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + SameLine(0, style.ItemInnerSpacing.x); + BeginGroup(); + } + + if (!(flags & ImGuiColorEditFlags_NoLabel)) + { + const char* label_display_end = FindRenderedTextEnd(label); + if (label != label_display_end) + { + if ((flags & ImGuiColorEditFlags_NoSidePreview)) + SameLine(0, style.ItemInnerSpacing.x); + TextEx(label, label_display_end); + } + } + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); + ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if ((flags & ImGuiColorEditFlags_NoLabel)) + Text("Current"); + + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip; + ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)); + if (ref_col != NULL) + { + Text("Original"); + ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); + if (ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2))) + { + memcpy(col, ref_col, components * sizeof(float)); + value_changed = true; + } + } + PopItemFlag(); + EndGroup(); + } + + // Convert back color to RGB + if (value_changed_h || value_changed_sv) + { + if (flags & ImGuiColorEditFlags_InputRGB) + { + ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]); + g.ColorEditSavedHue = H; + g.ColorEditSavedSat = S; + g.ColorEditSavedID = g.ColorEditCurrentID; + g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + col[0] = H; + col[1] = S; + col[2] = V; + } + } + + // R,G,B and H,S,V slider color editor + bool value_changed_fix_hue_wrap = false; + if ((flags & ImGuiColorEditFlags_NoInputs) == 0) + { + PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview; + ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; + if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB)) + { + // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. + // For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050) + value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap); + value_changed = true; + } + if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV); + if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) + value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex); + PopItemWidth(); + } + + // Try to cancel hue wrap (after ColorEdit4 call), if any + if (value_changed_fix_hue_wrap && (flags & ImGuiColorEditFlags_InputRGB)) + { + float new_H, new_S, new_V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); + if (new_H <= 0 && H > 0) + { + if (new_V <= 0 && V != new_V) + ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); + else if (new_S <= 0) + ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); + } + } + + if (value_changed) + { + if (flags & ImGuiColorEditFlags_InputRGB) + { + R = col[0]; + G = col[1]; + B = col[2]; + ColorConvertRGBtoHSV(R, G, B, H, S, V); + ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately. + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + H = col[0]; + S = col[1]; + V = col[2]; + ColorConvertHSVtoRGB(H, S, V, R, G, B); + } + } + + const int style_alpha8 = IM_F32_TO_INT8_SAT(style.Alpha); + const ImU32 col_black = IM_COL32(0,0,0,style_alpha8); + const ImU32 col_white = IM_COL32(255,255,255,style_alpha8); + const ImU32 col_midgrey = IM_COL32(128,128,128,style_alpha8); + const ImU32 col_hues[6 + 1] = { IM_COL32(255,0,0,style_alpha8), IM_COL32(255,255,0,style_alpha8), IM_COL32(0,255,0,style_alpha8), IM_COL32(0,255,255,style_alpha8), IM_COL32(0,0,255,style_alpha8), IM_COL32(255,0,255,style_alpha8), IM_COL32(255,0,0,style_alpha8) }; + + ImVec4 hue_color_f(1, 1, 1, style.Alpha); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); + ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); + ImU32 user_col32_striped_of_alpha = ColorConvertFloat4ToU32(ImVec4(R, G, B, style.Alpha)); // Important: this is still including the main rendering/style alpha!! + + ImVec2 sv_cursor_pos; + + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Render Hue Wheel + const float aeps = 0.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). + const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); + for (int n = 0; n < 6; n++) + { + const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; + const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; + const int vert_start_idx = draw_list->VtxBuffer.Size; + draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); + draw_list->PathStroke(col_white, 0, wheel_thickness); + const int vert_end_idx = draw_list->VtxBuffer.Size; + + // Paint colors over existing vertices + ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner); + ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner); + ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]); + } + + // Render Cursor + preview on Hue Wheel + float cos_hue_angle = ImCos(H * 2.0f * IM_PI); + float sin_hue_angle = ImSin(H * 2.0f * IM_PI); + ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f); + float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; + int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others. + draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments); + + // Render SV triangle (rotated according to hue) + ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); + ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); + ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); + ImVec2 uv_white = GetFontTexUvWhitePixel(); + draw_list->PrimReserve(3, 3); + draw_list->PrimVtx(tra, uv_white, hue_color32); + draw_list->PrimVtx(trb, uv_white, col_black); + draw_list->PrimVtx(trc, uv_white, col_white); + draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f); + sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // Render SV Square + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white); + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black); + RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f); + sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much + sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); + + // Render Hue Bar + for (int i = 0; i < 6; ++i) + draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]); + float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size); + RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); + } + + // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) + float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f; + int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others. + draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments); + + // Render alpha bar + if (alpha_bar) + { + float alpha = ImSaturate(col[3]); + ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); + RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); + draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK); + float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size); + RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); + } + + EndGroup(); + + if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0) + value_changed = false; + if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId + MarkItemEdited(g.LastItemData.ID); + + if (set_current_color_edit_id) + g.ColorEditCurrentID = 0; + PopID(); + + return value_changed; +} + +// A little color square. Return true when clicked. +// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. +// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. +// Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set. +bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(desc_id); + const float default_size = GetFrameHeight(); + const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque)) + flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf); + + ImVec4 col_rgb = col; + if (flags & ImGuiColorEditFlags_InputHSV) + ColorConvertHSVtoRGB(col_rgb.x, col_rgb.y, col_rgb.z, col_rgb.x, col_rgb.y, col_rgb.z); + + ImVec4 col_rgb_without_alpha(col_rgb.x, col_rgb.y, col_rgb.z, 1.0f); + float grid_step = ImMin(size.x, size.y) / 2.99f; + float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); + ImRect bb_inner = bb; + float off = 0.0f; + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. + bb_inner.Expand(off); + } + if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f) + { + float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f); + if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0) + RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight); + else + window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight); + window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft); + } + else + { + // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha + ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb; + if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0) + RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); + else + window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding); + } + RenderNavCursor(bb, id); + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + if (g.Style.FrameBorderSize > 0.0f) + RenderFrameBorder(bb.Min, bb.Max, rounding); + else + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border + } + + // Drag and Drop Source + // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test. + if (g.ActiveId == id && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropSource()) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col_rgb, sizeof(float) * 3, ImGuiCond_Once); + else + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col_rgb, sizeof(float) * 4, ImGuiCond_Once); + ColorButton(desc_id, col, flags); + SameLine(); + TextEx("Color"); + EndDragDropSource(); + } + + // Tooltip + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_)); + + return pressed; +} + +// Initialize/override default color options +void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_; + if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_; + if ((flags & ImGuiColorEditFlags_PickerMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_; + if ((flags & ImGuiColorEditFlags_InputMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected + g.ColorEditOptions = flags; +} + +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; + const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; + if (text_end > text) + { + TextEx(text, text_end); + Separator(); + } + + ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); + ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_; + ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz); + SameLine(); + if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_)) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); + else + Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); + } + else if (flags & ImGuiColorEditFlags_InputHSV) + { + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("H: %.3f, S: %.3f, V: %.3f", col[0], col[1], col[2]); + else + Text("H: %.3f, S: %.3f, V: %.3f, A: %.3f", col[0], col[1], col[2], col[3]); + } + EndTooltip(); +} + +void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) +{ + bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_); + bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_); + if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) + return; + + ImGuiContext& g = *GImGui; + PushItemFlag(ImGuiItemFlags_NoMarkEdited, true); + ImGuiColorEditFlags opts = g.ColorEditOptions; + if (allow_opt_inputs) + { + if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB; + if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV; + if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex; + } + if (allow_opt_datatype) + { + if (allow_opt_inputs) Separator(); + if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8; + if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float; + } + + if (allow_opt_inputs || allow_opt_datatype) + Separator(); + if (Button("Copy as..", ImVec2(-1, 0))) + OpenPopup("Copy"); + if (BeginPopup("Copy")) + { + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + char buf[64]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb); + if (Selectable(buf)) + SetClipboardText(buf); + if (!(flags & ImGuiColorEditFlags_NoAlpha)) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + } + EndPopup(); + } + + g.ColorEditOptions = opts; + PopItemFlag(); + EndPopup(); +} + +void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) +{ + bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_); + bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); + if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context")) + return; + + ImGuiContext& g = *GImGui; + PushItemFlag(ImGuiItemFlags_NoMarkEdited, true); + if (allow_opt_picker) + { + ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function + PushItemWidth(picker_size.x); + for (int picker_type = 0; picker_type < 2; picker_type++) + { + // Draw small/thumbnail version of each picker type (over an invisible button for selection) + if (picker_type > 0) Separator(); + PushID(picker_type); + ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha); + if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; + if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; + ImVec2 backup_pos = GetCursorScreenPos(); + if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup + g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_); + SetCursorScreenPos(backup_pos); + ImVec4 previewing_ref_col; + memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4)); + ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags); + PopID(); + } + PopItemWidth(); + } + if (allow_opt_alpha_bar) + { + if (allow_opt_picker) Separator(); + CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); + } + PopItemFlag(); + EndPopup(); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: TreeNode, CollapsingHeader, etc. +//------------------------------------------------------------------------- +// - TreeNode() +// - TreeNodeV() +// - TreeNodeEx() +// - TreeNodeExV() +// - TreeNodeBehavior() [Internal] +// - TreePush() +// - TreePop() +// - GetTreeNodeToLabelSpacing() +// - SetNextItemOpen() +// - CollapsingHeader() +//------------------------------------------------------------------------- + +bool ImGui::TreeNode(const char* str_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const void* ptr_id, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, 0, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNode(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL); +} + +bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) +{ + return TreeNodeExV(str_id, 0, fmt, args); +} + +bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) +{ + return TreeNodeExV(ptr_id, 0, fmt, args); +} + +bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, flags, label, NULL); +} + +bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(str_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + bool is_open = TreeNodeExV(ptr_id, flags, fmt, args); + va_end(args); + return is_open; +} + +bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiID id = window->GetID(str_id); + const char* label, *label_end; + ImFormatStringToTempBufferV(&label, &label_end, fmt, args); + return TreeNodeBehavior(id, flags, label, label_end); +} + +bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiID id = window->GetID(ptr_id); + const char* label, *label_end; + ImFormatStringToTempBufferV(&label, &label_end, fmt, args); + return TreeNodeBehavior(id, flags, label, label_end); +} + +bool ImGui::TreeNodeGetOpen(ImGuiID storage_id) +{ + ImGuiContext& g = *GImGui; + ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; + return storage->GetInt(storage_id, 0) != 0; +} + +void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open) +{ + ImGuiContext& g = *GImGui; + ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; + storage->SetInt(storage_id, open ? 1 : 0); +} + +bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags) +{ + if (flags & ImGuiTreeNodeFlags_Leaf) + return true; + + // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStorage* storage = window->DC.StateStorage; + + bool is_open; + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen) + { + if (g.NextItemData.OpenCond & ImGuiCond_Always) + { + is_open = g.NextItemData.OpenVal; + TreeNodeSetOpen(storage_id, is_open); + } + else + { + // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. + const int stored_value = storage->GetInt(storage_id, -1); + if (stored_value == -1) + { + is_open = g.NextItemData.OpenVal; + TreeNodeSetOpen(storage_id, is_open); + } + else + { + is_open = stored_value != 0; + } + } + } + else + { + is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; + } + + // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). + // NB- If we are above max depth we still allow manually opened nodes to be logged. + if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && (window->DC.TreeDepth - g.LogDepthRef) < g.LogDepthToExpand) + is_open = true; + + return is_open; +} + +// Store ImGuiTreeNodeStackData for just submitted node. +// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase. +static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1); + ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back(); + tree_node_data->ID = g.LastItemData.ID; + tree_node_data->TreeFlags = flags; + tree_node_data->ItemFlags = g.LastItemData.ItemFlags; + tree_node_data->NavRect = g.LastItemData.NavRect; + window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth); +} + +// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop. +bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); + + if (!label_end) + label_end = FindRenderedTextEnd(label); + const ImVec2 label_size = CalcTextSize(label, label_end, false); + + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow + + // We vertically grow up to current line height up the typical widget height. + const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); + const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL); + const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL); + ImRect frame_bb; + frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; + frame_bb.Min.y = window->DC.CursorPos.y; + frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanLabelWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x; + frame_bb.Max.y = window->DC.CursorPos.y + frame_height; + if (display_frame) + { + const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits + frame_bb.Min.x -= outer_extend; + frame_bb.Max.x += outer_extend; + } + + ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); + ItemSize(ImVec2(text_width, frame_height), padding.y); + + // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing + ImRect interact_bb = frame_bb; + if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) + interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f); + + // Compute open and multi-select states before ItemAdd() as it clear NextItem data. + ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id; + bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); + + bool is_visible; + if (span_all_columns || span_all_columns_label) + { + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + is_visible = ItemAdd(interact_bb, id); + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + else + { + is_visible = ItemAdd(interact_bb, id); + } + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + g.LastItemData.DisplayRect = frame_bb; + + // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled: + // Store data for the current depth to allow returning to this node from any child item. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle. + bool store_tree_node_stack_data = false; + if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + { + if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive) + if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + store_tree_node_stack_data = true; + } + + const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; + if (!is_visible) + { + if (store_tree_node_stack_data && is_open) + TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; + } + + if (span_all_columns || span_all_columns_label) + { + TablePushBackgroundChannel(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } + + ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; + if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) + button_flags |= ImGuiButtonFlags_AllowOverlap; + if (!is_leaf) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + + // We allow clicking on the arrow section with keyboard modifiers held, in order to easily + // allow browsing a tree while preserving selection with code implementing multi-selection patterns. + // When clicking on the rest of the tree node we always disallow keyboard modifiers. + const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; + const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; + const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); + + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default + flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow; + + // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. + // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. + // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) + // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) + // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0) + // It is rather standard that arrow click react on Down rather than Up. + // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. + if (is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + + bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; + const bool was_selected = selected; + + // Multi-selection support (header) + if (is_multi_select) + { + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); + if (is_mouse_x_over_arrow) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + } + else + { + if (window != g.HoveredWindow || !is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_NoKeyModsAllowed; + } + + bool hovered, held; + bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + bool toggled = false; + if (!is_leaf) + { + if (pressed && g.DragDropHoldJustPressedId != id) + { + if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select)) + toggled = true; // Single click + if (flags & ImGuiTreeNodeFlags_OpenOnArrow) + toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job + if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2) + toggled = true; // Double click + } + else if (pressed && g.DragDropHoldJustPressedId == id) + { + IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold); + if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = true; + else + pressed = false; // Cancel press so it doesn't trigger selection. + } + + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open) + { + toggled = true; + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavMoveRequestCancel(); + } + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + { + toggled = true; + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavMoveRequestCancel(); + } + + if (toggled) + { + is_open = !is_open; + window->DC.StateStorage->SetInt(storage_id, is_open); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; + } + } + + // Multi-selection support (footer) + if (is_multi_select) + { + bool pressed_copy = pressed && !toggled; + MultiSelectItemFooter(id, &selected, &pressed_copy); + if (pressed) + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb); + } + + if (selected != was_selected) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + { + const ImU32 text_col = GetColorU32(ImGuiCol_Text); + ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact; + if (is_multi_select) + nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle + if (display_frame) + { + // Framed type + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); + RenderNavCursor(frame_bb, id, nav_render_cursor_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f); + else // Leaf without bullet, left-adjusted text + text_pos.x -= text_offset_x - padding.x; + if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) + frame_bb.Max.x -= g.FontSize + style.FramePadding.x; + if (g.LogEnabled) + LogSetNextTextDecoration("###", "###"); + } + else + { + // Unframed typed for tree nodes + if (hovered || selected) + { + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); + } + RenderNavCursor(frame_bb, id, nav_render_cursor_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f); + if (g.LogEnabled) + LogSetNextTextDecoration(">", NULL); + } + + if (span_all_columns && !span_all_columns_label) + TablePopBackgroundChannel(); + + // Label + if (display_frame) + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + else + RenderText(text_pos, label, label_end, false); + + if (span_all_columns_label) + TablePopBackgroundChannel(); + } + + if (store_tree_node_stack_data && is_open) + TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + return is_open; +} + +void ImGui::TreePush(const char* str_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(str_id); +} + +void ImGui::TreePush(const void* ptr_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(ptr_id); +} + +void ImGui::TreePushOverrideID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + Indent(); + window->DC.TreeDepth++; + PushOverrideID(id); +} + +void ImGui::TreePop() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + Unindent(); + + window->DC.TreeDepth--; + ImU32 tree_depth_mask = (1 << window->DC.TreeDepth); + + if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request + { + ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back(); + IM_ASSERT(data->ID == window->IDStack.back()); + if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) + { + // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) + if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data); + } + g.TreeNodeStack.pop_back(); + window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask; + } + + IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much. + PopID(); +} + +// Horizontal distance preceding label when using TreeNode() or Bullet() +float ImGui::GetTreeNodeToLabelSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + (g.Style.FramePadding.x * 2.0f); +} + +// Set next TreeNode/CollapsingHeader open state. +void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond) +{ + ImGuiContext& g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen; + g.NextItemData.OpenVal = is_open; + g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always); +} + +// Set next TreeNode/CollapsingHeader storage id. +void ImGui::SetNextItemStorageID(ImGuiID storage_id) +{ + ImGuiContext& g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID; + g.NextItemData.StorageId = storage_id; +} + +// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). +// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). +bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label); +} + +// p_visible == NULL : regular collapsing header +// p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false +// p_visible != NULL && *p_visible == false : do not show the header at all +// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen. +bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + if (p_visible && !*p_visible) + return false; + + ImGuiID id = window->GetID(label); + flags |= ImGuiTreeNodeFlags_CollapsingHeader; + if (p_visible) + flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton; + bool is_open = TreeNodeBehavior(id, flags, label); + if (p_visible != NULL) + { + // Create a small overlapping close button + // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. + // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow. + ImGuiContext& g = *GImGui; + ImGuiLastItemData last_item_backup = g.LastItemData; + float button_size = g.FontSize; + float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size); + float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y; + ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id); + if (CloseButton(close_button_id, ImVec2(button_x, button_y))) + *p_visible = false; + g.LastItemData = last_item_backup; + } + + return is_open; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Selectable +//------------------------------------------------------------------------- +// - Selectable() +//------------------------------------------------------------------------- + +// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image. +// But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. +// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags. +// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. +bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. + ImGuiID id = window->GetID(label); + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(size, 0.0f); + + // Fill horizontal space + // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets. + const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0; + const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x; + const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; + if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth)) + size.x = ImMax(label_size.x, max_x - min_x); + + // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos. + ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y); + if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) + { + const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; + const float spacing_y = style.ItemSpacing.y; + const float spacing_L = IM_TRUNC(spacing_x * 0.50f); + const float spacing_U = IM_TRUNC(spacing_y * 0.50f); + bb.Min.x -= spacing_L; + bb.Min.y -= spacing_U; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); + } + //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } + + const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; + const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None; + bool is_visible; + if (span_all_columns) + { + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + is_visible = ItemAdd(bb, id, NULL, extra_item_flags); + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + else + { + is_visible = ItemAdd(bb, id, NULL, extra_item_flags); + } + + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (!is_visible) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd) + return false; + + const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (disabled_item && !disabled_global) // Only testing this as an optimization + BeginDisabled(); + + // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, + // which would be advantageous since most selectable are not selected. + if (span_all_columns) + { + if (g.CurrentTable) + TablePushBackgroundChannel(); + else if (window->DC.CurrentColumns) + PushColumnsBackground(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } + + // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries + ImGuiButtonFlags button_flags = 0; + if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; } + if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; } + if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; } + if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; } + if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } + if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } + + // Multi-selection support (header) + const bool was_selected = selected; + if (is_multi_select) + { + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); + } + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + + // Multi-selection support (footer) + if (is_multi_select) + { + MultiSelectItemFooter(id, &selected, &pressed); + } + else + { + // Auto-select when moved into + // - This will be more fully fleshed in the range-select branch + // - This is not exposed as it won't nicely work with some user side handling of shift/control + // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons + // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) + // - (2) usage will fail with clipped items + // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. + if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) + if (g.NavJustMovedToId == id) + selected = pressed = true; + } + + // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad + if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) + { + if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) + { + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect) + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + } + } + if (pressed) + MarkItemEdited(id); + + if (selected != was_selected) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + + // Render + if (is_visible) + { + const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight); + if (highlighted || selected) + { + // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106) + ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + if (g.NavId == id) + { + ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding; + if (is_multi_select) + nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavCursor(bb, id, nav_render_cursor_flags); + } + } + + if (span_all_columns) + { + if (g.CurrentTable) + TablePopBackgroundChannel(); + else if (window->DC.CurrentColumns) + PopColumnsBackground(); + } + + // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns. + if (is_visible) + RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb); + + // Automatically close popups + if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups)) + CloseCurrentPopup(); + + if (disabled_item && !disabled_global) + EndDisabled(); + + // Selectable() always returns a pressed state! + // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve + // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect(). + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; //-V1020 +} + +bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) +{ + if (Selectable(label, *p_selected, flags, size_arg)) + { + *p_selected = !*p_selected; + return true; + } + return false; +} + + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Typing-Select support +//------------------------------------------------------------------------- + +// [Experimental] Currently not exposed in public API. +// Consume character inputs and return search request, if any. +// This would typically only be called on the focused window or location you want to grab inputs for, e.g. +// if (ImGui::IsWindowFocused(...)) +// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest()) +// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1); +// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer). +ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiTypingSelectState* data = &g.TypingSelectState; + ImGuiTypingSelectRequest* out_request = &data->Request; + + // Clear buffer + const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config. + const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times + if (data->SearchBuffer[0] != 0) + { + bool clear_buffer = false; + clear_buffer |= (g.NavFocusScopeId != data->FocusScope); + clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time); + clear_buffer |= g.NavAnyRequest; + clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere + clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter); + clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0; + //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); } + if (clear_buffer) + data->Clear(); + } + + // Append to buffer + const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1; + int buffer_len = (int)ImStrlen(data->SearchBuffer); + bool select_request = false; + for (ImWchar w : g.IO.InputQueueCharacters) + { + const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1); + if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks + continue; + char w_buf[5]; + ImTextCharToUtf8(w_buf, (unsigned int)w); + if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0) + { + select_request = true; // Same character: don't need to append to buffer. + continue; + } + if (data->SingleCharModeLock) + { + data->Clear(); // Different character: clear + buffer_len = 0; + } + memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append + buffer_len += w_len; + select_request = true; + } + g.IO.InputQueueCharacters.resize(0); + + // Handle backspace + if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat)) + { + char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len); + *p = 0; + buffer_len = (int)(p - data->SearchBuffer); + } + + // Return request if any + if (buffer_len == 0) + return NULL; + if (select_request) + { + data->FocusScope = g.NavFocusScopeId; + data->LastRequestFrame = g.FrameCount; + data->LastRequestTime = (float)g.Time; + } + out_request->Flags = flags; + out_request->SearchBufferLen = buffer_len; + out_request->SearchBuffer = data->SearchBuffer; + out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount); + out_request->SingleCharMode = false; + out_request->SingleCharSize = 0; + + // Calculate if buffer contains the same character repeated. + // - This can be used to implement a special search mode on first character. + // - Performed on UTF-8 codepoint for correctness. + // - SingleCharMode is always set for first input character, because it usually leads to a "next". + if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode) + { + const char* buf_begin = out_request->SearchBuffer; + const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen; + const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end); + const char* p = buf_begin + c0_len; + for (; p < buf_end; p += c0_len) + if (memcmp(buf_begin, p, (size_t)c0_len) != 0) + break; + const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0; + out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock); + out_request->SingleCharSize = (ImS8)c0_len; + data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode. + } + + return out_request; +} + +static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2) +{ + int match_len = 0; + while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++)) + match_len++; + return match_len; +} + +// Default handler for finding a result for typing-select. You may implement your own. +// You might want to display a tooltip to visualize the current request SearchBuffer +// When SingleCharMode is set: +// - it is better to NOT display a tooltip of other on-screen display indicator. +// - the index of the currently focused item is required. +// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData. +int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot. + return -1; + int idx = -1; + if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode)) + idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx); + else + idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data); + if (idx != -1) + SetNavCursorVisibleAfterMove(); + return idx; +} + +// Special handling when a single character is repeated: perform search on a single letter and goes to next. +int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + // FIXME: Assume selection user data is index. Would be extremely practical. + //if (nav_item_idx == -1) + // nav_item_idx = (int)g.NavLastValidSelectionUserData; + + int first_match_idx = -1; + bool return_next_match = false; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize) + continue; + if (return_next_match) // Return next matching item after current item. + return idx; + if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value. + return idx; + if (first_match_idx == -1) // Record first match for wrapping. + first_match_idx = idx; + if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match. + return_next_match = true; + } + return first_match_idx; // First result +} + +int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data) +{ + int longest_match_idx = -1; + int longest_match_len = 0; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name); + if (match_len <= longest_match_len) + continue; + longest_match_idx = idx; + longest_match_len = match_len; + if (match_len == req->SearchBufferLen) + break; + } + return longest_match_idx; +} + +void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + Text("SearchBuffer = \"%s\"", data->SearchBuffer); + Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock); + Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame); +#else + IM_UNUSED(data); +#endif +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Box-Select support +// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet. +//------------------------------------------------------------------------- +// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step() +//------------------------------------------------------------------------- +// - BoxSelectPreStartDrag() [Internal] +// - BoxSelectActivateDrag() [Internal] +// - BoxSelectDeactivateDrag() [Internal] +// - BoxSelectScrollWithMouseDrag() [Internal] +// - BeginBoxSelect() [Internal] +// - EndBoxSelect() [Internal] +//------------------------------------------------------------------------- + +// Call on the initial click. +static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) +{ + ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + bs->ID = id; + bs->IsStarting = true; // Consider starting box-select. + bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); + bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid; + bs->KeyMods = g.IO.KeyMods; + bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); + bs->ScrollAccum = ImVec2(0.0f, 0.0f); +} + +static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID); + bs->IsActive = true; + bs->Window = window; + bs->IsStarting = false; + ImGui::SetActiveID(bs->ID, window); + ImGui::SetActiveIdUsingAllKeyboardKeys(); + if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + bs->RequestClear = true; +} + +static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs) +{ + ImGuiContext& g = *GImGui; + bs->IsActive = bs->IsStarting = false; + if (g.ActiveId == bs->ID) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID); + ImGui::ClearActiveID(); + } + bs->ID = 0; +} + +static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(bs->Window == window); + for (int n = 0; n < 2; n++) // each axis + { + const float mouse_pos = g.IO.MousePos[n]; + const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f; + const float scroll_curr = window->Scroll[n]; + if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n])) + continue; + + const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime; + bs->ScrollAccum[n] += scroll_step; + + // Accumulate into a stored value so we can handle high-framerate + const float scroll_step_i = ImFloor(bs->ScrollAccum[n]); + if (scroll_step_i == 0.0f) + continue; + if (n == 0) + ImGui::SetScrollX(window, scroll_curr + scroll_step_i); + else + ImGui::SetScrollY(window, scroll_curr + scroll_step_i); + bs->ScrollAccum[n] -= scroll_step_i; + } +} + +bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + KeepAliveID(box_select_id); + if (bs->ID != box_select_id) + return false; + + // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. + bs->UnclipMode = false; + bs->RequestClear = false; + if (bs->IsStarting && IsMouseDragPastThreshold(0)) + BoxSelectActivateDrag(bs, window); + else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) + BoxSelectDeactivateDrag(bs); + if (!bs->IsActive) + return false; + + // Current frame absolute prev/current rectangles are used to toggle selection. + // They are derived from positions relative to scrolling space. + ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already + ImVec2 curr_end_pos_abs = g.IO.MousePos; + if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow + curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); + bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + + // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) + // Storing an extra rect used by widgets supporting box-select. + if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) + { + bs->UnclipMode = true; + bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis. + bs->UnclipRect.Add(bs->BoxSelectRectCurr); + } + + //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); + return true; +} + +void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + IM_ASSERT(bs->IsActive); + bs->UnclipMode = false; + + // Render selection rectangle + bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view + ImRect box_select_r = bs->BoxSelectRectCurr; + box_select_r.ClipWith(scope_rect); + window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling + + // Scroll + const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; + if (enable_scroll) + { + ImRect scroll_r = scope_rect; + scroll_r.Expand(-g.FontSize); + //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); + if (!scroll_r.Contains(g.IO.MousePos)) + BoxSelectScrollWithMouseDrag(bs, window, scroll_r); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select support +//------------------------------------------------------------------------- +// - DebugLogMultiSelectRequests() [Internal] +// - CalcScopeRect() [Internal] +// - BeginMultiSelect() +// - EndMultiSelect() +// - SetNextItemSelectionUserData() +// - MultiSelectItemHeader() [Internal] +// - MultiSelectItemFooter() [Internal] +// - DebugNodeMultiSelectState() [Internal] +//------------------------------------------------------------------------- + +static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(function); + for (const ImGuiSelectionRequest& req : io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection); + } +} + +static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + { + // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only + return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); + } + else + { + // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) + ImRect scope_rect = window->InnerClipRect; + if (g.CurrentTable != NULL) + scope_rect = g.CurrentTable->HostClipRect; + + // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? + scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); + return scope_rect; + } +} + +// Return ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). +// Passing 'selection_size' and 'items_count' parameters is currently optional. +// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0, +// allowing a first press to clear selection THEN the second press to leave child window and return to parent. +// - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently). +// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1. +// - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. +ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size) + g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData()); + ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1]; + IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. + g.CurrentMultiSelect = ms; + if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) + flags |= ImGuiMultiSelectFlags_ScopeWindow; + if (flags & ImGuiMultiSelectFlags_SingleSelect) + flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d); + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags &= ~ImGuiMultiSelectFlags_BoxSelect1d; + + // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250) + // They should perhaps be stacked properly? + if (ImGuiTable* table = g.CurrentTable) + if (table->CurrentColumn != -1) + TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it. + + // FIXME: BeginFocusScope() + const ImGuiID id = window->IDStack.back(); + ms->Clear(); + ms->FocusScopeId = id; + ms->Flags = flags; + ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); + ms->BackupCursorMaxPos = window->DC.CursorMaxPos; + ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; + PushFocusScope(ms->FocusScopeId); + if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. + window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; + + // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. + ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods; + if (flags & ImGuiMultiSelectFlags_NoRangeSelect) + ms->KeyMods &= ~ImGuiMod_Shift; + + // Bind storage + ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); + storage->ID = id; + storage->LastFrameActive = g.FrameCount; + storage->LastSelectionSize = selection_size; + storage->Window = window; + ms->Storage = storage; + + // Output to user + ms->IO.Requests.resize(0); + ms->IO.RangeSrcItem = storage->RangeSrcItem; + ms->IO.NavIdItem = storage->NavIdItem; + ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + ms->IO.ItemsCount = items_count; + + // Clear when using Navigation to move within the scope + // (we compare FocusScopeId so it possible to use multiple selections inside a same window) + bool request_clear = false; + bool request_select_all = false; + if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) + { + if (ms->KeyMods & ImGuiMod_Shift) + ms->IsKeyboardSetRange = true; + if (ms->IsKeyboardSetRange) + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) + request_clear = true; + } + else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) + { + // Also clear on leaving scope (may be optional?) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) + request_clear = true; + } + + // Box-select handling: update active state. + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + { + ms->BoxSelectId = GetID("##BoxSelect"); + if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags)) + request_clear |= bs->RequestClear; + } + + if (ms->IsFocused) + { + // Shortcut: Clear selection (Escape) + // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. + // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock. + if (flags & ImGuiMultiSelectFlags_ClearOnEscape) + { + if (selection_size != 0 || bs->IsActive) + if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0)) + { + request_clear = true; + if (bs->IsActive) + BoxSelectDeactivateDrag(bs); + } + } + + // Shortcut: Select all (CTRL+A) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) + request_select_all = true; + } + + if (request_clear || request_select_all) + { + MultiSelectAddSetAll(ms, request_select_all); + if (!request_select_all) + storage->LastSelectionSize = 0; + } + ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; + ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid; + + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); + + return &ms->IO; +} + +// Return updated ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). +ImGuiMultiSelectIO* ImGui::EndMultiSelect() +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + ImGuiMultiSelectState* storage = ms->Storage; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!"); + IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); + IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect); + + ImRect scope_rect = CalcScopeRect(ms, window); + if (ms->IsFocused) + { + // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. + if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. + storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; + } + if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); + storage->NavIdItem = ImGuiSelectionUserData_Invalid; + storage->NavIdSelected = -1; + } + + if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId)) + EndBoxSelect(scope_rect, ms->Flags); + } + + if (ms->IsEndIO == false) + ms->IO.Requests.resize(0); + + // Clear selection when clicking void? + // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! + // The InnerRect test is necessary for non-child/decorated windows. + bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); + if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) + scope_hovered &= scope_rect.Contains(g.IO.MousePos); + if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) + { + if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + { + if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) + { + BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + SetHoveredID(ms->BoxSelectId); + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select. + } + } + + if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) + if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) + MultiSelectAddSetAll(ms, false); + } + + // Courtesy nav wrapping helper flag + if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX) + { + IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope + ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + } + + // Unwind + window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); + PopFocusScope(); + + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO); + + ms->FocusScopeId = 0; + ms->Flags = ImGuiMultiSelectFlags_None; + g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL; + + return &ms->IO; +} + +void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) +{ + // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code! + // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api. + ImGuiContext& g = *GImGui; + g.NextItemData.SelectionUserData = selection_user_data; + g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; + + if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) + { + // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect; + if (ms->IO.RangeSrcItem == selection_user_data) + ms->RangeSrcPassedBy = true; + } + else + { + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; + } +} + +// In charge of: +// - Applying SetAll for submitted items. +// - Applying SetRange for submitted items and record end points. +// - Altering button behavior flags to facilitate use with drag and drop. +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + + bool selected = *p_selected; + if (ms->IsFocused) + { + ImGuiMultiSelectState* storage = ms->Storage; + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; + IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + + // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect(). + // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. + // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect() + if (ms->LoopRequestSetAll != -1) + selected = (ms->LoopRequestSetAll == 1); + + // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) + // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) + if (ms->IsKeyboardSetRange) + { + IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); + const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) + ms->RangeDstPassedBy = true; + if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected ? 1 : 0; + } + const bool is_range_src = storage->RangeSrcItem == item_data; + if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) + { + // Apply range-select value to visible items + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); + selected = (storage->RangeSelected != 0); + } + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + // Clear other items + selected = false; + } + } + *p_selected = selected; + } + + // Alter button behavior flags + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. + if (p_button_flags != NULL) + { + ImGuiButtonFlags button_flags = *p_button_flags; + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + *p_button_flags = button_flags; + } +} + +// In charge of: +// - Auto-select on navigation. +// - Box-select toggle handling. +// - Right-click handling. +// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse. +// - Record current selection state for RangeSrc +// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite. +void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + bool selected = *p_selected; + bool pressed = *p_pressed; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + ImGuiMultiSelectState* storage = ms->Storage; + if (pressed) + ms->IsFocused = true; + + bool hovered = false; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) + hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (!ms->IsFocused && !hovered) + return; + + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; + + ImGuiMultiSelectFlags flags = ms->Flags; + const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0; + bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; + bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; + + bool apply_to_range_src = false; + + if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) + apply_to_range_src = true; + if (ms->IsEndIO == false) + { + ms->IO.Requests.resize(0); + ms->IsEndIO = true; + } + + // Auto-select as you navigate a list + if (g.NavJustMovedToId == id) + { + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (is_ctrl && is_shift) + pressed = true; + else if (!is_ctrl) + selected = pressed = true; + } + else + { + // With NoAutoSelect, using Shift+keyboard performs a write/copy + if (is_shift) + pressed = true; + else if (!is_ctrl) + apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this + } + } + + if (apply_to_range_src) + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected; // Will be updated at the end of this function anyway. + } + + // Box-select toggle handling + if (ms->BoxSelectId != 0) + if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) + { + const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) + { + if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce) + { + pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway) + bs->IsStartedSetNavIdOnce = false; + } + else + { + selected = !selected; + MultiSelectAddSetRange(ms, selected, +1, item_data, item_data); + } + storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1); + } + } + + // Right-click handling. + // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 + if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (g.ActiveId != 0 && g.ActiveId != id) + ClearActiveID(); + SetFocusID(id, window); + if (!pressed && !selected) + { + pressed = true; + is_ctrl = is_shift = false; + } + } + + // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected. + // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something + // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional? + const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput); + + // Alter selection + if (pressed && (!enter_pressed || !selected)) + { + // Box-select + ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) + BoxSelectPreStartDrag(ms->BoxSelectId, item_data); + + //---------------------------------------------------------------------------------------- + // ACTION | Begin | Pressed/Activated | End + //---------------------------------------------------------------------------------------- + // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1 + // Keys Navigated: Ctrl | n/a | n/a + // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst + // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1 + // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1 + // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1 + //---------------------------------------------------------------------------------------- + // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1 + // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst + //---------------------------------------------------------------------------------------- + + if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + bool request_clear = false; + if (is_singleselect) + request_clear = true; + else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) + request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true; + else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + MultiSelectAddSetAll(ms, false); + } + + int range_direction; + bool range_selected; + if (is_shift && !is_singleselect) + { + //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); + if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) + storage->RangeSrcItem = item_data; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + // Shift+Arrow always select + // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) + range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + } + else + { + // Shift+Arrow copy source selection state + // Shift+Click always copy from target selection state + if (ms->IsKeyboardSetRange) + range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + else + range_selected = !selected; + } + range_direction = ms->RangeSrcPassedBy ? +1 : -1; + } + else + { + // Ctrl inverts selection, otherwise always select + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + selected = is_ctrl ? !selected : true; + else + selected = !selected; + storage->RangeSrcItem = item_data; + range_selected = selected; + range_direction = +1; + } + MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data); + } + + // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) + if (storage->RangeSrcItem == item_data) + storage->RangeSelected = selected ? 1 : 0; + + // Update/store the selection state of focused item + if (g.NavId == id) + { + storage->NavIdItem = item_data; + storage->NavIdSelected = selected ? 1 : 0; + } + if (storage->NavIdItem == item_data) + ms->NavIdPassedBy = true; + ms->LastSubmittedItem = item_data; + + *p_selected = selected; + *p_pressed = pressed; +} + +void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected) +{ + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ms->IO.Requests.resize(0); // Can always clear previous requests + ms->IO.Requests.push_back(req); // Add new request +} + +void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item) +{ + // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges) + if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0) + { + ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1]; + if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected) + { + prev->RangeLastItem = last_item; + return; + } + } + + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item }; + ms->IO.Requests.push_back(req); // Add new request +} + +void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (!open) + return; + Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); + Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); + Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user + TreePop(); +#else + IM_UNUSED(storage); +#endif +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select helpers +//------------------------------------------------------------------------- +// - ImGuiSelectionBasicStorage +// - ImGuiSelectionExternalStorage +//------------------------------------------------------------------------- + +ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage() +{ + Size = 0; + PreserveOrder = false; + UserData = NULL; + AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; + _SelectionOrder = 1; // Always >0 +} + +void ImGuiSelectionBasicStorage::Clear() +{ + Size = 0; + _SelectionOrder = 1; // Always >0 + _Storage.Data.resize(0); +} + +void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r) +{ + ImSwap(Size, r.Size); + ImSwap(_SelectionOrder, r._SelectionOrder); + _Storage.Data.swap(r._Storage.Data); +} + +bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const +{ + return _Storage.GetInt(id, 0) != 0; +} + +static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs) +{ + int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i; + int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); +} + +// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user. +// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector instead of ImGuiStorage...) +bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id) +{ + ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; + ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; + if (PreserveOrder && it == NULL && it_end != NULL) + ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt() + if (it == NULL) + it = _Storage.Data.Data; + IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); + if (it != it_end) + while (it->val_i == 0 && it < it_end) + it++; + const bool has_more = (it != it_end); + *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); + *out_id = has_more ? it->key : 0; + if (PreserveOrder && !has_more) + _Storage.BuildSortByKey(); + return has_more; +} + +void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) +{ + int* p_int = _Storage.GetIntRef(id, 0); + if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; } + else if (!selected && *p_int != 0) { *p_int = 0; Size--; } +} + +// Optimized for batch edits (with same value of 'selected') +static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order) +{ + ImGuiStorage* storage = &selection->_Storage; + ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); + const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); + if (selected == (is_contained && it->val_i != 0)) + return; + if (selected && !is_contained) + storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() + else if (is_contained) + it->val_i = selected ? selection_order : 0; // Modify in-place. + selection->Size += selected ? +1 : -1; +} + +static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends) +{ + ImGuiStorage* storage = &selection->_Storage; + if (selected && selection->Size != size_before_amends) + storage->BuildSortByKey(); // When done selecting: sort everything +} + +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. +// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. +// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. +// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform +// a lookup in order to have some way to iterate/interpolate between two items. +// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices +// and constructing a view index <> object id/ptr data structure anyway. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC. +// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. +// The most simple implementation (using indices everywhere) would look like: +// for (ImGuiSelectionRequest& req : ms_io->Requests) +// { +// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } } +// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } } +// } +void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) +{ + // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional. + // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect(). + // Other scheme may handle SetAll differently. + IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); + IM_ASSERT(AdapterIndexToStorageId != NULL); + + // This is optimized/specialized to cope with very large selections (e.g. 100k+ items) + // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish(). + // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. + // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code. + // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling + // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.) + // FIXME-OPT: For each block of consecutive SetRange request: + // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage. + // - rewrite sorted storage a single time. + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + { + Clear(); + if (req.Selected) + { + _Storage.Data.reserve(ms_io->ItemsCount); + const int size_before_amends = _Storage.Data.Size; + for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder); + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } + } + else if (req.Type == ImGuiSelectionRequestType_SetRange) + { + const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; + //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected); + if (selection_changes == 1 || (selection_changes < Size / 100)) + { + // Multiple sorted insertion + copy likely to be faster. + // Technically we could do a single copy with a little more work (sort sequential SetRange requests) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); + } + else + { + // Append insertion + single sort likely be faster. + // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 + const int size_before_amends = _Storage.Data.Size; + int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0); + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); + if (req.Selected) + _SelectionOrder += selection_changes; + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } + } + } +} + +//------------------------------------------------------------------------- + +ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage() +{ + UserData = NULL; + AdapterSetItemSelected = NULL; +} + +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage +// This makes no assumption about underlying storage. +void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) +{ + IM_ASSERT(AdapterSetItemSelected); + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + for (int idx = 0; idx < ms_io->ItemsCount; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: ListBox +//------------------------------------------------------------------------- +// - BeginListBox() +// - EndListBox() +// - ListBox() +//------------------------------------------------------------------------- + +// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. +// This handle some subtleties with capturing info from the label. +// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle. +// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" +// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.5f * item_height). +bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + // Size default to hold ~7.25 items. + // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. + ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); + ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); + ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + g.NextItemData.ClearFlags(); + + if (!IsRectVisible(bb.Min, bb.Max)) + { + ItemSize(bb.GetSize(), style.FramePadding.y); + ItemAdd(bb, 0, &frame_bb); + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well. + BeginGroup(); + if (label_size.x > 0.0f) + { + ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); + RenderText(label_pos, label); + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); + AlignTextToFramePadding(); + } + + BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle); + return true; +} + +void ImGui::EndListBox() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?"); + IM_UNUSED(window); + + EndChild(); + EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label +} + +bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) +{ + const bool value_changed = ListBox(label, current_item, Items_ArrayGetter, (void*)items, items_count, height_items); + return value_changed; +} + +// This is merely a helper around BeginListBox(), EndListBox(). +// Considering using those directly to submit custom data or store selection differently. +bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items) +{ + ImGuiContext& g = *GImGui; + + // Calculate size from "height_in_items" + if (height_in_items < 0) + height_in_items = ImMin(items_count, 7); + float height_in_items_f = height_in_items + 0.25f; + ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); + + if (!BeginListBox(label, size)) + return false; + + // Assume all items have even height (= 1 line of text). If you need items of different height, + // you can create a custom version of ListBox() in your code without using the clipper. + bool value_changed = false; + ImGuiListClipper clipper; + clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + clipper.IncludeItemByIndex(*current_item); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + const char* item_text = getter(user_data, i); + if (item_text == NULL) + item_text = "*Unknown item*"; + + PushID(i); + const bool item_selected = (i == *current_item); + if (Selectable(item_text, item_selected)) + { + *current_item = i; + value_changed = true; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); + } + EndListBox(); + + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + + return value_changed; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: PlotLines, PlotHistogram +//------------------------------------------------------------------------- +// - PlotEx() [Internal] +// - PlotLines() +// - PlotHistogram() +//------------------------------------------------------------------------- +// Plot/Graph widgets are not very good. +// Consider writing your own, or using a third-party one, see: +// - ImPlot https://github.com/epezent/implot +// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions +//------------------------------------------------------------------------- + +int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return -1; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav)) + return -1; + bool hovered; + ButtonBehavior(frame_bb, id, &hovered, NULL); + + // Determine scale from values if not specified + if (scale_min == FLT_MAX || scale_max == FLT_MAX) + { + float v_min = FLT_MAX; + float v_max = -FLT_MAX; + for (int i = 0; i < values_count; i++) + { + const float v = values_getter(data, i); + if (v != v) // Ignore NaN values + continue; + v_min = ImMin(v_min, v); + v_max = ImMax(v_max, v); + } + if (scale_min == FLT_MAX) + scale_min = v_min; + if (scale_max == FLT_MAX) + scale_max = v_max; + } + + RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + + const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1; + int idx_hovered = -1; + if (values_count >= values_count_min) + { + int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); + + // Tooltip on hover + if (hovered && inner_bb.Contains(g.IO.MousePos)) + { + const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); + const int v_idx = (int)(t * item_count); + IM_ASSERT(v_idx >= 0 && v_idx < values_count); + + const float v0 = values_getter(data, (v_idx + values_offset) % values_count); + const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); + if (plot_type == ImGuiPlotType_Lines) + SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1); + else if (plot_type == ImGuiPlotType_Histogram) + SetTooltip("%d: %8.4g", v_idx, v0); + idx_hovered = v_idx; + } + + const float t_step = 1.0f / (float)res_w; + const float inv_scale = (scale_min == scale_max) ? 0.0f : (1.0f / (scale_max - scale_min)); + + float v0 = values_getter(data, (0 + values_offset) % values_count); + float t0 = 0.0f; + ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + + const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); + const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); + + for (int n = 0; n < res_w; n++) + { + const float t1 = t0 + t_step; + const int v1_idx = (int)(t0 * item_count + 0.5f); + IM_ASSERT(v1_idx >= 0 && v1_idx < values_count); + const float v1 = values_getter(data, (v1_idx + values_offset + 1) % values_count); + const ImVec2 tp1 = ImVec2( t1, 1.0f - ImSaturate((v1 - scale_min) * inv_scale) ); + + // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. + ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); + ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); + if (plot_type == ImGuiPlotType_Lines) + { + window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + } + else if (plot_type == ImGuiPlotType_Histogram) + { + if (pos1.x >= pos0.x + 2.0f) + pos1.x -= 1.0f; + window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); + } + + t0 = t1; + tp0 = tp1; + } + } + + // Text overlay + if (overlay_text) + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); + + // Return hovered index or -1 if none are hovered. + // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). + return idx_hovered; +} + +struct ImGuiPlotArrayGetterData +{ + const float* Values; + int Stride; + + ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } +}; + +static float Plot_ArrayGetter(void* data, int idx) +{ + ImGuiPlotArrayGetterData* plot_data = (ImGuiPlotArrayGetterData*)data; + const float v = *(const float*)(const void*)((const unsigned char*)plot_data->Values + (size_t)idx * plot_data->Stride); + return v; +} + +void ImGui::PlotLines(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, const float* values, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size, int stride) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +void ImGui::PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 graph_size) +{ + PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Value helpers +// Those is not very useful, legacy API. +//------------------------------------------------------------------------- +// - Value() +//------------------------------------------------------------------------- + +void ImGui::Value(const char* prefix, bool b) +{ + Text("%s: %s", prefix, (b ? "true" : "false")); +} + +void ImGui::Value(const char* prefix, int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, unsigned int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, float v, const char* float_format) +{ + if (float_format) + { + char fmt[64]; + ImFormatString(fmt, IM_ARRAYSIZE(fmt), "%%s: %s", float_format); + Text(fmt, prefix, v); + } + else + { + Text("%s: %.3f", prefix, v); + } +} + +//------------------------------------------------------------------------- +// [SECTION] MenuItem, BeginMenu, EndMenu, etc. +//------------------------------------------------------------------------- +// - ImGuiMenuColumns [Internal] +// - BeginMenuBar() +// - EndMenuBar() +// - BeginMainMenuBar() +// - EndMainMenuBar() +// - BeginMenu() +// - EndMenu() +// - MenuItemEx() [Internal] +// - MenuItem() +//------------------------------------------------------------------------- + +// Helpers for internal use +void ImGuiMenuColumns::Update(float spacing, bool window_reappearing) +{ + if (window_reappearing) + memset(Widths, 0, sizeof(Widths)); + Spacing = (ImU16)spacing; + CalcNextTotalWidth(true); + memset(Widths, 0, sizeof(Widths)); + TotalWidth = NextTotalWidth; + NextTotalWidth = 0; +} + +void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets) +{ + ImU16 offset = 0; + bool want_spacing = false; + for (int i = 0; i < IM_ARRAYSIZE(Widths); i++) + { + ImU16 width = Widths[i]; + if (want_spacing && width > 0) + offset += Spacing; + want_spacing |= (width > 0); + if (update_offsets) + { + if (i == 1) { OffsetLabel = offset; } + if (i == 2) { OffsetShortcut = offset; } + if (i == 3) { OffsetMark = offset; } + } + offset += width; + } + NextTotalWidth = offset; +} + +float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark) +{ + Widths[0] = ImMax(Widths[0], (ImU16)w_icon); + Widths[1] = ImMax(Widths[1], (ImU16)w_label); + Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut); + Widths[3] = ImMax(Widths[3], (ImU16)w_mark); + CalcNextTotalWidth(false); + return (float)ImMax(TotalWidth, NextTotalWidth); +} + +// FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere.. +// Currently the main responsibility of this function being to setup clip-rect + horizontal layout + menu navigation layer. +// Ideally we also want this to be responsible for claiming space out of the main window scrolling rectangle, in which case ImGuiWindowFlags_MenuBar will become unnecessary. +// Then later the same system could be used for multiple menu-bars, scrollbars, side-bars. +bool ImGui::BeginMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + if (!(window->Flags & ImGuiWindowFlags_MenuBar)) + return false; + + IM_ASSERT(!window->DC.MenuBarAppending); + BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore + PushID("##MenuBar"); + + // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect. + // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy. + const float border_top = ImMax(window->WindowBorderSize * 0.5f - window->TitleBarHeight, 0.0f); + ImRect bar_rect = window->MenuBarRect(); + ImRect clip_rect(IM_ROUND(bar_rect.Min.x + window->WindowBorderSize * 0.5f), IM_ROUND(bar_rect.Min.y + border_top), IM_ROUND(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, window->WindowBorderSize * 0.5f))), IM_ROUND(bar_rect.Max.y)); + clip_rect.ClipWith(window->OuterRectClipped); + PushClipRect(clip_rect.Min, clip_rect.Max, false); + + // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags). + window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.IsSameLine = false; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + window->DC.MenuBarAppending = true; + AlignTextToFramePadding(); + return true; +} + +void ImGui::EndMenuBar() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); + IM_ASSERT(window->DC.MenuBarAppending); + + // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. + if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + { + // Try to find out if the request is for one of our child menu + ImGuiWindow* nav_earliest_child = g.NavWindow; + while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) + nav_earliest_child = nav_earliest_child->ParentWindow; + if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + { + // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. + // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering) + const ImGuiNavLayer layer = ImGuiNavLayer_Menu; + IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary) + FocusWindow(window); + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto? + if (g.NavCursorVisible) + { + g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again + g.NavCursorHideFrames = 2; + } + g.NavHighlightItemUnderNav = g.NavMousePosDirty = true; + NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat + } + } + + PopClipRect(); + PopID(); + window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. + + // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here. + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.EmitItem = false; + ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos; + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent. + EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.IsSameLine = false; + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.MenuBarAppending = false; + window->DC.CursorMaxPos = restore_cursor_max_pos; +} + +// Important: calling order matters! +// FIXME: Somehow overlapping with docking tech. +// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts) +bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags) +{ + IM_ASSERT(dir != ImGuiDir_None); + + ImGuiWindow* bar_window = FindWindowByName(name); + if (bar_window == NULL || bar_window->BeginCount == 0) + { + // Calculate and set window size/position + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); + ImRect avail_rect = viewport->GetBuildWorkRect(); + ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + ImVec2 pos = avail_rect.Min; + if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) + pos[axis] = avail_rect.Max[axis] - axis_size; + ImVec2 size = avail_rect.GetSize(); + size[axis] = axis_size; + SetNextWindowPos(pos); + SetNextWindowSize(size); + + // Report our size into work area (for next frame) using actual window size + if (dir == ImGuiDir_Up || dir == ImGuiDir_Left) + viewport->BuildWorkInsetMin[axis] += axis_size; + else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right) + viewport->BuildWorkInsetMax[axis] += axis_size; + } + + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint + bool is_open = Begin(name, NULL, window_flags); + PopStyleVar(2); + + return is_open; +} + +bool ImGui::BeginMainMenuBar() +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + + // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. + // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea? + // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings. + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; + float height = GetFrameHeight(); + bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags); + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); + if (!is_open) + { + End(); + return false; + } + + // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356) + g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings; + BeginMenuBar(); + return is_open; +} + +void ImGui::EndMainMenuBar() +{ + ImGuiContext& g = *GImGui; + if (!g.CurrentWindow->DC.MenuBarAppending) + { + IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar + return; + } + + EndMenuBar(); + g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356) + + // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window + // FIXME: With this strategy we won't be able to restore a NULL focus. + if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0) + FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild); + + End(); +} + +static bool IsRootOfOpenMenuSet() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu)) + return false; + + // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others + // (e.g. inside menu bar vs loose menu items) based on parent ID. + // This would however prevent the use of e.g. PushID() user code submitting menus. + // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag, + // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects. + // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup + // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu. + // In the end, lack of ID check made it so we could no longer differentiate between separate menu sets. To compensate for that, we at least check parent window nav layer. + // This fixes the most common case of menu opening on hover when moving between window content and menu bar. Multiple different menu sets in same nav layer would still + // open on hover, but that should be a lesser problem, because if such menus are close in proximity in window content then it won't feel weird and if they are far apart + // it likely won't be a problem anyone runs into. + const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; + if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) + return false; + return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true); +} + +bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None); + + // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu) + // The first menu in a hierarchy isn't so hovering doesn't get across (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation. + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + window_flags |= ImGuiWindowFlags_ChildWindow; + + // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin(). + // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame. + // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used. + if (g.MenusIdSubmittedThisFrame.contains(id)) + { + if (menu_is_open) + menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + else + g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values + return menu_is_open; + } + + // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu + g.MenusIdSubmittedThisFrame.push_back(id); + + ImVec2 label_size = CalcTextSize(label, NULL, true); + + // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window) + // This is only done for items for the menu set and not the full parent window. + const bool menuset_is_open = IsRootOfOpenMenuSet(); + if (menuset_is_open) + PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); + + // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu, + // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup(). + // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering. + ImVec2 popup_pos, pos = window->DC.CursorPos; + PushID(label); + if (!enabled) + BeginDisabled(); + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + bool pressed; + + // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Menu inside a horizontal menu bar + // Selectable extend their highlight by half ItemSpacing in each direction. + // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() + popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); + PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f); + float w = label_size.x; + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y)); + LogSetNextTextDecoration("[", "]"); + RenderText(text_pos, label); + PopStyleVar(); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + // Menu inside a regular/vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame + float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); + LogSetNextTextDecoration("", ">"); + RenderText(text_pos, label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right); + } + if (!enabled) + EndDisabled(); + + const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav; + if (menuset_is_open) + PopItemFlag(); + + bool want_open = false; + bool want_open_nav_init = false; + bool want_close = false; + if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) + { + // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu + // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. + bool moving_toward_child_menu = false; + ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below) + ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL; + if (g.HoveredWindow == window && child_menu_window != NULL) + { + const float ref_unit = g.FontSize; // FIXME-DPI + const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f; + const ImRect next_window_rect = child_menu_window->Rect(); + ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta); + ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR(); + ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR(); + const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack. + ta.x += child_dir * -0.5f; + tb.x += child_dir * ref_unit; + tc.x += child_dir * ref_unit; + tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus + tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f); + moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); + //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] + } + + // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not) + // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon. + // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.) + if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0) + want_close = true; + + // Open + // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test) + if (!menu_is_open && pressed) // Click/activate to open + want_open = true; + else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open + want_open = true; + else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback) + want_open = true; + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open + { + want_open = want_open_nav_init = true; + NavMoveRequestCancel(); + SetNavCursorVisibleAfterMove(); + } + } + else + { + // Menu bar + if (menu_is_open && pressed && menuset_is_open) // Click an open menu again to close it + { + want_close = true; + want_open = menu_is_open = false; + } + else if (pressed || (hovered && menuset_is_open && !menu_is_open)) // First click to open, then hover to open others + { + want_open = true; + } + else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open + { + want_open = true; + NavMoveRequestCancel(); + } + } + + if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' + want_close = true; + if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None)) + ClosePopupToLevel(g.BeginPopupStack.Size, true); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); + PopID(); + + if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) + { + // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame. + OpenPopup(label); + } + else if (want_open) + { + menu_is_open = true; + OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0)); + } + + if (menu_is_open) + { + ImGuiLastItemData last_item_in_parent = g.LastItemData; + SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos. + PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding + menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open may be 'false' when the popup is completely clipped (e.g. zero size display) + PopStyleVar(); + if (menu_is_open) + { + // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do: + // Perform an init request in the case the popup was already open (via a previous mouse hover) + if (want_open && want_open_nav_init && !g.NavInitRequest) + { + FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal); + NavInitWindow(g.CurrentWindow, false); + } + + // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu() + // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck) + g.LastItemData = last_item_in_parent; + if (g.HoveredWindow == window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + } + } + else + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + } + + return menu_is_open; +} + +bool ImGui::BeginMenu(const char* label, bool enabled) +{ + return BeginMenuEx(label, NULL, enabled); +} + +void ImGui::EndMenu() +{ + // Nav: When a left move request our menu failed, close ourselves. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls + ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert. + if (window->BeginCount == window->BeginCountPreviousFrame) + if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet()) + if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical) + { + ClosePopupToLevel(g.BeginPopupStack.Size - 1, true); + NavMoveRequestCancel(); + } + + EndPopup(); +} + +bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImVec2 pos = window->DC.CursorPos; + ImVec2 label_size = CalcTextSize(label, NULL, true); + + // See BeginMenuEx() for comments about this. + const bool menuset_is_open = IsRootOfOpenMenuSet(); + if (menuset_is_open) + PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); + + // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73), + // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only. + bool pressed; + PushID(label); + if (!enabled) + BeginDisabled(); + + // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + { + // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful + // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark. + float w = label_size.x; + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f); + pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f)); + PopStyleVar(); + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) + RenderText(text_pos, label); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + } + else + { + // Menu item inside a vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame + float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) + { + RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + if (shortcut_w > 0.0f) + { + PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); + LogSetNextTextDecoration("(", ")"); + RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false); + PopStyleColor(); + } + if (selected) + RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); + } + } + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); + if (!enabled) + EndDisabled(); + PopID(); + if (menuset_is_open) + PopItemFlag(); + + return pressed; +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) +{ + return MenuItemEx(label, NULL, shortcut, selected, enabled); +} + +bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) +{ + if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled)) + { + if (p_selected) + *p_selected = !*p_selected; + return true; + } + return false; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: BeginTabBar, EndTabBar, etc. +//------------------------------------------------------------------------- +// - BeginTabBar() +// - BeginTabBarEx() [Internal] +// - EndTabBar() +// - TabBarLayout() [Internal] +// - TabBarCalcTabID() [Internal] +// - TabBarCalcMaxTabWidth() [Internal] +// - TabBarFindTabById() [Internal] +// - TabBarFindTabByOrder() [Internal] +// - TabBarGetCurrentTab() [Internal] +// - TabBarGetTabName() [Internal] +// - TabBarRemoveTab() [Internal] +// - TabBarCloseTab() [Internal] +// - TabBarScrollClamp() [Internal] +// - TabBarScrollToTab() [Internal] +// - TabBarQueueFocus() [Internal] +// - TabBarQueueReorder() [Internal] +// - TabBarProcessReorderFromMousePos() [Internal] +// - TabBarProcessReorder() [Internal] +// - TabBarScrollingButtons() [Internal] +// - TabBarTabListPopupButton() [Internal] +//------------------------------------------------------------------------- + +struct ImGuiTabBarSection +{ + int TabCount; // Number of tabs in this section. + float Width; // Sum of width of tabs in this section (after shrinking down) + float Spacing; // Horizontal spacing at the end of the section. + + ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); } +}; + +namespace ImGui +{ + static void TabBarLayout(ImGuiTabBar* tab_bar); + static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window); + static float TabBarCalcMaxTabWidth(); + static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling); + static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections); + static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar); + static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar); +} + +ImGuiTabBar::ImGuiTabBar() +{ + memset(this, 0, sizeof(*this)); + CurrFrameVisible = PrevFrameVisible = -1; + LastTabItemIdx = -1; +} + +static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab) +{ + return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; +} + +static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + const int a_section = TabItemGetSectionIdx(a); + const int b_section = TabItemGetSectionIdx(b); + if (a_section != b_section) + return a_section - b_section; + return (int)(a->IndexDuringLayout - b->IndexDuringLayout); +} + +static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + return (int)(a->BeginOrder - b->BeginOrder); +} + +static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref) +{ + ImGuiContext& g = *GImGui; + return ref.Ptr ? (ImGuiTabBar*)ref.Ptr : g.TabBars.GetByIndex(ref.Index); +} + +static ImGuiPtrOrIndex GetTabBarRefFromTabBar(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + if (g.TabBars.Contains(tab_bar)) + return ImGuiPtrOrIndex(g.TabBars.GetIndex(tab_bar)); + return ImGuiPtrOrIndex(tab_bar); +} + +bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiID id = window->GetID(str_id); + ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id); + ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2); + tab_bar->ID = id; + tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f); + tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f); + //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false)) + flags |= ImGuiTabBarFlags_IsFocused; + return BeginTabBarEx(tab_bar, tab_bar_bb, flags); +} + +bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + IM_ASSERT(tab_bar->ID != 0); + if ((flags & ImGuiTabBarFlags_DockNode) == 0) + PushOverrideID(tab_bar->ID); + + // Add to stack + g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar)); + g.CurrentTabBar = tab_bar; + tab_bar->Window = window; + + // Append with multiple BeginTabBar()/EndTabBar() pairs. + tab_bar->BackupCursorPos = window->DC.CursorPos; + if (tab_bar->CurrFrameVisible == g.FrameCount) + { + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); + tab_bar->BeginCount++; + return true; + } + + // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable + if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable))) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); + tab_bar->TabsAddedNew = false; + + // Flags + if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) + flags |= ImGuiTabBarFlags_FittingPolicyDefault_; + + tab_bar->Flags = flags; + tab_bar->BarRect = tab_bar_bb; + tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab() + tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible; + tab_bar->CurrFrameVisible = g.FrameCount; + tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight; + tab_bar->CurrTabsContentsHeight = 0.0f; + tab_bar->ItemSpacingY = g.Style.ItemSpacing.y; + tab_bar->FramePadding = g.Style.FramePadding; + tab_bar->TabsActiveCount = 0; + tab_bar->LastTabItemIdx = -1; + tab_bar->BeginCount = 1; + + // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); + + // Draw separator + // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable) + const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected); + if (g.Style.TabBarBorderSize > 0.0f) + { + const float y = tab_bar->BarRect.Max.y; + window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col); + } + return true; +} + +void ImGui::EndTabBar() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!"); + return; + } + + // Fallback in case no TabItem have been submitted + if (tab_bar->WantLayout) + TabBarLayout(tab_bar); + + // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed(). + const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); + if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing) + { + tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight); + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight; + } + else + { + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight; + } + if (tab_bar->BeginCount > 1) + window->DC.CursorPos = tab_bar->BackupCursorPos; + + tab_bar->LastTabItemIdx = -1; + if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) + PopID(); + + g.CurrentTabBarStack.pop_back(); + g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back()); +} + +// Scrolling happens only in the central section (leading/trailing sections are not scrolling) +static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections) +{ + return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing; +} + +// This is called only once a frame before by the first call to ItemTab() +// The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions. +static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + tab_bar->WantLayout = false; + + // Garbage collect by compacting list + // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section) + int tab_dst_n = 0; + bool need_sort_by_section = false; + ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing + for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n]; + if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose) + { + // Remove tab + if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; } + if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; } + if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; } + continue; + } + if (tab_dst_n != tab_src_n) + tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n]; + + tab = &tab_bar->Tabs[tab_dst_n]; + tab->IndexDuringLayout = (ImS16)tab_dst_n; + + // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another) + int curr_tab_section_n = TabItemGetSectionIdx(tab); + if (tab_dst_n > 0) + { + ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1]; + int prev_tab_section_n = TabItemGetSectionIdx(prev_tab); + if (curr_tab_section_n == 0 && prev_tab_section_n != 0) + need_sort_by_section = true; + if (prev_tab_section_n == 2 && curr_tab_section_n != 2) + need_sort_by_section = true; + } + + sections[curr_tab_section_n].TabCount++; + tab_dst_n++; + } + if (tab_bar->Tabs.Size != tab_dst_n) + tab_bar->Tabs.resize(tab_dst_n); + + if (need_sort_by_section) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection); + + // Calculate spacing between sections + sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + + // Setup next selected tab + ImGuiID scroll_to_tab_id = 0; + if (tab_bar->NextSelectedTabId) + { + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId; + tab_bar->NextSelectedTabId = 0; + scroll_to_tab_id = tab_bar->SelectedTabId; + } + + // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot). + if (tab_bar->ReorderRequestTabId != 0) + { + if (TabBarProcessReorder(tab_bar)) + if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId) + scroll_to_tab_id = tab_bar->ReorderRequestTabId; + tab_bar->ReorderRequestTabId = 0; + } + + // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!) + const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0; + if (tab_list_popup_button) + if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x! + scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; + + // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central + // (whereas our tabs are stored as: leading, central, trailing) + int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount }; + g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); + + // Compute ideal tabs widths + store them into shrink buffer + ImGuiTabItem* most_recently_selected_tab = NULL; + int curr_section_n = -1; + bool found_selected_tab_id = false; + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); + + if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button)) + most_recently_selected_tab = tab; + if (tab->ID == tab_bar->SelectedTabId) + found_selected_tab_id = true; + if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID) + scroll_to_tab_id = tab->ID; + + // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar. + // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet, + // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window. + const char* tab_name = TabBarGetTabName(tab_bar, tab); + const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument); + tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x; + + int section_n = TabItemGetSectionIdx(tab); + ImGuiTabBarSection* section = §ions[section_n]; + section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f); + curr_section_n = section_n; + + // Store data so we can build an array sorted by width if we need to shrink tabs down + IM_MSVC_WARNING_SUPPRESS(6385); + ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++]; + shrink_width_item->Index = tab_n; + shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth; + tab->Width = ImMax(tab->ContentWidth, 1.0f); + } + + // Compute total ideal width (used for e.g. auto-resizing a window) + tab_bar->WidthAllTabsIdeal = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) + tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing; + + // Horizontal scrolling buttons + // (note that TabBarScrollButtons() will alter BarRect.Max.x) + if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll)) + if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar)) + { + scroll_to_tab_id = scroll_and_select_tab->ID; + if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0) + tab_bar->SelectedTabId = scroll_to_tab_id; + } + + // Shrink widths if full tabs don't fit in their allocated space + float section_0_w = sections[0].Width + sections[0].Spacing; + float section_1_w = sections[1].Width + sections[1].Spacing; + float section_2_w = sections[2].Width + sections[2].Spacing; + bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth(); + float width_excess; + if (central_section_is_visible) + width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section + else + width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section + + // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore + if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible)) + { + int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount); + int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0); + ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess); + + // Apply shrunk values into tabs and sections + for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index]; + float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width); + if (shrinked_width < 0.0f) + continue; + + shrinked_width = ImMax(1.0f, shrinked_width); + int section_n = TabItemGetSectionIdx(tab); + sections[section_n].Width -= (tab->Width - shrinked_width); + tab->Width = shrinked_width; + } + } + + // Layout all active tabs + int section_tab_index = 0; + float tab_offset = 0.0f; + tab_bar->WidthAllTabs = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) + { + ImGuiTabBarSection* section = §ions[section_n]; + if (section_n == 2) + tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset); + + for (int tab_n = 0; tab_n < section->TabCount; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n]; + tab->Offset = tab_offset; + tab->NameOffset = -1; + tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f); + } + tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f); + tab_offset += section->Spacing; + section_tab_index += section->TabCount; + } + + // Clear name buffers + tab_bar->TabsNames.Buf.resize(0); + + // If we have lost the selected tab, select the next most recently active one + if (found_selected_tab_id == false) + tab_bar->SelectedTabId = 0; + if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL) + scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID; + + // Lock in visible tab + tab_bar->VisibleTabId = tab_bar->SelectedTabId; + tab_bar->VisibleTabWasSubmitted = false; + + // Apply request requests + if (scroll_to_tab_id != 0) + TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections); + else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow)) + { + const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH; + const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX; + if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f) + { + const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f; + tab_bar->ScrollingTargetDistToVisibility = 0.0f; + tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step); + } + SetKeyOwner(wheel_key, tab_bar->ID); + } + + // Update scrolling + tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim); + tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget); + if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget) + { + // Scrolling speed adjust itself so we can always reach our target in 1/3 seconds. + // Teleport if we are aiming far off the visible line + tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, 70.0f * g.FontSize); + tab_bar->ScrollingSpeed = ImMax(tab_bar->ScrollingSpeed, ImFabs(tab_bar->ScrollingTarget - tab_bar->ScrollingAnim) / 0.3f); + const bool teleport = (tab_bar->PrevFrameVisible + 1 < g.FrameCount) || (tab_bar->ScrollingTargetDistToVisibility > 10.0f * g.FontSize); + tab_bar->ScrollingAnim = teleport ? tab_bar->ScrollingTarget : ImLinearSweep(tab_bar->ScrollingAnim, tab_bar->ScrollingTarget, g.IO.DeltaTime * tab_bar->ScrollingSpeed); + } + else + { + tab_bar->ScrollingSpeed = 0.0f; + } + tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing; + tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing; + + // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame) + ImGuiWindow* window = g.CurrentWindow; + window->DC.CursorPos = tab_bar->BarRect.Min; + ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal); +} + +// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack. +static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window) +{ + IM_ASSERT(docked_window == NULL); // master branch only + IM_UNUSED(docked_window); + if (tab_bar->Flags & ImGuiTabBarFlags_DockNode) + { + ImGuiID id = ImHashStr(label); + KeepAliveID(id); + return id; + } + else + { + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(label); + } +} + +static float ImGui::TabBarCalcMaxTabWidth() +{ + ImGuiContext& g = *GImGui; + return g.FontSize * 20.0f; +} + +ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id) +{ + if (tab_id != 0) + for (int n = 0; n < tab_bar->Tabs.Size; n++) + if (tab_bar->Tabs[n].ID == tab_id) + return &tab_bar->Tabs[n]; + return NULL; +} + +// Order = visible order, not submission order! (which is tab->BeginOrder) +ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order) +{ + if (order < 0 || order >= tab_bar->Tabs.Size) + return NULL; + return &tab_bar->Tabs[order]; +} + +ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar) +{ + if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size) + return NULL; + return &tab_bar->Tabs[tab_bar->LastTabItemIdx]; +} + +const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + if (tab->NameOffset == -1) + return "N/A"; + IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size); + return tab_bar->TabsNames.Buf.Data + tab->NameOffset; +} + +// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. +void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) +{ + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) + tab_bar->Tabs.erase(tab); + if (tab_bar->VisibleTabId == tab_id) { tab_bar->VisibleTabId = 0; } + if (tab_bar->SelectedTabId == tab_id) { tab_bar->SelectedTabId = 0; } + if (tab_bar->NextSelectedTabId == tab_id) { tab_bar->NextSelectedTabId = 0; } +} + +// Called on manual closure attempt +void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + if (tab->Flags & ImGuiTabItemFlags_Button) + return; // A button appended with TabItemButton(). + + if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0) + { + // This will remove a frame of lag for selecting another tab on closure. + // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure + tab->WantClose = true; + if (tab_bar->VisibleTabId == tab->ID) + { + tab->LastFrameVisible = -1; + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0; + } + } + else + { + // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup) + if (tab_bar->VisibleTabId != tab->ID) + TabBarQueueFocus(tab_bar, tab); + } +} + +static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling) +{ + scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth()); + return ImMax(scrolling, 0.0f); +} + +// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys +static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections) +{ + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id); + if (tab == NULL) + return; + if (tab->Flags & ImGuiTabItemFlags_SectionMask_) + return; + + ImGuiContext& g = *GImGui; + float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar) + int order = TabBarGetTabOrder(tab_bar, tab); + + // Scrolling happens only in the central section (leading/trailing sections are not scrolling) + float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections); + + // We make all tabs positions all relative Sections[0].Width to make code simpler + float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f); + float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f); + tab_bar->ScrollingTargetDistToVisibility = 0.0f; + if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width)) + { + // Scroll to the left + tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f); + tab_bar->ScrollingTarget = tab_x1; + } + else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width) + { + // Scroll to the right + tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f); + tab_bar->ScrollingTarget = tab_x2 - scrollable_width; + } +} + +void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + tab_bar->NextSelectedTabId = tab->ID; +} + +void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name) +{ + IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars + ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL); + tab_bar->NextSelectedTabId = tab_id; +} + +void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset) +{ + IM_ASSERT(offset != 0); + IM_ASSERT(tab_bar->ReorderRequestTabId == 0); + tab_bar->ReorderRequestTabId = tab->ID; + tab_bar->ReorderRequestOffset = (ImS16)offset; +} + +void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(tab_bar->ReorderRequestTabId == 0); + if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0) + return; + + const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; + const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0); + + // Count number of contiguous tabs we are crossing over + const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1; + const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab); + int dst_idx = src_idx; + for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir) + { + // Reordered tabs must share the same section + const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i]; + if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder) + break; + if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_)) + break; + dst_idx = i; + + // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered. + const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x; + const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x; + //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255)); + if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2)) + break; + } + + if (dst_idx != src_idx) + TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx); +} + +bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) +{ + ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); + if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder)) + return false; + + //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools + int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset; + if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size) + return false; + + // Reordered tabs must share the same section + // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too) + ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; + if (tab2->Flags & ImGuiTabItemFlags_NoReorder) + return false; + if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_)) + return false; + + ImGuiTabItem item_tmp = *tab1; + ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2; + ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1; + const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset; + memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem)); + *tab2 = item_tmp; + + if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) + MarkIniSettingsDirty(); + return true; +} + +static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const ImVec2 arrow_button_size(g.FontSize - 2.0f, g.FontSize + g.Style.FramePadding.y * 2.0f); + const float scrolling_buttons_width = arrow_button_size.x * 2.0f; + + const ImVec2 backup_cursor_pos = window->DC.CursorPos; + //window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255)); + + int select_dir = 0; + ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; + arrow_col.w *= 0.5f; + + PushStyleColor(ImGuiCol_Text, arrow_col); + PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); + const float backup_repeat_delay = g.IO.KeyRepeatDelay; + const float backup_repeat_rate = g.IO.KeyRepeatRate; + g.IO.KeyRepeatDelay = 0.250f; + g.IO.KeyRepeatRate = 0.200f; + float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width); + window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick)) + select_dir = -1; + window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick)) + select_dir = +1; + PopItemFlag(); + PopStyleColor(2); + g.IO.KeyRepeatRate = backup_repeat_rate; + g.IO.KeyRepeatDelay = backup_repeat_delay; + + ImGuiTabItem* tab_to_scroll_to = NULL; + if (select_dir != 0) + if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) + { + int selected_order = TabBarGetTabOrder(tab_bar, tab_item); + int target_order = selected_order + select_dir; + + // Skip tab item buttons until another tab item is found or end is reached + while (tab_to_scroll_to == NULL) + { + // If we are at the end of the list, still scroll to make our tab visible + tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; + + // Cross through buttons + // (even if first/last item is a button, return it so we can update the scroll) + if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button) + { + target_order += select_dir; + selected_order += select_dir; + tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL; + } + } + } + window->DC.CursorPos = backup_cursor_pos; + tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; + + return tab_to_scroll_to; +} + +static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // We use g.Style.FramePadding.y to match the square ArrowButton size + const float tab_list_popup_button_width = g.FontSize + g.Style.FramePadding.y; + const ImVec2 backup_cursor_pos = window->DC.CursorPos; + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x - g.Style.FramePadding.y, tab_bar->BarRect.Min.y); + tab_bar->BarRect.Min.x += tab_list_popup_button_width; + + ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; + arrow_col.w *= 0.5f; + PushStyleColor(ImGuiCol_Text, arrow_col); + PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest); + PopStyleColor(2); + + ImGuiTabItem* tab_to_select = NULL; + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + + const char* tab_name = TabBarGetTabName(tab_bar, tab); + if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) + tab_to_select = tab; + } + EndCombo(); + } + + window->DC.CursorPos = backup_cursor_pos; + return tab_to_select; +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: BeginTabItem, EndTabItem, etc. +//------------------------------------------------------------------------- +// - BeginTabItem() +// - EndTabItem() +// - TabItemButton() +// - TabItemEx() [Internal] +// - SetTabItemClosed() +// - TabItemCalcSize() [Internal] +// - TabItemBackground() [Internal] +// - TabItemLabelAndCloseButton() [Internal] +//------------------------------------------------------------------------- + +bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! + + bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL); + if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; + PushOverrideID(tab->ID); // We already hashed 'label' so push into the ID stack directly instead of doing another hash through PushID(label) + } + return ret; +} + +void ImGui::EndTabItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return; + } + IM_ASSERT(tab_bar->LastTabItemIdx >= 0); + ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; + if (!(tab->Flags & ImGuiTabItemFlags_NoPushId)) + PopID(); +} + +bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL); +} + +void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return; + } + SetNextItemWidth(width); + TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL); +} + +bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window) +{ + // Layout whole tab bar if not already done + ImGuiContext& g = *GImGui; + if (tab_bar->WantLayout) + { + ImGuiNextItemData backup_next_item_data = g.NextItemData; + TabBarLayout(tab_bar); + g.NextItemData = backup_next_item_data; + } + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window); + + // If the user called us with *p_open == false, we early out and don't render. + // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID. + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + if (p_open && !*p_open) + { + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); + return false; + } + + IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button)); + IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing + + // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented) + if (flags & ImGuiTabItemFlags_NoCloseButton) + p_open = NULL; + else if (p_open == NULL) + flags |= ImGuiTabItemFlags_NoCloseButton; + + // Acquire tab data + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id); + bool tab_is_new = false; + if (tab == NULL) + { + tab_bar->Tabs.push_back(ImGuiTabItem()); + tab = &tab_bar->Tabs.back(); + tab->ID = id; + tab_bar->TabsAddedNew = tab_is_new = true; + } + tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab); + + // Calculate tab contents size + ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument)); + tab->RequestedWidth = -1.0f; + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth) + size.x = tab->RequestedWidth = g.NextItemData.Width; + if (tab_is_new) + tab->Width = ImMax(1.0f, size.x); + tab->ContentWidth = size.x; + tab->BeginOrder = tab_bar->TabsActiveCount++; + + const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); + const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; + const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); + const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument); + const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; + tab->LastFrameVisible = g.FrameCount; + tab->Flags = flags; + + // Append name _WITH_ the zero-terminator + if (docked_window != NULL) + { + IM_ASSERT(docked_window == NULL); // master branch only + } + else + { + tab->NameOffset = (ImS32)tab_bar->TabsNames.size(); + tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1); + } + + // Update selected tab + if (!is_tab_button) + { + if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) + if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) + TabBarQueueFocus(tab_bar, tab); // New tabs gets activated + if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar + TabBarQueueFocus(tab_bar, tab); + } + + // Lock visibility + // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!) + bool tab_contents_visible = (tab_bar->VisibleTabId == id); + if (tab_contents_visible) + tab_bar->VisibleTabWasSubmitted = true; + + // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches + if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing) + if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs)) + tab_contents_visible = true; + + // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted + // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'. + if (tab_appearing && (!tab_bar_appearing || tab_is_new)) + { + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); + if (is_tab_button) + return false; + return tab_contents_visible; + } + + if (tab_bar->SelectedTabId == id) + tab->LastFrameSelected = g.FrameCount; + + // Backup current layout position + const ImVec2 backup_main_cursor_pos = window->DC.CursorPos; + + // Layout + const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; + size.x = tab->Width; + if (is_central_section) + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + else + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); + ImVec2 pos = window->DC.CursorPos; + ImRect bb(pos, pos + size); + + // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) + const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX); + if (want_clip_rect) + PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true); + + ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; + ItemSize(bb.GetSize(), style.FramePadding.y); + window->DC.CursorMaxPos = backup_cursor_max_pos; + + if (!ItemAdd(bb, id)) + { + if (want_clip_rect) + PopClipRect(); + window->DC.CursorPos = backup_main_cursor_pos; + return tab_contents_visible; + } + + // Click to Select a tab + // Allow the close button to overlap + ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap); + if (g.DragDropActive) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + bool hovered, held, pressed; + if (flags & ImGuiTabItemFlags_Invisible) + hovered = held = pressed = false; + else + pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + if (pressed && !is_tab_button) + TabBarQueueFocus(tab_bar, tab); + + // Drag and drop: re-order tabs + if (held && !tab_appearing && IsMouseDragging(0)) + { + if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) + { + // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x) + { + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); + } + else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x) + { + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); + } + } + } + +#if 0 + if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth) + { + // Enlarge tab display when hovering + bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); + display_draw_list = GetForegroundDrawList(window); + TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive)); + } +#endif + + // Render tab shape + const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible); + if (is_visible) + { + ImDrawList* display_draw_list = window->DrawList; + const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed)); + TabItemBackground(display_draw_list, bb, flags, tab_col); + if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f) + { + // Might be moved to TabItemBackground() ? + ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale); + ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale); + ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline); + if (style.TabRounding > 0.0f) + { + float rounding = style.TabRounding; + display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9); + display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11); + display_draw_list->PathStroke(overline_col, 0, style.TabBarOverlineSize); + } + else + { + display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize); + } + } + RenderNavCursor(bb, id); + + // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. + const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button) + TabBarQueueFocus(tab_bar, tab); + + if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; + + // Render tab label, process close button + const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, id) : 0; + bool just_closed; + bool text_clipped; + TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped); + if (just_closed && p_open != NULL) + { + *p_open = false; + TabBarCloseTab(tab_bar, tab); + } + + // Tooltip + // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok) + // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores) + // FIXME: This is a mess. + // FIXME: We may want disabled tab to still display the tooltip? + if (text_clipped && g.HoveredId == id && !held) + if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) + SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); + } + + // Restore main window position so user can draw there + if (want_clip_rect) + PopClipRect(); + window->DC.CursorPos = backup_main_cursor_pos; + + IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected + if (is_tab_button) + return pressed; + return tab_contents_visible; +} + +// [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed. +// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar(). +// Tabs closed by the close button will automatically be flagged to avoid this issue. +void ImGui::SetTabItemClosed(const char* label) +{ + ImGuiContext& g = *GImGui; + bool is_within_manual_tab_bar = g.CurrentTabBar && !(g.CurrentTabBar->Flags & ImGuiTabBarFlags_DockNode); + if (is_within_manual_tab_bar) + { + ImGuiTabBar* tab_bar = g.CurrentTabBar; + ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL); + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) + tab->WantClose = true; // Will be processed by next call to TabBarLayout() + } +} + +ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker) +{ + ImGuiContext& g = *GImGui; + ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f); + if (has_close_button_or_unsaved_marker) + size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. + else + size.x += g.Style.FramePadding.x + 1.0f; + return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); +} + +ImVec2 ImGui::TabItemCalcSize(ImGuiWindow*) +{ + IM_ASSERT(0); // This function exists to facilitate merge with 'docking' branch. + return ImVec2(0.0f, 0.0f); +} + +void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col) +{ + // While rendering tabs, we trim 1 pixel off the top of our bounding box so they can fit within a regular frame height while looking "detached" from it. + ImGuiContext& g = *GImGui; + const float width = bb.GetWidth(); + IM_UNUSED(flags); + IM_ASSERT(width > 0.0f); + const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); + const float y1 = bb.Min.y + 1.0f; + const float y2 = bb.Max.y - g.Style.TabBarBorderSize; + draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); + draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9); + draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12); + draw_list->PathLineTo(ImVec2(bb.Max.x, y2)); + draw_list->PathFillConvex(col); + if (g.Style.TabBorderSize > 0.0f) + { + draw_list->PathLineTo(ImVec2(bb.Min.x + 0.5f, y2)); + draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); + draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); + draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); + draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); + } +} + +// Render text label (with custom clipping) + Unsaved Document marker + Close Button logic +// We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter. +void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped) +{ + ImGuiContext& g = *GImGui; + ImVec2 label_size = CalcTextSize(label, NULL, true); + + if (out_just_closed) + *out_just_closed = false; + if (out_text_clipped) + *out_text_clipped = false; + + if (bb.GetWidth() <= 1.0f) + return; + + // In Style V2 we'll have full override of all colors per state (e.g. focused, selected) + // But right now if you want to alter text color of tabs this is what you need to do. +#if 0 + const float backup_alpha = g.Style.Alpha; + if (!is_contents_visible) + g.Style.Alpha *= 0.7f; +#endif + + // Render text label (with clipping + alpha gradient) + unsaved marker + ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y); + ImRect text_ellipsis_clip_bb = text_pixel_clip_bb; + + // Return clipped state ignoring the close button + if (out_text_clipped) + { + *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x; + //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255)); + } + + const float button_sz = g.FontSize; + const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y); + + // Close Button & Unsaved Marker + // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap() + // 'hovered' will be true when hovering the Tab but NOT when hovering the close button + // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button + // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false + bool close_button_pressed = false; + bool close_button_visible = false; + bool is_hovered = g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id; // Any interaction account for this too. + + if (close_button_id != 0) + { + if (is_contents_visible) + close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected)); + else + close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected)); + } + + // When tabs/document is unsaved, the unsaved marker takes priority over the close button. + const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered); + if (unsaved_marker_visible) + { + const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz)); + RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text)); + } + else if (close_button_visible) + { + ImGuiLastItemData last_item_backup = g.LastItemData; + if (CloseButton(close_button_id, button_pos)) + close_button_pressed = true; + g.LastItemData = last_item_backup; + + // Close with middle mouse button + if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2)) + close_button_pressed = true; + } + + // This is all rather complicated + // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position) + // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist.. + float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f; + if (close_button_visible || unsaved_marker_visible) + { + text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f); + text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f; + ellipsis_max_x = text_pixel_clip_bb.Max.x; + } + LogSetNextTextDecoration("/", "\\"); + RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size); + +#if 0 + if (!is_contents_visible) + g.Style.Alpha = backup_alpha; +#endif + + if (out_just_closed) + *out_just_closed = close_button_pressed; +} + + +#endif // #ifndef IMGUI_DISABLE diff --git a/thirdparty/imgui/imstb_rectpack.h b/thirdparty/imgui/imstb_rectpack.h new file mode 100644 index 00000000..f6917e7a --- /dev/null +++ b/thirdparty/imgui/imstb_rectpack.h @@ -0,0 +1,627 @@ +// [DEAR IMGUI] +// This is a slightly modified version of stb_rect_pack.h 1.01. +// Grep for [DEAR IMGUI] to find the changes. +// +// stb_rect_pack.h - v1.01 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Before #including, +// +// #define STB_RECT_PACK_IMPLEMENTATION +// +// in the file that you want to have the implementation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// github:IntellectualKitty +// +// Bugfixes / warning fixes +// Jeremy Jaussaud +// Fabian Giesen +// +// Version history: +// +// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section +// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles +// 0.99 (2019-02-07) warning fixes +// 0.11 (2017-03-03) return packing success/fail result +// 0.10 (2016-10-25) remove cast-away-const to avoid warnings +// 0.09 (2016-08-27) fix compiler warnings +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// See end of file for license information. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct stbrp_context stbrp_context; +typedef struct stbrp_node stbrp_node; +typedef struct stbrp_rect stbrp_rect; + +typedef int stbrp_coord; + +#define STBRP__MAXVAL 0x7fffffff +// Mostly for internal use, but this is the maximum supported coordinate value. + +STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); +// Assign packed locations to rectangles. The rectangles are of type +// 'stbrp_rect' defined below, stored in the array 'rects', and there +// are 'num_rects' many of them. +// +// Rectangles which are successfully packed have the 'was_packed' flag +// set to a non-zero value and 'x' and 'y' store the minimum location +// on each axis (i.e. bottom-left in cartesian coordinates, top-left +// if you imagine y increasing downwards). Rectangles which do not fit +// have the 'was_packed' flag set to 0. +// +// You should not try to access the 'rects' array from another thread +// while this function is running, as the function temporarily reorders +// the array while it executes. +// +// To pack into another rectangle, you need to call stbrp_init_target +// again. To continue packing into the same rectangle, you can call +// this function again. Calling this multiple times with multiple rect +// arrays will probably produce worse packing results than calling it +// a single time with the full rectangle array, but the option is +// available. +// +// The function returns 1 if all of the rectangles were successfully +// packed and 0 otherwise. + +struct stbrp_rect +{ + // reserved for your use: + int id; + + // input: + stbrp_coord w, h; + + // output: + stbrp_coord x, y; + int was_packed; // non-zero if valid packing + +}; // 16 bytes, nominally + + +STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes); +// Initialize a rectangle packer to: +// pack a rectangle that is 'width' by 'height' in dimensions +// using temporary storage provided by the array 'nodes', which is 'num_nodes' long +// +// You must call this function every time you start packing into a new target. +// +// There is no "shutdown" function. The 'nodes' memory must stay valid for +// the following stbrp_pack_rects() call (or calls), but can be freed after +// the call (or calls) finish. +// +// Note: to guarantee best results, either: +// 1. make sure 'num_nodes' >= 'width' +// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1' +// +// If you don't do either of the above things, widths will be quantized to multiples +// of small integers to guarantee the algorithm doesn't run out of temporary storage. +// +// If you do #2, then the non-quantized algorithm will be used, but the algorithm +// may run out of temporary storage and be unable to pack some rectangles. + +STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem); +// Optionally call this function after init but before doing any packing to +// change the handling of the out-of-temp-memory scenario, described above. +// If you call init again, this will be reset to the default (false). + + +STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic); +// Optionally select which packing heuristic the library should use. Different +// heuristics will produce better/worse results for different data sets. +// If you call init again, this will be reset to the default. + +enum +{ + STBRP_HEURISTIC_Skyline_default=0, + STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default, + STBRP_HEURISTIC_Skyline_BF_sortHeight +}; + + +////////////////////////////////////////////////////////////////////////////// +// +// the details of the following structures don't matter to you, but they must +// be visible so you can handle the memory allocations for them + +struct stbrp_node +{ + stbrp_coord x,y; + stbrp_node *next; +}; + +struct stbrp_context +{ + int width; + int height; + int align; + int init_mode; + int heuristic; + int num_nodes; + stbrp_node *active_head; + stbrp_node *free_head; + stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +#ifdef _MSC_VER +#define STBRP__NOTUSED(v) (void)(v) +#define STBRP__CDECL __cdecl +#else +#define STBRP__NOTUSED(v) (void)sizeof(v) +#define STBRP__CDECL +#endif + +enum +{ + STBRP__INIT_skyline = 1 +}; + +STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic) +{ + switch (context->init_mode) { + case STBRP__INIT_skyline: + STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight); + context->heuristic = heuristic; + break; + default: + STBRP_ASSERT(0); + } +} + +STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem) +{ + if (allow_out_of_mem) + // if it's ok to run out of memory, then don't bother aligning them; + // this gives better packing, but may fail due to OOM (even though + // the rectangles easily fit). @TODO a smarter approach would be to only + // quantize once we've hit OOM, then we could get rid of this parameter. + context->align = 1; + else { + // if it's not ok to run out of memory, then quantize the widths + // so that num_nodes is always enough nodes. + // + // I.e. num_nodes * align >= width + // align >= width / num_nodes + // align = ceil(width/num_nodes) + + context->align = (context->width + context->num_nodes-1) / context->num_nodes; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; + + for (i=0; i < num_nodes-1; ++i) + nodes[i].next = &nodes[i+1]; + nodes[i].next = NULL; + context->init_mode = STBRP__INIT_skyline; + context->heuristic = STBRP_HEURISTIC_Skyline_default; + context->free_head = &nodes[0]; + context->active_head = &context->extra[0]; + context->width = width; + context->height = height; + context->num_nodes = num_nodes; + stbrp_setup_allow_out_of_mem(context, 0); + + // node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) + context->extra[0].x = 0; + context->extra[0].y = 0; + context->extra[0].next = &context->extra[1]; + context->extra[1].x = (stbrp_coord) width; + context->extra[1].y = (1<<30); + context->extra[1].next = NULL; +} + +// find minimum y position if it starts at x1 +static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste) +{ + stbrp_node *node = first; + int x1 = x0 + width; + int min_y, visited_width, waste_area; + + STBRP__NOTUSED(c); + + STBRP_ASSERT(first->x <= x0); + + #if 0 + // skip in case we're past the node + while (node->next->x <= x0) + ++node; + #else + STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency + #endif + + STBRP_ASSERT(node->x <= x0); + + min_y = 0; + waste_area = 0; + visited_width = 0; + while (node->x < x1) { + if (node->y > min_y) { + // raise min_y higher. + // we've accounted for all waste up to min_y, + // but we'll now add more waste for everything we've visted + waste_area += visited_width * (node->y - min_y); + min_y = node->y; + // the first time through, visited_width might be reduced + if (node->x < x0) + visited_width += node->next->x - x0; + else + visited_width += node->next->x - node->x; + } else { + // add waste area + int under_width = node->next->x - node->x; + if (under_width + visited_width > width) + under_width = width - visited_width; + waste_area += under_width * (min_y - node->y); + visited_width += under_width; + } + node = node->next; + } + + *pwaste = waste_area; + return min_y; +} + +typedef struct +{ + int x,y; + stbrp_node **prev_link; +} stbrp__findresult; + +static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height) +{ + int best_waste = (1<<30), best_x, best_y = (1 << 30); + stbrp__findresult fr; + stbrp_node **prev, *node, *tail, **best = NULL; + + // align to multiple of c->align + width = (width + c->align - 1); + width -= width % c->align; + STBRP_ASSERT(width % c->align == 0); + + // if it can't possibly fit, bail immediately + if (width > c->width || height > c->height) { + fr.prev_link = NULL; + fr.x = fr.y = 0; + return fr; + } + + node = c->active_head; + prev = &c->active_head; + while (node->x + width <= c->width) { + int y,waste; + y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste); + if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL + // bottom left + if (y < best_y) { + best_y = y; + best = prev; + } + } else { + // best-fit + if (y + height <= c->height) { + // can only use it if it first vertically + if (y < best_y || (y == best_y && waste < best_waste)) { + best_y = y; + best_waste = waste; + best = prev; + } + } + } + prev = &node->next; + node = node->next; + } + + best_x = (best == NULL) ? 0 : (*best)->x; + + // if doing best-fit (BF), we also have to try aligning right edge to each node position + // + // e.g, if fitting + // + // ____________________ + // |____________________| + // + // into + // + // | | + // | ____________| + // |____________| + // + // then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned + // + // This makes BF take about 2x the time + + if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) { + tail = c->active_head; + node = c->active_head; + prev = &c->active_head; + // find first node that's admissible + while (tail->x < width) + tail = tail->next; + while (tail) { + int xpos = tail->x - width; + int y,waste; + STBRP_ASSERT(xpos >= 0); + // find the left position that matches this + while (node->next->x <= xpos) { + prev = &node->next; + node = node->next; + } + STBRP_ASSERT(node->next->x > xpos && node->x <= xpos); + y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste); + if (y + height <= c->height) { + if (y <= best_y) { + if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { + best_x = xpos; + //STBRP_ASSERT(y <= best_y); [DEAR IMGUI] + best_y = y; + best_waste = waste; + best = prev; + } + } + } + tail = tail->next; + } + } + + fr.prev_link = best; + fr.x = best_x; + fr.y = best_y; + return fr; +} + +static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height) +{ + // find best position according to heuristic + stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height); + stbrp_node *node, *cur; + + // bail if: + // 1. it failed + // 2. the best node doesn't fit (we don't always check this) + // 3. we're out of memory + if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) { + res.prev_link = NULL; + return res; + } + + // on success, create new node + node = context->free_head; + node->x = (stbrp_coord) res.x; + node->y = (stbrp_coord) (res.y + height); + + context->free_head = node->next; + + // insert the new node into the right starting point, and + // let 'cur' point to the remaining nodes needing to be + // stiched back in + + cur = *res.prev_link; + if (cur->x < res.x) { + // preserve the existing one, so start testing with the next one + stbrp_node *next = cur->next; + cur->next = node; + cur = next; + } else { + *res.prev_link = node; + } + + // from here, traverse cur and free the nodes, until we get to one + // that shouldn't be freed + while (cur->next && cur->next->x <= res.x + width) { + stbrp_node *next = cur->next; + // move the current node to the free list + cur->next = context->free_head; + context->free_head = cur; + cur = next; + } + + // stitch the list back in + node->next = cur; + + if (cur->x < res.x + width) + cur->x = (stbrp_coord) (res.x + width); + +#ifdef _DEBUG + cur = context->active_head; + while (cur->x < context->width) { + STBRP_ASSERT(cur->x < cur->next->x); + cur = cur->next; + } + STBRP_ASSERT(cur->next == NULL); + + { + int count=0; + cur = context->active_head; + while (cur) { + cur = cur->next; + ++count; + } + cur = context->free_head; + while (cur) { + cur = cur->next; + ++count; + } + STBRP_ASSERT(count == context->num_nodes+2); + } +#endif + + return res; +} + +static int STBRP__CDECL rect_height_compare(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + if (p->h > q->h) + return -1; + if (p->h < q->h) + return 1; + return (p->w > q->w) ? -1 : (p->w < q->w); +} + +static int STBRP__CDECL rect_original_order(const void *a, const void *b) +{ + const stbrp_rect *p = (const stbrp_rect *) a; + const stbrp_rect *q = (const stbrp_rect *) b; + return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); +} + +STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) +{ + int i, all_rects_packed = 1; + + // we use the 'was_packed' field internally to allow sorting/unsorting + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = i; + } + + // sort according to heuristic + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare); + + for (i=0; i < num_rects; ++i) { + if (rects[i].w == 0 || rects[i].h == 0) { + rects[i].x = rects[i].y = 0; // empty rect needs no space + } else { + stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h); + if (fr.prev_link) { + rects[i].x = (stbrp_coord) fr.x; + rects[i].y = (stbrp_coord) fr.y; + } else { + rects[i].x = rects[i].y = STBRP__MAXVAL; + } + } + } + + // unsort + STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order); + + // set was_packed flags and all_rects_packed status + for (i=0; i < num_rects; ++i) { + rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL); + if (!rects[i].was_packed) + all_rects_packed = 0; + } + + // return the all_rects_packed status + return all_rects_packed; +} +#endif + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/thirdparty/imgui/imstb_textedit.h b/thirdparty/imgui/imstb_textedit.h new file mode 100644 index 00000000..b7a761c8 --- /dev/null +++ b/thirdparty/imgui/imstb_textedit.h @@ -0,0 +1,1469 @@ +// [DEAR IMGUI] +// This is a slightly modified version of stb_textedit.h 1.14. +// Those changes would need to be pushed into nothings/stb: +// - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) +// - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783) +// - Added name to struct or it may be forward declared in our code. +// - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925) +// Grep for [DEAR IMGUI] to find the changes. +// - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_* + +// stb_textedit.h - v1.14 - public domain - Sean Barrett +// Development of this library was sponsored by RAD Game Tools +// +// This C header file implements the guts of a multi-line text-editing +// widget; you implement display, word-wrapping, and low-level string +// insertion/deletion, and stb_textedit will map user inputs into +// insertions & deletions, plus updates to the cursor position, +// selection state, and undo state. +// +// It is intended for use in games and other systems that need to build +// their own custom widgets and which do not have heavy text-editing +// requirements (this library is not recommended for use for editing large +// texts, as its performance does not scale and it has limited undo). +// +// Non-trivial behaviors are modelled after Windows text controls. +// +// +// LICENSE +// +// See end of file for license information. +// +// +// DEPENDENCIES +// +// Uses the C runtime function 'memmove', which you can override +// by defining IMSTB_TEXTEDIT_memmove before the implementation. +// Uses no other functions. Performs no runtime allocations. +// +// +// VERSION HISTORY +// +// 1.14 (2021-07-11) page up/down, various fixes +// 1.13 (2019-02-07) fix bug in undo size management +// 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash +// 1.11 (2017-03-03) fix HOME on last line, dragging off single-line textfield +// 1.10 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.9 (2016-08-27) customizable move-by-word +// 1.8 (2016-04-02) better keyboard handling when mouse button is down +// 1.7 (2015-09-13) change y range handling in case baseline is non-0 +// 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove +// 1.5 (2014-09-10) add support for secondary keys for OS X +// 1.4 (2014-08-17) fix signed/unsigned warnings +// 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary +// 1.2 (2014-05-27) fix some RAD types that had crept into the new code +// 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE ) +// 1.0 (2012-07-26) improve documentation, initial public release +// 0.3 (2012-02-24) bugfixes, single-line mode; insert mode +// 0.2 (2011-11-28) fixes to undo/redo +// 0.1 (2010-07-08) initial version +// +// ADDITIONAL CONTRIBUTORS +// +// Ulf Winklemann: move-by-word in 1.1 +// Fabian Giesen: secondary key inputs in 1.5 +// Martins Mozeiko: STB_TEXTEDIT_memmove in 1.6 +// Louis Schnellbach: page up/down in 1.14 +// +// Bugfixes: +// Scott Graham +// Daniel Keller +// Omar Cornut +// Dan Thompson +// +// USAGE +// +// This file behaves differently depending on what symbols you define +// before including it. +// +// +// Header-file mode: +// +// If you do not define STB_TEXTEDIT_IMPLEMENTATION before including this, +// it will operate in "header file" mode. In this mode, it declares a +// single public symbol, STB_TexteditState, which encapsulates the current +// state of a text widget (except for the string, which you will store +// separately). +// +// To compile in this mode, you must define STB_TEXTEDIT_CHARTYPE to a +// primitive type that defines a single character (e.g. char, wchar_t, etc). +// +// To save space or increase undo-ability, you can optionally define the +// following things that are used by the undo system: +// +// STB_TEXTEDIT_POSITIONTYPE small int type encoding a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// If you don't define these, they are set to permissive types and +// moderate sizes. The undo system does no memory allocations, so +// it grows STB_TexteditState by the worst-case storage which is (in bytes): +// +// [4 + 3 * sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATECOUNT +// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHARCOUNT +// +// +// Implementation mode: +// +// If you define STB_TEXTEDIT_IMPLEMENTATION before including this, it +// will compile the implementation of the text edit widget, depending +// on a large number of symbols which must be defined before the include. +// +// The implementation is defined only as static functions. You will then +// need to provide your own APIs in the same file which will access the +// static functions. +// +// The basic concept is that you provide a "string" object which +// behaves like an array of characters. stb_textedit uses indices to +// refer to positions in the string, implicitly representing positions +// in the displayed textedit. This is true for both plain text and +// rich text; even with rich text stb_truetype interacts with your +// code as if there was an array of all the displayed characters. +// +// Symbols that must be the same in header-file and implementation mode: +// +// STB_TEXTEDIT_CHARTYPE the character type +// STB_TEXTEDIT_POSITIONTYPE small type that is a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// Symbols you must define for implementation mode: +// +// STB_TEXTEDIT_STRING the type of object representing a string being edited, +// typically this is a wrapper object with other data you need +// +// STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1)) +// STB_TEXTEDIT_LAYOUTROW(&r,obj,n) returns the results of laying out a line of characters +// starting from character #n (see discussion below) +// STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character +// to the xpos of the i+1'th char for a line of characters +// starting at character #n (i.e. accounts for kerning +// with previous char) +// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character +// (return type is int, -1 means not valid to insert) +// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based +// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize +// as manually wordwrapping for end-of-line positioning +// +// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i +// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*) +// +// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key +// +// STB_TEXTEDIT_K_LEFT keyboard input to move cursor left +// STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right +// STB_TEXTEDIT_K_UP keyboard input to move cursor up +// STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page +// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page +// STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME +// STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END +// STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME +// STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text // e.g. ctrl-END +// STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor +// STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor +// STB_TEXTEDIT_K_UNDO keyboard input to perform undo +// STB_TEXTEDIT_K_REDO keyboard input to perform redo +// +// Optional: +// STB_TEXTEDIT_K_INSERT keyboard input to toggle insert mode +// STB_TEXTEDIT_IS_SPACE(ch) true if character is whitespace (e.g. 'isspace'), +// required for default WORDLEFT/WORDRIGHT handlers +// STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to +// STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to +// STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word // e.g. ctrl-LEFT +// STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word // e.g. ctrl-RIGHT +// STB_TEXTEDIT_K_LINESTART2 secondary keyboard input to move cursor to start of line +// STB_TEXTEDIT_K_LINEEND2 secondary keyboard input to move cursor to end of line +// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text +// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text +// +// Keyboard input must be encoded as a single integer value; e.g. a character code +// and some bitflags that represent shift states. to simplify the interface, SHIFT must +// be a bitflag, so we can test the shifted state of cursor movements to allow selection, +// i.e. (STB_TEXTEDIT_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. +// +// You can encode other things, such as CONTROL or ALT, in additional bits, and +// then test for their presence in e.g. STB_TEXTEDIT_K_WORDLEFT. For example, +// my Windows implementations add an additional CONTROL bit, and an additional KEYDOWN +// bit. Then all of the STB_TEXTEDIT_K_ values bitwise-or in the KEYDOWN bit, +// and I pass both WM_KEYDOWN and WM_CHAR events to the "key" function in the +// API below. The control keys will only match WM_KEYDOWN events because of the +// keydown bit I add, and STB_TEXTEDIT_KEYTOTEXT only tests for the KEYDOWN +// bit so it only decodes WM_CHAR events. +// +// STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed +// row of characters assuming they start on the i'th character--the width and +// the height and the number of characters consumed. This allows this library +// to traverse the entire layout incrementally. You need to compute word-wrapping +// here. +// +// Each textfield keeps its own insert mode state, which is not how normal +// applications work. To keep an app-wide insert mode, update/copy the +// "insert_mode" field of STB_TexteditState before/after calling API functions. +// +// API +// +// void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +// +// void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +// int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +// void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXEDIT_KEYTYPE key) +// void stb_textedit_text(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int text_len) +// +// Each of these functions potentially updates the string and updates the +// state. +// +// initialize_state: +// set the textedit state to a known good default state when initially +// constructing the textedit. +// +// click: +// call this with the mouse x,y on a mouse down; it will update the cursor +// and reset the selection start/end to the cursor point. the x,y must +// be relative to the text widget, with (0,0) being the top left. +// +// drag: +// call this with the mouse x,y on a mouse drag/up; it will update the +// cursor and the selection end point +// +// cut: +// call this to delete the current selection; returns true if there was +// one. you should FIRST copy the current selection to the system paste buffer. +// (To copy, just copy the current selection out of the string yourself.) +// +// paste: +// call this to paste text at the current cursor point or over the current +// selection if there is one. +// +// key: +// call this for keyboard inputs sent to the textfield. you can use it +// for "key down" events or for "translated" key events. if you need to +// do both (as in Win32), or distinguish Unicode characters from control +// inputs, set a high bit to distinguish the two; then you can define the +// various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit +// set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is +// clear. STB_TEXTEDIT_KEYTYPE defaults to int, but you can #define it to +// anything other type you want before including. +// if the STB_TEXTEDIT_KEYTOTEXT function is defined, selected keys are +// transformed into text and stb_textedit_text() is automatically called. +// +// text: [DEAR IMGUI] added 2024-09 +// call this to text inputs sent to the textfield. +// +// +// When rendering, you can read the cursor position and selection state from +// the STB_TexteditState. +// +// +// Notes: +// +// This is designed to be usable in IMGUI, so it allows for the possibility of +// running in an IMGUI that has NOT cached the multi-line layout. For this +// reason, it provides an interface that is compatible with computing the +// layout incrementally--we try to make sure we make as few passes through +// as possible. (For example, to locate the mouse pointer in the text, we +// could define functions that return the X and Y positions of characters +// and binary search Y and then X, but if we're doing dynamic layout this +// will run the layout algorithm many times, so instead we manually search +// forward in one pass. Similar logic applies to e.g. up-arrow and +// down-arrow movement.) +// +// If it's run in a widget that *has* cached the layout, then this is less +// efficient, but it's not horrible on modern computers. But you wouldn't +// want to edit million-line files with it. + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Header-file mode +//// +//// + +#ifndef INCLUDE_IMSTB_TEXTEDIT_H +#define INCLUDE_IMSTB_TEXTEDIT_H + +//////////////////////////////////////////////////////////////////////// +// +// STB_TexteditState +// +// Definition of STB_TexteditState which you should store +// per-textfield; it includes cursor position, selection state, +// and undo state. +// + +#ifndef IMSTB_TEXTEDIT_UNDOSTATECOUNT +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 +#endif +#ifndef IMSTB_TEXTEDIT_UNDOCHARCOUNT +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 +#endif +#ifndef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_CHARTYPE int +#endif +#ifndef IMSTB_TEXTEDIT_POSITIONTYPE +#define IMSTB_TEXTEDIT_POSITIONTYPE int +#endif + +typedef struct +{ + // private data + IMSTB_TEXTEDIT_POSITIONTYPE where; + IMSTB_TEXTEDIT_POSITIONTYPE insert_length; + IMSTB_TEXTEDIT_POSITIONTYPE delete_length; + int char_storage; +} StbUndoRecord; + +typedef struct +{ + // private data + StbUndoRecord undo_rec [IMSTB_TEXTEDIT_UNDOSTATECOUNT]; + IMSTB_TEXTEDIT_CHARTYPE undo_char[IMSTB_TEXTEDIT_UNDOCHARCOUNT]; + short undo_point, redo_point; + int undo_char_point, redo_char_point; +} StbUndoState; + +typedef struct STB_TexteditState +{ + ///////////////////// + // + // public data + // + + int cursor; + // position of the text cursor within the string + + int select_start; // selection start point + int select_end; + // selection start and end point in characters; if equal, no selection. + // note that start may be less than or greater than end (e.g. when + // dragging the mouse, start is where the initial click was, and you + // can drag in either direction) + + unsigned char insert_mode; + // each textfield keeps its own insert mode state. to keep an app-wide + // insert mode, copy this value in/out of the app state + + int row_count_per_page; + // page size in number of row. + // this value MUST be set to >0 for pageup or pagedown in multilines documents. + + ///////////////////// + // + // private data + // + unsigned char cursor_at_end_of_line; // not implemented yet + unsigned char initialized; + unsigned char has_preferred_x; + unsigned char single_line; + unsigned char padding1, padding2, padding3; + float preferred_x; // this determines where the cursor up/down tries to seek to along x + StbUndoState undostate; +} STB_TexteditState; + + +//////////////////////////////////////////////////////////////////////// +// +// StbTexteditRow +// +// Result of layout query, used by stb_textedit to determine where +// the text in each row is. + +// result of layout query +typedef struct +{ + float x0,x1; // starting x location, end x location (allows for align=right, etc) + float baseline_y_delta; // position of baseline relative to previous row's baseline + float ymin,ymax; // height of row above and below baseline + int num_chars; +} StbTexteditRow; +#endif //INCLUDE_IMSTB_TEXTEDIT_H + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Implementation mode +//// +//// + + +// implementation isn't include-guarded, since it might have indirectly +// included just the "header" portion +#ifdef IMSTB_TEXTEDIT_IMPLEMENTATION + +#ifndef IMSTB_TEXTEDIT_memmove +#include +#define IMSTB_TEXTEDIT_memmove memmove +#endif + + +///////////////////////////////////////////////////////////////////////////// +// +// Mouse input handling +// + +// traverse the layout to locate the nearest character to a display position +static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) +{ + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + float base_y = 0, prev_x; + int i=0, k; + + r.x0 = r.x1 = 0; + r.ymin = r.ymax = 0; + r.num_chars = 0; + + // search rows to find one that straddles 'y' + while (i < n) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + return n; + + if (i==0 && y < base_y + r.ymin) + return 0; + + if (y < base_y + r.ymax) + break; + + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // below all text, return 'after' last character + if (i >= n) + return n; + + // check if it's before the beginning of the line + if (x < r.x0) + return i; + + // check if it's before the end of the line + if (x < r.x1) { + // search characters in row for one that straddles 'x' + prev_x = r.x0; + for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) { + float w = STB_TEXTEDIT_GETWIDTH(str, i, k); + if (x < prev_x+w) { + if (x < prev_x+w/2) + return k+i; + else + return IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k); + } + prev_x += w; + } + // shouldn't happen, but if it does, fall through to end-of-line case + } + + // if the last character is a newline, return that. otherwise return 'after' the last character + if (STB_TEXTEDIT_GETCHAR(str, i+r.num_chars-1) == STB_TEXTEDIT_NEWLINE) + return i+r.num_chars-1; + else + return i+r.num_chars; +} + +// API click: on mouse down, move the cursor to the clicked location, and reset the selection +static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse + // goes off the top or bottom of the text + if( state->single_line ) + { + StbTexteditRow r; + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + y = r.ymin; + } + + state->cursor = stb_text_locate_coord(str, x, y); + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; +} + +// API drag: on mouse drag, move the cursor and selection endpoint to the clicked location +static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +{ + int p = 0; + + // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse + // goes off the top or bottom of the text + if( state->single_line ) + { + StbTexteditRow r; + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + y = r.ymin; + } + + if (state->select_start == state->select_end) + state->select_start = state->cursor; + + p = stb_text_locate_coord(str, x, y); + state->cursor = state->select_end = p; +} + +///////////////////////////////////////////////////////////////////////////// +// +// Keyboard input handling +// + +// forward declarations +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); + +typedef struct +{ + float x,y; // position of n'th character + float height; // height of line + int first_char, length; // first char of row, and length + int prev_first; // first char of previous row +} StbFindState; + +// find the x/y location of a character, and remember info about the previous row in +// case we get a move-up event (for page up, we'll have to rescan) +static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING *str, int n, int single_line) +{ + StbTexteditRow r; + int prev_start = 0; + int z = STB_TEXTEDIT_STRINGLEN(str); + int i=0, first; + + if (n == z && single_line) { + // special case if it's at the end (may not be needed?) + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + find->y = 0; + find->first_char = 0; + find->length = z; + find->height = r.ymax - r.ymin; + find->x = r.x1; + return; + } + + // search rows to find the one that straddles character n + find->y = 0; + + for(;;) { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (n < i + r.num_chars) + break; + if (i + r.num_chars == z && z > 0 && STB_TEXTEDIT_GETCHAR(str, z - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] special handling for last line + break; // [DEAR IMGUI] + prev_start = i; + i += r.num_chars; + find->y += r.baseline_y_delta; + if (i == z) // [DEAR IMGUI] + { + r.num_chars = 0; // [DEAR IMGUI] + break; // [DEAR IMGUI] + } + } + + find->first_char = first = i; + find->length = r.num_chars; + find->height = r.ymax - r.ymin; + find->prev_first = prev_start; + + // now scan to find xpos + find->x = r.x0; + for (i=0; first+i < n; i = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, first + i) - first) + find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); +} + +#define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) + +// make the selection/cursor state valid if client altered the string +static void stb_textedit_clamp(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + int n = STB_TEXTEDIT_STRINGLEN(str); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start > n) state->select_start = n; + if (state->select_end > n) state->select_end = n; + // if clamping forced them to be equal, move the cursor to match + if (state->select_start == state->select_end) + state->cursor = state->select_start; + } + if (state->cursor > n) state->cursor = n; +} + +// delete characters while updating undo +static void stb_textedit_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) +{ + stb_text_makeundo_delete(str, state, where, len); + STB_TEXTEDIT_DELETECHARS(str, where, len); + state->has_preferred_x = 0; +} + +// delete the section +static void stb_textedit_delete_selection(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + stb_textedit_clamp(str, state); + if (STB_TEXT_HAS_SELECTION(state)) { + if (state->select_start < state->select_end) { + stb_textedit_delete(str, state, state->select_start, state->select_end - state->select_start); + state->select_end = state->cursor = state->select_start; + } else { + stb_textedit_delete(str, state, state->select_end, state->select_start - state->select_end); + state->select_start = state->cursor = state->select_end; + } + state->has_preferred_x = 0; + } +} + +// canoncialize the selection so start <= end +static void stb_textedit_sortselection(STB_TexteditState *state) +{ + if (state->select_end < state->select_start) { + int temp = state->select_end; + state->select_end = state->select_start; + state->select_start = temp; + } +} + +// move cursor to first character of selection +static void stb_textedit_move_to_first(STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + state->cursor = state->select_start; + state->select_end = state->select_start; + state->has_preferred_x = 0; + } +} + +// move cursor to last character of selection +static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_sortselection(state); + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->select_start = state->select_end; + state->has_preferred_x = 0; + } +} + +// [DEAR IMGUI] +// Functions must be implemented for UTF8 support +// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit. +// There is not necessarily a '[DEAR IMGUI]' at the usage sites. +#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx) (idx - 1) +#endif +#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) (idx + 1) +#endif + +#ifdef STB_TEXTEDIT_IS_SPACE +static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx ) +{ + return idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str,idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str, idx) ) ) : 1; +} + +#ifndef STB_TEXTEDIT_MOVEWORDLEFT +static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c ) +{ + --c; // always move at least one character + while( c >= 0 && !is_word_boundary( str, c ) ) + --c; + + if( c < 0 ) + c = 0; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDLEFT stb_textedit_move_to_word_previous +#endif + +#ifndef STB_TEXTEDIT_MOVEWORDRIGHT +static int stb_textedit_move_to_word_next( IMSTB_TEXTEDIT_STRING *str, int c ) +{ + const int len = STB_TEXTEDIT_STRINGLEN(str); + ++c; // always move at least one character + while( c < len && !is_word_boundary( str, c ) ) + ++c; + + if( c > len ) + c = len; + + return c; +} +#define STB_TEXTEDIT_MOVEWORDRIGHT stb_textedit_move_to_word_next +#endif + +#endif + +// update selection and cursor to match each other +static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) +{ + if (!STB_TEXT_HAS_SELECTION(state)) + state->select_start = state->select_end = state->cursor; + else + state->cursor = state->select_end; +} + +// API cut: delete selection +static int stb_textedit_cut(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + if (STB_TEXT_HAS_SELECTION(state)) { + stb_textedit_delete_selection(str,state); // implicitly clamps + state->has_preferred_x = 0; + return 1; + } + return 0; +} + +// API paste: replace existing selection with passed-in text +static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE *text, int len) +{ + // if there's a selection, the paste should delete it + stb_textedit_clamp(str, state); + stb_textedit_delete_selection(str,state); + // try to insert the characters + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, len)) { + stb_text_makeundo_insert(state, state->cursor, len); + state->cursor += len; + state->has_preferred_x = 0; + return 1; + } + // note: paste failure will leave deleted selection, may be restored with an undo (see https://github.com/nothings/stb/issues/734 for details) + return 0; +} + +#ifndef STB_TEXTEDIT_KEYTYPE +#define STB_TEXTEDIT_KEYTYPE int +#endif + +// [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility. +static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + // can't add newline in single-line mode + if (text[0] == '\n' && state->single_line) + return; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + state->cursor += text_len; + state->has_preferred_x = 0; + } + } + else { + stb_textedit_delete_selection(str, state); // implicitly clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + stb_text_makeundo_insert(state, state->cursor, text_len); + state->cursor += text_len; + state->has_preferred_x = 0; + } + } +} + +// API key: process a keyboard input +static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) +{ +retry: + switch (key) { + default: { +#ifdef STB_TEXTEDIT_KEYTOTEXT + int c = STB_TEXTEDIT_KEYTOTEXT(key); + if (c > 0) { + IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE)c; + stb_textedit_text(str, state, &ch, 1); + } +#endif + break; + } + +#ifdef STB_TEXTEDIT_K_INSERT + case STB_TEXTEDIT_K_INSERT: + state->insert_mode = !state->insert_mode; + break; +#endif + + case STB_TEXTEDIT_K_UNDO: + stb_text_undo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_REDO: + stb_text_redo(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT: + // if currently there's a selection, move cursor to start of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else + if (state->cursor > 0) + state->cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_RIGHT: + // if currently there's a selection, move cursor to end of selection + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); + stb_textedit_clamp(str, state); + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_LEFT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + // move selection left + if (state->select_end > 0) + state->select_end = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->select_end); + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_MOVEWORDLEFT + case STB_TEXTEDIT_K_WORDLEFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDLEFT(str, state->cursor); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + +#ifdef STB_TEXTEDIT_MOVEWORDRIGHT + case STB_TEXTEDIT_K_WORDRIGHT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + else { + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor); + stb_textedit_clamp( str, state ); + } + break; + + case STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT: + if( !STB_TEXT_HAS_SELECTION( state ) ) + stb_textedit_prep_selection_at_cursor(state); + + state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor); + state->select_end = state->cursor; + + stb_textedit_clamp( str, state ); + break; +#endif + + case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + // move selection right + state->select_end = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->select_end); + stb_textedit_clamp(str, state); + state->cursor = state->select_end; + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_DOWN: + case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGDOWN: + case STB_TEXTEDIT_K_PGDOWN | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, j, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGDOWN; + int row_count = is_page ? state->row_count_per_page : 1; + + if (!is_page && state->single_line) { + // on windows, up&down in single-line behave like left&right + key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_last(str, state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; + int start = find.first_char + find.length; + + if (find.length == 0) + break; + + // [DEAR IMGUI] + // going down while being on the last line shouldn't bring us to that line end + if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE) + break; + + // now find character position down a row + state->cursor = start; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + + // go to next line + find.first_char = find.first_char + find.length; + find.length = row.num_chars; + } + break; + } + + case STB_TEXTEDIT_K_UP: + case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGUP: + case STB_TEXTEDIT_K_PGUP | STB_TEXTEDIT_K_SHIFT: { + StbFindState find; + StbTexteditRow row; + int i, j, prev_scan, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGUP; + int row_count = is_page ? state->row_count_per_page : 1; + + if (!is_page && state->single_line) { + // on windows, up&down become left&right + key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); + goto retry; + } + + if (sel) + stb_textedit_prep_selection_at_cursor(state); + else if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_move_to_first(state); + + // compute current position of cursor point + stb_textedit_clamp(str, state); + stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); + + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; + + // can only go up if there's a previous row + if (find.prev_first == find.first_char) + break; + + // now find character position up a row + state->cursor = find.prev_first; + STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); + x = row.x0; + for (i=0; i < row.num_chars; ++i) { + float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) + break; + #endif + x += dx; + if (x > goal_x) + break; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); + } + stb_textedit_clamp(str, state); + + state->has_preferred_x = 1; + state->preferred_x = goal_x; + + if (sel) + state->select_end = state->cursor; + + // go to previous line + // (we need to scan previous line the hard way. maybe we could expose this as a new API function?) + prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0; + while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE) + --prev_scan; + find.first_char = find.prev_first; + find.prev_first = prev_scan; + } + break; + } + + case STB_TEXTEDIT_K_DELETE: + case STB_TEXTEDIT_K_DELETE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + int n = STB_TEXTEDIT_STRINGLEN(str); + if (state->cursor < n) + stb_textedit_delete(str, state, state->cursor, IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor) - state->cursor); + } + state->has_preferred_x = 0; + break; + + case STB_TEXTEDIT_K_BACKSPACE: + case STB_TEXTEDIT_K_BACKSPACE | STB_TEXTEDIT_K_SHIFT: + if (STB_TEXT_HAS_SELECTION(state)) + stb_textedit_delete_selection(str, state); + else { + stb_textedit_clamp(str, state); + if (state->cursor > 0) { + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + stb_textedit_delete(str, state, prev, state->cursor - prev); + state->cursor = prev; + } + } + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2: +#endif + case STB_TEXTEDIT_K_TEXTSTART: + state->cursor = state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2: +#endif + case STB_TEXTEDIT_K_TEXTEND: + state->cursor = STB_TEXTEDIT_STRINGLEN(str); + state->select_start = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTSTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = 0; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + case STB_TEXTEDIT_K_TEXTEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_TEXTEND | STB_TEXTEDIT_K_SHIFT: + stb_textedit_prep_selection_at_cursor(state); + state->cursor = state->select_end = STB_TEXTEDIT_STRINGLEN(str); + state->has_preferred_x = 0; + break; + + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2: +#endif + case STB_TEXTEDIT_K_LINESTART: + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + if (state->single_line) + state->cursor = 0; + else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2: +#endif + case STB_TEXTEDIT_K_LINEEND: { + int n = STB_TEXTEDIT_STRINGLEN(str); + stb_textedit_clamp(str, state); + stb_textedit_move_to_first(state); + if (state->single_line) + state->cursor = n; + else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) + ++state->cursor; + state->has_preferred_x = 0; + break; + } + +#ifdef STB_TEXTEDIT_K_LINESTART2 + case STB_TEXTEDIT_K_LINESTART2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT: + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + if (state->single_line) + state->cursor = 0; + else while (state->cursor > 0 && STB_TEXTEDIT_GETCHAR(str, state->cursor-1) != STB_TEXTEDIT_NEWLINE) + --state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + break; + +#ifdef STB_TEXTEDIT_K_LINEEND2 + case STB_TEXTEDIT_K_LINEEND2 | STB_TEXTEDIT_K_SHIFT: +#endif + case STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT: { + int n = STB_TEXTEDIT_STRINGLEN(str); + stb_textedit_clamp(str, state); + stb_textedit_prep_selection_at_cursor(state); + if (state->single_line) + state->cursor = n; + else while (state->cursor < n && STB_TEXTEDIT_GETCHAR(str, state->cursor) != STB_TEXTEDIT_NEWLINE) + ++state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + break; + } + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Undo processing +// +// @OPTIMIZE: the undo/redo buffer should be circular + +static void stb_textedit_flush_redo(StbUndoState *state) +{ + state->redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; +} + +// discard the oldest entry in the undo list +static void stb_textedit_discard_undo(StbUndoState *state) +{ + if (state->undo_point > 0) { + // if the 0th undo state has characters, clean those up + if (state->undo_rec[0].char_storage >= 0) { + int n = state->undo_rec[0].insert_length, i; + // delete n characters from all other records + state->undo_char_point -= n; + IMSTB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); + for (i=0; i < state->undo_point; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it + } + --state->undo_point; + IMSTB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); + } +} + +// discard the oldest entry in the redo list--it's bad if this +// ever happens, but because undo & redo have to store the actual +// characters in different cases, the redo character buffer can +// fill up even though the undo buffer didn't +static void stb_textedit_discard_redo(StbUndoState *state) +{ + int k = IMSTB_TEXTEDIT_UNDOSTATECOUNT-1; + + if (state->redo_point <= k) { + // if the k'th undo state has characters, clean those up + if (state->undo_rec[k].char_storage >= 0) { + int n = state->undo_rec[k].insert_length, i; + // move the remaining redo character data to the end of the buffer + state->redo_char_point += n; + IMSTB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((IMSTB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); + // adjust the position of all the other records to account for above memmove + for (i=state->redo_point; i < k; ++i) + if (state->undo_rec[i].char_storage >= 0) + state->undo_rec[i].char_storage += n; + } + // now move all the redo records towards the end of the buffer; the first one is at 'redo_point' + // [DEAR IMGUI] + size_t move_size = (size_t)((IMSTB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); + const char* buf_begin = (char*)state->undo_rec; (void)buf_begin; + const char* buf_end = (char*)state->undo_rec + sizeof(state->undo_rec); (void)buf_end; + IM_ASSERT(((char*)(state->undo_rec + state->redo_point)) >= buf_begin); + IM_ASSERT(((char*)(state->undo_rec + state->redo_point + 1) + move_size) <= buf_end); + IMSTB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); + + // now move redo_point to point to the new one + ++state->redo_point; + } +} + +static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numchars) +{ + // any time we create a new undo record, we discard redo + stb_textedit_flush_redo(state); + + // if we have no free records, we have to make room, by sliding the + // existing records down + if (state->undo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) + stb_textedit_discard_undo(state); + + // if the characters to store won't possibly fit in the buffer, we can't undo + if (numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) { + state->undo_point = 0; + state->undo_char_point = 0; + return NULL; + } + + // if we don't have enough free characters in the buffer, we have to make room + while (state->undo_char_point + numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) + stb_textedit_discard_undo(state); + + return &state->undo_rec[state->undo_point++]; +} + +static IMSTB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) +{ + StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); + if (r == NULL) + return NULL; + + r->where = pos; + r->insert_length = (IMSTB_TEXTEDIT_POSITIONTYPE) insert_len; + r->delete_length = (IMSTB_TEXTEDIT_POSITIONTYPE) delete_len; + + if (insert_len == 0) { + r->char_storage = -1; + return NULL; + } else { + r->char_storage = state->undo_char_point; + state->undo_char_point += insert_len; + return &state->undo_char[r->char_storage]; + } +} + +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord u, *r; + if (s->undo_point == 0) + return; + + // we need to do two things: apply the undo record, and create a redo record + u = s->undo_rec[s->undo_point-1]; + r = &s->undo_rec[s->redo_point-1]; + r->char_storage = -1; + + r->insert_length = u.delete_length; + r->delete_length = u.insert_length; + r->where = u.where; + + if (u.delete_length) { + // if the undo record says to delete characters, then the redo record will + // need to re-insert the characters that get deleted, so we need to store + // them. + + // there are three cases: + // there's enough room to store the characters + // characters stored for *redoing* don't leave room for redo + // characters stored for *undoing* don't leave room for redo + // if the last is true, we have to bail + + if (s->undo_char_point + u.delete_length >= IMSTB_TEXTEDIT_UNDOCHARCOUNT) { + // the undo records take up too much character space; there's no space to store the redo characters + r->insert_length = 0; + } else { + int i; + + // there's definitely room to store the characters eventually + while (s->undo_char_point + u.delete_length > s->redo_char_point) { + // should never happen: + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) + return; + // there's currently not enough room, so discard a redo record + stb_textedit_discard_redo(s); + } + r = &s->undo_rec[s->redo_point-1]; + + r->char_storage = s->redo_char_point - u.delete_length; + s->redo_char_point = s->redo_char_point - u.delete_length; + + // now save the characters + for (i=0; i < u.delete_length; ++i) + s->undo_char[r->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u.where + i); + } + + // now we can carry out the deletion + STB_TEXTEDIT_DELETECHARS(str, u.where, u.delete_length); + } + + // check type of recorded action: + if (u.insert_length) { + // easy case: was a deletion, so we need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, u.where, &s->undo_char[u.char_storage], u.insert_length); + s->undo_char_point -= u.insert_length; + } + + state->cursor = u.where + u.insert_length; + + s->undo_point--; + s->redo_point--; +} + +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) +{ + StbUndoState *s = &state->undostate; + StbUndoRecord *u, r; + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) + return; + + // we need to do two things: apply the redo record, and create an undo record + u = &s->undo_rec[s->undo_point]; + r = s->undo_rec[s->redo_point]; + + // we KNOW there must be room for the undo record, because the redo record + // was derived from an undo record + + u->delete_length = r.insert_length; + u->insert_length = r.delete_length; + u->where = r.where; + u->char_storage = -1; + + if (r.delete_length) { + // the redo record requires us to delete characters, so the undo record + // needs to store the characters + + if (s->undo_char_point + u->insert_length > s->redo_char_point) { + u->insert_length = 0; + u->delete_length = 0; + } else { + int i; + u->char_storage = s->undo_char_point; + s->undo_char_point = s->undo_char_point + u->insert_length; + + // now save the characters + for (i=0; i < u->insert_length; ++i) + s->undo_char[u->char_storage + i] = STB_TEXTEDIT_GETCHAR(str, u->where + i); + } + + STB_TEXTEDIT_DELETECHARS(str, r.where, r.delete_length); + } + + if (r.insert_length) { + // easy case: need to insert n characters + STB_TEXTEDIT_INSERTCHARS(str, r.where, &s->undo_char[r.char_storage], r.insert_length); + s->redo_char_point += r.insert_length; + } + + state->cursor = r.where + r.insert_length; + + s->undo_point++; + s->redo_point++; +} + +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length) +{ + stb_text_createundo(&state->undostate, where, 0, length); +} + +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) +{ + int i; + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + if (p) { + for (i=0; i < length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) +{ + int i; + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + if (p) { + for (i=0; i < old_length; ++i) + p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); + } +} + +// reset the state to default +static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_line) +{ + state->undostate.undo_point = 0; + state->undostate.undo_char_point = 0; + state->undostate.redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; + state->select_end = state->select_start = 0; + state->cursor = 0; + state->has_preferred_x = 0; + state->preferred_x = 0; + state->cursor_at_end_of_line = 0; + state->initialized = 1; + state->single_line = (unsigned char) is_single_line; + state->insert_mode = 0; + state->row_count_per_page = 0; +} + +// API initialize +static void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +{ + stb_textedit_clear_state(state, is_single_line); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +static int stb_textedit_paste(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE const *ctext, int len) +{ + return stb_textedit_paste_internal(str, state, (IMSTB_TEXTEDIT_CHARTYPE *) ctext, len); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif//IMSTB_TEXTEDIT_IMPLEMENTATION + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/thirdparty/imgui/imstb_truetype.h b/thirdparty/imgui/imstb_truetype.h new file mode 100644 index 00000000..976f09cb --- /dev/null +++ b/thirdparty/imgui/imstb_truetype.h @@ -0,0 +1,5085 @@ +// [DEAR IMGUI] +// This is a slightly modified version of stb_truetype.h 1.26. +// Mostly fixing for compiler and static analyzer warnings. +// Grep for [DEAR IMGUI] to find the changes. + +// stb_truetype.h - v1.26 - public domain +// authored from 2009-2021 by Sean Barrett / RAD Game Tools +// +// ======================================================================= +// +// NO SECURITY GUARANTEE -- DO NOT USE THIS ON UNTRUSTED FONT FILES +// +// This library does no range checking of the offsets found in the file, +// meaning an attacker can use it to read arbitrary memory. +// +// ======================================================================= +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// render glyphs to one-channel SDF bitmaps (signed-distance field/function) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// Dougall Johnson: OpenType / Type 2 font handling +// Daniel Ribeiro Maciel: basic GPOS-based kerning +// +// Misc other: +// Ryan Gordon +// Simon Glass +// github:IntellectualKitty +// Imanol Celaya +// Daniel Ribeiro Maciel +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket Fabian "ryg" Giesen github:NiLuJe +// Cass Everitt Martins Mozeiko github:aloucks +// stoiko (Haemimont Games) Cap Petschulat github:oyvindjam +// Brian Hook Omar Cornut github:vassvik +// Walter van Niftrik Ryan Griege +// David Gow Peter LaValle +// David Given Sergey Popov +// Ivan-Assen Ivanov Giumo X. Clanjor +// Anthony Pesch Higor Euripedes +// Johan Duparc Thomas Fields +// Hou Qiming Derek Vinyard +// Rob Loach Cort Stratton +// Kenney Phillis Jr. Brian Costabile +// Ken Voskuil (kaesve) +// +// VERSION HISTORY +// +// 1.26 (2021-08-28) fix broken rasterizer +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) GPOS kerning, STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// See end of file for license information. +// +// USAGE +// +// Include this file in whatever places need to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversampling() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- indexing for TTC font collections +// stbtt_GetNumberOfFonts() -- number of fonts for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetFontVMetricsOS2() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// DETAILED USAGE: +// +// Scale: +// Select how high you want the font to be, in points or pixels. +// Call ScaleForPixelHeight or ScaleForMappingEmToPixels to compute +// a scale factor SF that will be used by all other functions. +// +// Baseline: +// You need to select a y-coordinate that is the baseline of where +// your text will appear. Call GetFontBoundingBox to get the baseline-relative +// bounding box for all characters. SF*-y0 will be the distance in pixels +// that the worst-case character could extend above the baseline, so if +// you want the top edge of characters to appear at the top of the +// screen where y=0, then you would set the baseline to SF*-y0. +// +// Current point: +// Set the current point where the first character will appear. The +// first character could extend left of the current point; this is font +// dependent. You can either choose a current point that is the leftmost +// point and hope, or add some padding, or check the bounding box or +// left-side-bearing of the first character to be displayed and set +// the current point based on that. +// +// Displaying a character: +// Compute the bounding box of the character. It will contain signed values +// relative to . I.e. if it returns x0,y0,x1,y1, +// then the character should be displayed in the rectangle from +// to = 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype, e.g. if you don't +//// link with the C runtime library. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // e.g. #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + #ifndef STBTT_sqrt + #include + #define STBTT_sqrt(x) sqrt(x) + #define STBTT_pow(x,y) pow(x,y) + #endif + + #ifndef STBTT_fmod + #include + #define STBTT_fmod(x,y) fmod(x,y) + #endif + + #ifndef STBTT_cos + #include + #define STBTT_cos(x) cos(x) + #define STBTT_acos(x) acos(x) + #endif + + #ifndef STBTT_fabs + #include + #define STBTT_fabs(x) fabs(x) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) ((void)(u),malloc(x)) + #define STBTT_free(x,u) ((void)(u),free(x)) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef STBTT_STATIC +#define STBTT_DEF static +#else +#define STBTT_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// private structure +typedef struct +{ + unsigned char *data; + int cursor; + int size; +} stbtt__buf; + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap); +// Query the font vertical metrics without having to create a font first. + + +////////////////////////////////////////////////////////////////////////////// +// +// NEW TEXTURE BAKING API +// +// This provides options for packing multiple fonts into one atlas, not +// perfectly but better than nothing. + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; + float xoff2,yoff2; +} stbtt_packedchar; + +typedef struct stbtt_pack_context stbtt_pack_context; +typedef struct stbtt_fontinfo stbtt_fontinfo; +#ifndef STB_RECT_PACK_VERSION +typedef struct stbrp_rect stbrp_rect; +#endif + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int width, int height, int stride_in_bytes, int padding, void *alloc_context); +// Initializes a packing context stored in the passed-in stbtt_pack_context. +// Future calls using this context will pack characters into the bitmap passed +// in here: a 1-channel bitmap that is width * height. stride_in_bytes is +// the distance from one row to the next (or 0 to mean they are packed tightly +// together). "padding" is the amount of padding to leave between each +// character (normally you want '1' for bitmaps you'll use as textures with +// bilinear filtering). +// +// Returns 0 on failure, 1 on success. + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc); +// Cleans up the packing context and frees all memory. + +#define STBTT_POINT_SIZE(x) (-(x)) + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_char_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range); +// Creates character bitmaps from the font_index'th font found in fontdata (use +// font_index=0 if you don't know what that is). It creates num_chars_in_range +// bitmaps for characters with unicode values starting at first_unicode_char_in_range +// and increasing. Data for how to render them is stored in chardata_for_range; +// pass these to stbtt_GetPackedQuad to get back renderable quads. +// +// font_size is the full height of the character from ascender to descender, +// as computed by stbtt_ScaleForPixelHeight. To use a point size as computed +// by stbtt_ScaleForMappingEmToPixels, wrap the point size in STBTT_POINT_SIZE() +// and pass that result as 'font_size': +// ..., 20 , ... // font max minus min y is 20 pixels tall +// ..., STBTT_POINT_SIZE(20), ... // 'M' is 20 pixels tall + +typedef struct +{ + float font_size; + int first_unicode_codepoint_in_range; // if non-zero, then the chars are continuous, and this is the first codepoint + int *array_of_unicode_codepoints; // if non-zero, then this is an array of unicode codepoints + int num_chars; + stbtt_packedchar *chardata_for_range; // output + unsigned char h_oversample, v_oversample; // don't set these, they're used internally +} stbtt_pack_range; + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges); +// Creates character bitmaps from multiple ranges of characters stored in +// ranges. This will usually create a better-packed bitmap than multiple +// calls to stbtt_PackFontRange. Note that you can call this multiple +// times within a single PackBegin/PackEnd. + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample); +// Oversampling a font increases the quality by allowing higher-quality subpixel +// positioning, and is especially valuable at smaller text sizes. +// +// This function sets the amount of oversampling for all following calls to +// stbtt_PackFontRange(s) or stbtt_PackFontRangesGatherRects for a given +// pack context. The default (no oversampling) is achieved by h_oversample=1 +// and v_oversample=1. The total number of pixels required is +// h_oversample*v_oversample larger than the default; for example, 2x2 +// oversampling requires 4x the storage of 1x1. For best results, render +// oversampled textures with bilinear filtering. Look at the readme in +// stb/tests/oversample for information about oversampled fonts +// +// To use with PackFontRangesGather etc., you must set it before calls +// call to PackFontRangesGatherRects. + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip); +// If skip != 0, this tells stb_truetype to skip any codepoints for which +// there is no corresponding glyph. If skip=0, which is the default, then +// codepoints without a glyph received the font's "missing character" glyph, +// typically an empty box by convention. + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int align_to_integer); + +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects); +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects); +// Calling these functions in sequence is roughly equivalent to calling +// stbtt_PackFontRanges(). If you more control over the packing of multiple +// fonts, or if you want to pack custom data into a font texture, take a look +// at the source to of stbtt_PackFontRanges() and create a custom version +// using these functions, e.g. call GatherRects multiple times, +// building up a single array of rects, then call PackRects once, +// then call RenderIntoRects repeatedly. This may result in a +// better packing than calling PackFontRanges multiple times +// (or it may not). + +// this is an opaque structure that you shouldn't mess with which holds +// all the context needed from PackBegin to PackEnd. +struct stbtt_pack_context { + void *user_allocator_context; + void *pack_info; + int width; + int height; + int stride_in_bytes; + int padding; + int skip_missing; + unsigned int h_oversample, v_oversample; + unsigned char *pixels; + void *nodes; +}; + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data); +// This function will determine the number of fonts in a font file. TrueType +// collection (.ttc) files may contain multiple fonts, while TrueType font +// (.ttf) files only contain one font. The number of fonts can be used for +// indexing with the previous function where the index is between zero and one +// less than the total fonts. If an error occurs, -1 is returned. + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. + +// The following structure is defined publicly so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern,gpos,svg; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph + + stbtt__buf cff; // cff font data + stbtt__buf charstrings; // the charstring index + stbtt__buf gsubrs; // global charstring subroutines index + stbtt__buf subrs; // private charstring subroutines index + stbtt__buf fontdicts; // array of font dicts + stbtt__buf fdselect; // map from glyph to fontdict +}; + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. +// Returns 0 if the character codepoint is not defined in the font. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap); +// analogous to GetFontVMetrics, but returns the "typographic" values from the OS/2 +// table (specific to MS/Windows TTF files). +// +// Returns 1 on success (table present), 0 on failure. + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + +typedef struct stbtt_kerningentry +{ + int glyph1; // use stbtt_FindGlyphIndex + int glyph2; + int advance; +} stbtt_kerningentry; + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info); +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length); +// Retrieves a complete list of all of the kerning pairs provided by the font +// stbtt_GetKerningTable never writes more than table_length entries and returns how many entries it did write. +// The table will be sorted by (a.glyph1 == b.glyph1)?(a.glyph2 < b.glyph2):(a.glyph1 < b.glyph1) + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve, + STBTT_vcubic + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy,cx1,cy1; + unsigned char type,padding; + } stbtt_vertex; +#endif + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates +// +// The shape is a series of contours. Each one starts with +// a STBTT_moveto, then consists of a series of mixed +// STBTT_lineto and STBTT_curveto segments. A lineto +// draws a line from previous endpoint to its x,y; a curveto +// draws a quadratic bezier from previous endpoint to +// its x,y, using cx,cy as the bezier control point. + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +STBTT_DEF unsigned char *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl); +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg); +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg); +// fills svg with the character's SVG data. +// returns data size or 0 if SVG not found. + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint); +// same as stbtt_MakeCodepointBitmapSubpixel, but prefiltering +// is performed (see stbtt_PackSetOversampling) + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int glyph); +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +// rasterize a shape with quadratic beziers into a bitmap +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, // 1-channel bitmap to draw into + float flatness_in_pixels, // allowable error of curve in pixels + stbtt_vertex *vertices, // array of vertices defining shape + int num_verts, // number of vertices in above array + float scale_x, float scale_y, // scale applied to input vertices + float shift_x, float shift_y, // translation applied to input vertices + int x_off, int y_off, // another translation applied to input + int invert, // if non-zero, vertically flip shape + void *userdata); // context for to STBTT_MALLOC + +////////////////////////////////////////////////////////////////////////////// +// +// Signed Distance Function (or Field) rendering + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata); +// frees the SDF bitmap allocated below + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff); +// These functions compute a discretized SDF field for a single character, suitable for storing +// in a single-channel texture, sampling with bilinear filtering, and testing against +// larger than some threshold to produce scalable fonts. +// info -- the font +// scale -- controls the size of the resulting SDF bitmap, same as it would be creating a regular bitmap +// glyph/codepoint -- the character to generate the SDF for +// padding -- extra "pixels" around the character which are filled with the distance to the character (not 0), +// which allows effects like bit outlines +// onedge_value -- value 0-255 to test the SDF against to reconstruct the character (i.e. the isocontour of the character) +// pixel_dist_scale -- what value the SDF should increase by when moving one SDF "pixel" away from the edge (on the 0..255 scale) +// if positive, > onedge_value is inside; if negative, < onedge_value is inside +// width,height -- output height & width of the SDF bitmap (including padding) +// xoff,yoff -- output origin of the character +// return value -- a 2D array of bytes 0..255, width*height in size +// +// pixel_dist_scale & onedge_value are a scale & bias that allows you to make +// optimal use of the limited 0..255 for your application, trading off precision +// and special effects. SDF values outside the range 0..255 are clamped to 0..255. +// +// Example: +// scale = stbtt_ScaleForPixelHeight(22) +// padding = 5 +// onedge_value = 180 +// pixel_dist_scale = 180/5.0 = 36.0 +// +// This will create an SDF bitmap in which the character is about 22 pixels +// high but the whole bitmap is about 22+5+5=32 pixels high. To produce a filled +// shape, sample the SDF at each pixel and fill the pixel if the SDF value +// is greater than or equal to 180/255. (You'll actually want to antialias, +// which is beyond the scope of this example.) Additionally, you can compute +// offset outlines (e.g. to stroke the character border inside & outside, +// or only outside). For example, to fill outside the character up to 3 SDF +// pixels, you would compare against (180-36.0*3)/255 = 72/255. The above +// choice of variables maps a range from 5 pixels outside the shape to +// 2 pixels inside the shape to 0..255; this is intended primarily for apply +// outside effects only (the interior range is needed to allow proper +// antialiasing of the font at *smaller* sizes) +// +// The function computes the SDF analytically at each SDF pixel, not by e.g. +// building a higher-res bitmap and approximating it. In theory the quality +// should be as high as possible for an SDF of this size & representation, but +// unclear if this is true in practice (perhaps building a higher-res bitmap +// and computing from that can allow drop-out prevention). +// +// The algorithm has not been optimized at all, so expect it to be slow +// if computing lots of characters or very large sizes. + + + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#endif + +#if STBTT_MAX_OVERSAMPLE > 255 +#error "STBTT_MAX_OVERSAMPLE cannot be > 255" +#endif + +typedef int stbtt__test_oversample_pow2[(STBTT_MAX_OVERSAMPLE & (STBTT_MAX_OVERSAMPLE-1)) == 0 ? 1 : -1]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +#ifdef _MSC_VER +#define STBTT__NOTUSED(v) (void)(v) +#else +#define STBTT__NOTUSED(v) (void)sizeof(v) +#endif + +////////////////////////////////////////////////////////////////////////// +// +// stbtt__buf helpers to parse data from file +// + +static stbtt_uint8 stbtt__buf_get8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor++]; +} + +static stbtt_uint8 stbtt__buf_peek8(stbtt__buf *b) +{ + if (b->cursor >= b->size) + return 0; + return b->data[b->cursor]; +} + +static void stbtt__buf_seek(stbtt__buf *b, int o) +{ + STBTT_assert(!(o > b->size || o < 0)); + b->cursor = (o > b->size || o < 0) ? b->size : o; +} + +static void stbtt__buf_skip(stbtt__buf *b, int o) +{ + stbtt__buf_seek(b, b->cursor + o); +} + +static stbtt_uint32 stbtt__buf_get(stbtt__buf *b, int n) +{ + stbtt_uint32 v = 0; + int i; + STBTT_assert(n >= 1 && n <= 4); + for (i = 0; i < n; i++) + v = (v << 8) | stbtt__buf_get8(b); + return v; +} + +static stbtt__buf stbtt__new_buf(const void *p, size_t size) +{ + stbtt__buf r; + STBTT_assert(size < 0x40000000); + r.data = (stbtt_uint8*) p; + r.size = (int) size; + r.cursor = 0; + return r; +} + +#define stbtt__buf_get16(b) stbtt__buf_get((b), 2) +#define stbtt__buf_get32(b) stbtt__buf_get((b), 4) + +static stbtt__buf stbtt__buf_range(const stbtt__buf *b, int o, int s) +{ + stbtt__buf r = stbtt__new_buf(NULL, 0); + if (o < 0 || s < 0 || o > b->size || s > b->size - o) return r; + r.data = b->data + o; + r.size = s; + return r; +} + +static stbtt__buf stbtt__cff_get_index(stbtt__buf *b) +{ + int count, start, offsize; + start = b->cursor; + count = stbtt__buf_get16(b); + if (count) { + offsize = stbtt__buf_get8(b); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(b, offsize * count); + stbtt__buf_skip(b, stbtt__buf_get(b, offsize) - 1); + } + return stbtt__buf_range(b, start, b->cursor - start); +} + +static stbtt_uint32 stbtt__cff_int(stbtt__buf *b) +{ + int b0 = stbtt__buf_get8(b); + if (b0 >= 32 && b0 <= 246) return b0 - 139; + else if (b0 >= 247 && b0 <= 250) return (b0 - 247)*256 + stbtt__buf_get8(b) + 108; + else if (b0 >= 251 && b0 <= 254) return -(b0 - 251)*256 - stbtt__buf_get8(b) - 108; + else if (b0 == 28) return stbtt__buf_get16(b); + else if (b0 == 29) return stbtt__buf_get32(b); + STBTT_assert(0); + return 0; +} + +static void stbtt__cff_skip_operand(stbtt__buf *b) { + int v, b0 = stbtt__buf_peek8(b); + STBTT_assert(b0 >= 28); + if (b0 == 30) { + stbtt__buf_skip(b, 1); + while (b->cursor < b->size) { + v = stbtt__buf_get8(b); + if ((v & 0xF) == 0xF || (v >> 4) == 0xF) + break; + } + } else { + stbtt__cff_int(b); + } +} + +static stbtt__buf stbtt__dict_get(stbtt__buf *b, int key) +{ + stbtt__buf_seek(b, 0); + while (b->cursor < b->size) { + int start = b->cursor, end, op; + while (stbtt__buf_peek8(b) >= 28) + stbtt__cff_skip_operand(b); + end = b->cursor; + op = stbtt__buf_get8(b); + if (op == 12) op = stbtt__buf_get8(b) | 0x100; + if (op == key) return stbtt__buf_range(b, start, end-start); + } + return stbtt__buf_range(b, 0, 0); +} + +static void stbtt__dict_get_ints(stbtt__buf *b, int key, int outcount, stbtt_uint32 *out) +{ + int i; + stbtt__buf operands = stbtt__dict_get(b, key); + for (i = 0; i < outcount && operands.cursor < operands.size; i++) + out[i] = stbtt__cff_int(&operands); +} + +static int stbtt__cff_index_count(stbtt__buf *b) +{ + stbtt__buf_seek(b, 0); + return stbtt__buf_get16(b); +} + +static stbtt__buf stbtt__cff_index_get(stbtt__buf b, int i) +{ + int count, offsize, start, end; + stbtt__buf_seek(&b, 0); + count = stbtt__buf_get16(&b); + offsize = stbtt__buf_get8(&b); + STBTT_assert(i >= 0 && i < count); + STBTT_assert(offsize >= 1 && offsize <= 4); + stbtt__buf_skip(&b, i*offsize); + start = stbtt__buf_get(&b, offsize); + end = stbtt__buf_get(&b, offsize); + return stbtt__buf_range(&b, 2+(count+1)*offsize+start, end - start); +} + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +static stbtt_uint16 ttUSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_int16 ttSHORT(stbtt_uint8 *p) { return p[0]*256 + p[1]; } +static stbtt_uint32 ttULONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } +static stbtt_int32 ttLONG(stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + if (stbtt_tag(font, "true")) return 1; // Apple specification for TrueType fonts + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +static int stbtt_GetFontOffsetForIndex_internal(unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*4); + } + } + return -1; +} + +static int stbtt_GetNumberOfFonts_internal(unsigned char *font_collection) +{ + // if it's just a font, there's only one valid font + if (stbtt__isfont(font_collection)) + return 1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + return ttLONG(font_collection+8); + } + } + return 0; +} + +static stbtt__buf stbtt__get_subrs(stbtt__buf cff, stbtt__buf fontdict) +{ + stbtt_uint32 subrsoff = 0, private_loc[2] = { 0, 0 }; + stbtt__buf pdict; + stbtt__dict_get_ints(&fontdict, 18, 2, private_loc); + if (!private_loc[1] || !private_loc[0]) return stbtt__new_buf(NULL, 0); + pdict = stbtt__buf_range(&cff, private_loc[1], private_loc[0]); + stbtt__dict_get_ints(&pdict, 19, 1, &subrsoff); + if (!subrsoff) return stbtt__new_buf(NULL, 0); + stbtt__buf_seek(&cff, private_loc[1]+subrsoff); + return stbtt__cff_get_index(&cff); +} + +// since most people won't use this, find this table the first time it's needed +static int stbtt__get_svg(stbtt_fontinfo *info) +{ + stbtt_uint32 t; + if (info->svg < 0) { + t = stbtt__find_table(info->data, info->fontstart, "SVG "); + if (t) { + stbtt_uint32 offset = ttULONG(info->data + t + 2); + info->svg = t + offset; + } else { + info->svg = 0; + } + } + return info->svg; +} + +static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, int fontstart) +{ + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + info->cff = stbtt__new_buf(NULL, 0); + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + info->gpos = stbtt__find_table(data, fontstart, "GPOS"); // not required + + if (!cmap || !info->head || !info->hhea || !info->hmtx) + return 0; + if (info->glyf) { + // required for truetype + if (!info->loca) return 0; + } else { + // initialization for CFF / Type2 fonts (OTF) + stbtt__buf b, topdict, topdictidx; + stbtt_uint32 cstype = 2, charstrings = 0, fdarrayoff = 0, fdselectoff = 0; + stbtt_uint32 cff; + + cff = stbtt__find_table(data, fontstart, "CFF "); + if (!cff) return 0; + + info->fontdicts = stbtt__new_buf(NULL, 0); + info->fdselect = stbtt__new_buf(NULL, 0); + + // @TODO this should use size from table (not 512MB) + info->cff = stbtt__new_buf(data+cff, 512*1024*1024); + b = info->cff; + + // read the header + stbtt__buf_skip(&b, 2); + stbtt__buf_seek(&b, stbtt__buf_get8(&b)); // hdrsize + + // @TODO the name INDEX could list multiple fonts, + // but we just use the first one. + stbtt__cff_get_index(&b); // name INDEX + topdictidx = stbtt__cff_get_index(&b); + topdict = stbtt__cff_index_get(topdictidx, 0); + stbtt__cff_get_index(&b); // string INDEX + info->gsubrs = stbtt__cff_get_index(&b); + + stbtt__dict_get_ints(&topdict, 17, 1, &charstrings); + stbtt__dict_get_ints(&topdict, 0x100 | 6, 1, &cstype); + stbtt__dict_get_ints(&topdict, 0x100 | 36, 1, &fdarrayoff); + stbtt__dict_get_ints(&topdict, 0x100 | 37, 1, &fdselectoff); + info->subrs = stbtt__get_subrs(b, topdict); + + // we only support Type 2 charstrings + if (cstype != 2) return 0; + if (charstrings == 0) return 0; + + if (fdarrayoff) { + // looks like a CID font + if (!fdselectoff) return 0; + stbtt__buf_seek(&b, fdarrayoff); + info->fontdicts = stbtt__cff_get_index(&b); + info->fdselect = stbtt__buf_range(&b, fdselectoff, b.size-fdselectoff); + } + + stbtt__buf_seek(&b, charstrings); + info->charstrings = stbtt__cff_get_index(&b); + } + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + info->svg = -1; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + case STBTT_PLATFORM_ID_UNICODE: + // Mac/iOS has these + // all the encodingIDs are unicode, so we don't bother to check it + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 end; + searchRange >>= 1; + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + { + stbtt_uint16 offset, start, last; + stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); + + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + last = ttUSHORT(data + endCount + 2*item); + if (unicode_codepoint < start || unicode_codepoint > last) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +STBTT_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + STBTT_assert(!info->cff.size); + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); + +STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + if (info->cff.size) { + stbtt__GetGlyphInfoT2(info, glyph_index, x0, y0, x1, y1); + } else { + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + } + return 1; +} + +STBTT_DEF int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +STBTT_DEF int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g; + if (info->cff.size) + return stbtt__GetGlyphInfoT2(info, glyph_index, NULL, NULL, NULL, NULL) == 0; + g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours < 0) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0 && vertices) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); + STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +typedef struct +{ + int bounds; + int started; + float first_x, first_y; + float x, y; + stbtt_int32 min_x, max_x, min_y, max_y; + + stbtt_vertex *pvertices; + int num_vertices; +} stbtt__csctx; + +#define STBTT__CSCTX_INIT(bounds) {bounds,0, 0,0, 0,0, 0,0,0,0, NULL, 0} + +static void stbtt__track_vertex(stbtt__csctx *c, stbtt_int32 x, stbtt_int32 y) +{ + if (x > c->max_x || !c->started) c->max_x = x; + if (y > c->max_y || !c->started) c->max_y = y; + if (x < c->min_x || !c->started) c->min_x = x; + if (y < c->min_y || !c->started) c->min_y = y; + c->started = 1; +} + +static void stbtt__csctx_v(stbtt__csctx *c, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy, stbtt_int32 cx1, stbtt_int32 cy1) +{ + if (c->bounds) { + stbtt__track_vertex(c, x, y); + if (type == STBTT_vcubic) { + stbtt__track_vertex(c, cx, cy); + stbtt__track_vertex(c, cx1, cy1); + } + } else { + stbtt_setvertex(&c->pvertices[c->num_vertices], type, x, y, cx, cy); + c->pvertices[c->num_vertices].cx1 = (stbtt_int16) cx1; + c->pvertices[c->num_vertices].cy1 = (stbtt_int16) cy1; + } + c->num_vertices++; +} + +static void stbtt__csctx_close_shape(stbtt__csctx *ctx) +{ + if (ctx->first_x != ctx->x || ctx->first_y != ctx->y) + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->first_x, (int)ctx->first_y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rmove_to(stbtt__csctx *ctx, float dx, float dy) +{ + stbtt__csctx_close_shape(ctx); + ctx->first_x = ctx->x = ctx->x + dx; + ctx->first_y = ctx->y = ctx->y + dy; + stbtt__csctx_v(ctx, STBTT_vmove, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rline_to(stbtt__csctx *ctx, float dx, float dy) +{ + ctx->x += dx; + ctx->y += dy; + stbtt__csctx_v(ctx, STBTT_vline, (int)ctx->x, (int)ctx->y, 0, 0, 0, 0); +} + +static void stbtt__csctx_rccurve_to(stbtt__csctx *ctx, float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) +{ + float cx1 = ctx->x + dx1; + float cy1 = ctx->y + dy1; + float cx2 = cx1 + dx2; + float cy2 = cy1 + dy2; + ctx->x = cx2 + dx3; + ctx->y = cy2 + dy3; + stbtt__csctx_v(ctx, STBTT_vcubic, (int)ctx->x, (int)ctx->y, (int)cx1, (int)cy1, (int)cx2, (int)cy2); +} + +static stbtt__buf stbtt__get_subr(stbtt__buf idx, int n) +{ + int count = stbtt__cff_index_count(&idx); + int bias = 107; + if (count >= 33900) + bias = 32768; + else if (count >= 1240) + bias = 1131; + n += bias; + if (n < 0 || n >= count) + return stbtt__new_buf(NULL, 0); + return stbtt__cff_index_get(idx, n); +} + +static stbtt__buf stbtt__cid_get_glyph_subrs(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt__buf fdselect = info->fdselect; + int nranges, start, end, v, fmt, fdselector = -1, i; + + stbtt__buf_seek(&fdselect, 0); + fmt = stbtt__buf_get8(&fdselect); + if (fmt == 0) { + // untested + stbtt__buf_skip(&fdselect, glyph_index); + fdselector = stbtt__buf_get8(&fdselect); + } else if (fmt == 3) { + nranges = stbtt__buf_get16(&fdselect); + start = stbtt__buf_get16(&fdselect); + for (i = 0; i < nranges; i++) { + v = stbtt__buf_get8(&fdselect); + end = stbtt__buf_get16(&fdselect); + if (glyph_index >= start && glyph_index < end) { + fdselector = v; + break; + } + start = end; + } + } + if (fdselector == -1) return stbtt__new_buf(NULL, 0); // [DEAR IMGUI] fixed, see #6007 and nothings/stb#1422 + return stbtt__get_subrs(info->cff, stbtt__cff_index_get(info->fontdicts, fdselector)); +} + +static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, stbtt__csctx *c) +{ + int in_header = 1, maskbits = 0, subr_stack_height = 0, sp = 0, v, i, b0; + int has_subrs = 0, clear_stack; + float s[48]; + stbtt__buf subr_stack[10], subrs = info->subrs, b; + float f; + +#define STBTT__CSERR(s) (0) + + // this currently ignores the initial width value, which isn't needed if we have hmtx + b = stbtt__cff_index_get(info->charstrings, glyph_index); + while (b.cursor < b.size) { + i = 0; + clear_stack = 1; + b0 = stbtt__buf_get8(&b); + switch (b0) { + // @TODO implement hinting + case 0x13: // hintmask + case 0x14: // cntrmask + if (in_header) + maskbits += (sp / 2); // implicit "vstem" + in_header = 0; + stbtt__buf_skip(&b, (maskbits + 7) / 8); + break; + + case 0x01: // hstem + case 0x03: // vstem + case 0x12: // hstemhm + case 0x17: // vstemhm + maskbits += (sp / 2); + break; + + case 0x15: // rmoveto + in_header = 0; + if (sp < 2) return STBTT__CSERR("rmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-2], s[sp-1]); + break; + case 0x04: // vmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("vmoveto stack"); + stbtt__csctx_rmove_to(c, 0, s[sp-1]); + break; + case 0x16: // hmoveto + in_header = 0; + if (sp < 1) return STBTT__CSERR("hmoveto stack"); + stbtt__csctx_rmove_to(c, s[sp-1], 0); + break; + + case 0x05: // rlineto + if (sp < 2) return STBTT__CSERR("rlineto stack"); + for (; i + 1 < sp; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + // hlineto/vlineto and vhcurveto/hvcurveto alternate horizontal and vertical + // starting from a different place. + + case 0x07: // vlineto + if (sp < 1) return STBTT__CSERR("vlineto stack"); + goto vlineto; + case 0x06: // hlineto + if (sp < 1) return STBTT__CSERR("hlineto stack"); + for (;;) { + if (i >= sp) break; + stbtt__csctx_rline_to(c, s[i], 0); + i++; + vlineto: + if (i >= sp) break; + stbtt__csctx_rline_to(c, 0, s[i]); + i++; + } + break; + + case 0x1F: // hvcurveto + if (sp < 4) return STBTT__CSERR("hvcurveto stack"); + goto hvcurveto; + case 0x1E: // vhcurveto + if (sp < 4) return STBTT__CSERR("vhcurveto stack"); + for (;;) { + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], (sp - i == 5) ? s[i + 4] : 0.0f); + i += 4; + hvcurveto: + if (i + 3 >= sp) break; + stbtt__csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], (sp - i == 5) ? s[i+4] : 0.0f, s[i+3]); + i += 4; + } + break; + + case 0x08: // rrcurveto + if (sp < 6) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x18: // rcurveline + if (sp < 8) return STBTT__CSERR("rcurveline stack"); + for (; i + 5 < sp - 2; i += 6) + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + if (i + 1 >= sp) return STBTT__CSERR("rcurveline stack"); + stbtt__csctx_rline_to(c, s[i], s[i+1]); + break; + + case 0x19: // rlinecurve + if (sp < 8) return STBTT__CSERR("rlinecurve stack"); + for (; i + 1 < sp - 6; i += 2) + stbtt__csctx_rline_to(c, s[i], s[i+1]); + if (i + 5 >= sp) return STBTT__CSERR("rlinecurve stack"); + stbtt__csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5]); + break; + + case 0x1A: // vvcurveto + case 0x1B: // hhcurveto + if (sp < 4) return STBTT__CSERR("(vv|hh)curveto stack"); + f = 0.0; + if (sp & 1) { f = s[i]; i++; } + for (; i + 3 < sp; i += 4) { + if (b0 == 0x1B) + stbtt__csctx_rccurve_to(c, s[i], f, s[i+1], s[i+2], s[i+3], 0.0); + else + stbtt__csctx_rccurve_to(c, f, s[i], s[i+1], s[i+2], 0.0, s[i+3]); + f = 0.0; + } + break; + + case 0x0A: // callsubr + if (!has_subrs) { + if (info->fdselect.size) + subrs = stbtt__cid_get_glyph_subrs(info, glyph_index); + has_subrs = 1; + } + // FALLTHROUGH + case 0x1D: // callgsubr + if (sp < 1) return STBTT__CSERR("call(g|)subr stack"); + v = (int) s[--sp]; + if (subr_stack_height >= 10) return STBTT__CSERR("recursion limit"); + subr_stack[subr_stack_height++] = b; + b = stbtt__get_subr(b0 == 0x0A ? subrs : info->gsubrs, v); + if (b.size == 0) return STBTT__CSERR("subr not found"); + b.cursor = 0; + clear_stack = 0; + break; + + case 0x0B: // return + if (subr_stack_height <= 0) return STBTT__CSERR("return outside subr"); + b = subr_stack[--subr_stack_height]; + clear_stack = 0; + break; + + case 0x0E: // endchar + stbtt__csctx_close_shape(c); + return 1; + + case 0x0C: { // two-byte escape + float dx1, dx2, dx3, dx4, dx5, dx6, dy1, dy2, dy3, dy4, dy5, dy6; + float dx, dy; + int b1 = stbtt__buf_get8(&b); + switch (b1) { + // @TODO These "flex" implementations ignore the flex-depth and resolution, + // and always draw beziers. + case 0x22: // hflex + if (sp < 7) return STBTT__CSERR("hflex stack"); + dx1 = s[0]; + dx2 = s[1]; + dy2 = s[2]; + dx3 = s[3]; + dx4 = s[4]; + dx5 = s[5]; + dx6 = s[6]; + stbtt__csctx_rccurve_to(c, dx1, 0, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, -dy2, dx6, 0); + break; + + case 0x23: // flex + if (sp < 13) return STBTT__CSERR("flex stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = s[10]; + dy6 = s[11]; + //fd is s[12] + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + case 0x24: // hflex1 + if (sp < 9) return STBTT__CSERR("hflex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dx4 = s[5]; + dx5 = s[6]; + dy5 = s[7]; + dx6 = s[8]; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, 0); + stbtt__csctx_rccurve_to(c, dx4, 0, dx5, dy5, dx6, -(dy1+dy2+dy5)); + break; + + case 0x25: // flex1 + if (sp < 11) return STBTT__CSERR("flex1 stack"); + dx1 = s[0]; + dy1 = s[1]; + dx2 = s[2]; + dy2 = s[3]; + dx3 = s[4]; + dy3 = s[5]; + dx4 = s[6]; + dy4 = s[7]; + dx5 = s[8]; + dy5 = s[9]; + dx6 = dy6 = s[10]; + dx = dx1+dx2+dx3+dx4+dx5; + dy = dy1+dy2+dy3+dy4+dy5; + if (STBTT_fabs(dx) > STBTT_fabs(dy)) + dy6 = -dy; + else + dx6 = -dx; + stbtt__csctx_rccurve_to(c, dx1, dy1, dx2, dy2, dx3, dy3); + stbtt__csctx_rccurve_to(c, dx4, dy4, dx5, dy5, dx6, dy6); + break; + + default: + return STBTT__CSERR("unimplemented"); + } + } break; + + default: + if (b0 != 255 && b0 != 28 && b0 < 32) + return STBTT__CSERR("reserved operator"); + + // push immediate + if (b0 == 255) { + f = (float)(stbtt_int32)stbtt__buf_get32(&b) / 0x10000; + } else { + stbtt__buf_skip(&b, -1); + f = (float)(stbtt_int16)stbtt__cff_int(&b); + } + if (sp >= 48) return STBTT__CSERR("push stack overflow"); + s[sp++] = f; + clear_stack = 0; + break; + } + if (clear_stack) sp = 0; + } + return STBTT__CSERR("no endchar"); + +#undef STBTT__CSERR +} + +static int stbtt__GetGlyphShapeT2(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + // runs the charstring twice, once to count and once to output (to avoid realloc) + stbtt__csctx count_ctx = STBTT__CSCTX_INIT(1); + stbtt__csctx output_ctx = STBTT__CSCTX_INIT(0); + if (stbtt__run_charstring(info, glyph_index, &count_ctx)) { + *pvertices = (stbtt_vertex*)STBTT_malloc(count_ctx.num_vertices*sizeof(stbtt_vertex), info->userdata); + output_ctx.pvertices = *pvertices; + if (stbtt__run_charstring(info, glyph_index, &output_ctx)) { + STBTT_assert(output_ctx.num_vertices == count_ctx.num_vertices); + return output_ctx.num_vertices; + } + } + *pvertices = NULL; + return 0; +} + +static int stbtt__GetGlyphInfoT2(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + stbtt__csctx c = STBTT__CSCTX_INIT(1); + int r = stbtt__run_charstring(info, glyph_index, &c); + if (x0) *x0 = r ? c.min_x : 0; + if (y0) *y0 = r ? c.min_y : 0; + if (x1) *x1 = r ? c.max_x : 0; + if (y1) *y1 = r ? c.max_y : 0; + return r ? c.num_vertices : 0; +} + +STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + if (!info->cff.size) + return stbtt__GetGlyphShapeTT(info, glyph_index, pvertices); + else + return stbtt__GetGlyphShapeT2(info, glyph_index, pvertices); +} + +STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info) +{ + stbtt_uint8 *data = info->data + info->kern; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + return ttUSHORT(data+10); +} + +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length) +{ + stbtt_uint8 *data = info->data + info->kern; + int k, length; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + length = ttUSHORT(data+10); + if (table_length < length) + length = table_length; + + for (k = 0; k < length; k++) + { + table[k].glyph1 = ttUSHORT(data+18+(k*6)); + table[k].glyph2 = ttUSHORT(data+20+(k*6)); + table[k].advance = ttSHORT(data+22+(k*6)); + } + + return length; +} + +static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) +{ + stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); + switch (coverageFormat) { + case 1: { + stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); + + // Binary search. + stbtt_int32 l=0, r=glyphCount-1, m; + int straw, needle=glyph; + while (l <= r) { + stbtt_uint8 *glyphArray = coverageTable + 4; + stbtt_uint16 glyphID; + m = (l + r) >> 1; + glyphID = ttUSHORT(glyphArray + 2 * m); + straw = glyphID; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + return m; + } + } + break; + } + + case 2: { + stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); + stbtt_uint8 *rangeArray = coverageTable + 4; + + // Binary search. + stbtt_int32 l=0, r=rangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *rangeRecord; + m = (l + r) >> 1; + rangeRecord = rangeArray + 6 * m; + strawStart = ttUSHORT(rangeRecord); + strawEnd = ttUSHORT(rangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else { + stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); + return startCoverageIndex + glyph - strawStart; + } + } + break; + } + + default: return -1; // unsupported + } + + return -1; +} + +static stbtt_int32 stbtt__GetGlyphClass(stbtt_uint8 *classDefTable, int glyph) +{ + stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); + switch (classDefFormat) + { + case 1: { + stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); + stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); + stbtt_uint8 *classDef1ValueArray = classDefTable + 6; + + if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) + return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + break; + } + + case 2: { + stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); + stbtt_uint8 *classRangeRecords = classDefTable + 4; + + // Binary search. + stbtt_int32 l=0, r=classRangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *classRangeRecord; + m = (l + r) >> 1; + classRangeRecord = classRangeRecords + 6 * m; + strawStart = ttUSHORT(classRangeRecord); + strawEnd = ttUSHORT(classRangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else + return (stbtt_int32)ttUSHORT(classRangeRecord + 4); + } + break; + } + + default: + return -1; // Unsupported definition type, return an error. + } + + // "All glyphs not assigned to a class fall into class 0". (OpenType spec) + return 0; +} + +// Define to STBTT_assert(x) if you want to break on unimplemented formats. +#define STBTT_GPOS_TODO_assert(x) + +static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint16 lookupListOffset; + stbtt_uint8 *lookupList; + stbtt_uint16 lookupCount; + stbtt_uint8 *data; + stbtt_int32 i, sti; + + if (!info->gpos) return 0; + + data = info->data + info->gpos; + + if (ttUSHORT(data+0) != 1) return 0; // Major version 1 + if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 + + lookupListOffset = ttUSHORT(data+8); + lookupList = data + lookupListOffset; + lookupCount = ttUSHORT(lookupList); + + for (i=0; i= pairSetCount) return 0; + + needle=glyph2; + r=pairValueCount-1; + l=0; + + // Binary search. + while (l <= r) { + stbtt_uint16 secondGlyph; + stbtt_uint8 *pairValue; + m = (l + r) >> 1; + pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; + secondGlyph = ttUSHORT(pairValue); + straw = secondGlyph; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + stbtt_int16 xAdvance = ttSHORT(pairValue + 2); + return xAdvance; + } + } + } else + return 0; + break; + } + + case 2: { + stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); + stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + if (valueFormat1 == 4 && valueFormat2 == 0) { // Support more formats? + stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); + stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); + int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); + int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); + + stbtt_uint16 class1Count = ttUSHORT(table + 12); + stbtt_uint16 class2Count = ttUSHORT(table + 14); + stbtt_uint8 *class1Records, *class2Records; + stbtt_int16 xAdvance; + + if (glyph1class < 0 || glyph1class >= class1Count) return 0; // malformed + if (glyph2class < 0 || glyph2class >= class2Count) return 0; // malformed + + class1Records = table + 16; + class2Records = class1Records + 2 * (glyph1class * class2Count); + xAdvance = ttSHORT(class2Records + 2 * glyph2class); + return xAdvance; + } else + return 0; + break; + } + + default: + return 0; // Unsupported position format + } + } + } + + return 0; +} + +STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int g2) +{ + int xAdvance = 0; + + if (info->gpos) + xAdvance += stbtt__GetGlyphGPOSInfoAdvance(info, g1, g2); + else if (info->kern) + xAdvance += stbtt__GetGlyphKernInfoAdvance(info, g1, g2); + + return xAdvance; +} + +STBTT_DEF int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern && !info->gpos) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +STBTT_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +STBTT_DEF void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +STBTT_DEF int stbtt_GetFontVMetricsOS2(const stbtt_fontinfo *info, int *typoAscent, int *typoDescent, int *typoLineGap) +{ + int tab = stbtt__find_table(info->data, info->fontstart, "OS/2"); + if (!tab) + return 0; + if (typoAscent ) *typoAscent = ttSHORT(info->data+tab + 68); + if (typoDescent) *typoDescent = ttSHORT(info->data+tab + 70); + if (typoLineGap) *typoLineGap = ttSHORT(info->data+tab + 72); + return 1; +} + +STBTT_DEF void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +STBTT_DEF float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +STBTT_DEF stbtt_uint8 *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl) +{ + int i; + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc_list = data + stbtt__get_svg((stbtt_fontinfo *) info); + + int numEntries = ttUSHORT(svg_doc_list); + stbtt_uint8 *svg_docs = svg_doc_list + 2; + + for(i=0; i= ttUSHORT(svg_doc)) && (gl <= ttUSHORT(svg_doc + 2))) + return svg_doc; + } + return 0; +} + +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg) +{ + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc; + + if (info->svg == 0) + return 0; + + svg_doc = stbtt_FindSVGDoc(info, gl); + if (svg_doc != NULL) { + *svg = (char *) data + info->svg + ttULONG(svg_doc + 4); + return ttULONG(svg_doc + 8); + } else { + return 0; + } +} + +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg) +{ + return stbtt_GetGlyphSVG(info, stbtt_FindGlyphIndex(info, unicode_codepoint), svg); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0=0,y0=0,x1,y1; // =0 suppresses compiler warning + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) { + // e.g. space character + if (ix0) *ix0 = 0; + if (iy0) *iy0 = 0; + if (ix1) *ix1 = 0; + if (iy1) *iy1 = 0; + } else { + // move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor( x0 * scale_x + shift_x); + if (iy0) *iy0 = STBTT_ifloor(-y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil ( x1 * scale_x + shift_x); + if (iy1) *iy1 = STBTT_iceil (-y0 * scale_y + shift_y); + } +} + +STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +STBTT_DEF void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + struct stbtt__hheap_chunk *next; +} stbtt__hheap_chunk; + +typedef struct stbtt__hheap +{ + struct stbtt__hheap_chunk *head; + void *first_free; + int num_remaining_in_head_chunk; +} stbtt__hheap; + +static void *stbtt__hheap_alloc(stbtt__hheap *hh, size_t size, void *userdata) +{ + if (hh->first_free) { + void *p = hh->first_free; + hh->first_free = * (void **) p; + return p; + } else { + if (hh->num_remaining_in_head_chunk == 0) { + int count = (size < 32 ? 2000 : size < 128 ? 800 : 100); + stbtt__hheap_chunk *c = (stbtt__hheap_chunk *) STBTT_malloc(sizeof(stbtt__hheap_chunk) + size * count, userdata); + if (c == NULL) + return NULL; + c->next = hh->head; + hh->head = c; + hh->num_remaining_in_head_chunk = count; + } + --hh->num_remaining_in_head_chunk; + return (char *) (hh->head) + sizeof(stbtt__hheap_chunk) + size * hh->num_remaining_in_head_chunk; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *p) +{ + *(void **) p = hh->first_free; + hh->first_free = p; +} + +static void stbtt__hheap_cleanup(stbtt__hheap *hh, void *userdata) +{ + stbtt__hheap_chunk *c = hh->head; + while (c) { + stbtt__hheap_chunk *n = c->next; + STBTT_free(c, userdata); + c = n; + } +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + + +typedef struct stbtt__active_edge +{ + struct stbtt__active_edge *next; + #if STBTT_RASTERIZER_VERSION==1 + int x,dx; + float ey; + int direction; + #elif STBTT_RASTERIZER_VERSION==2 + float fx,fdx,fdy; + float direction; + float sy; + float ey; + #else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" + #endif +} stbtt__active_edge; + +#if STBTT_RASTERIZER_VERSION == 1 +#define STBTT_FIXSHIFT 10 +#define STBTT_FIX (1 << STBTT_FIXSHIFT) +#define STBTT_FIXMASK (STBTT_FIX-1) + +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + if (!z) return z; + + // round dx down to avoid overshooting + if (dxdy < 0) + z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); + else + z->dx = STBTT_ifloor(STBTT_FIX * dxdy); + + z->x = STBTT_ifloor(STBTT_FIX * e->x0 + z->dx * (start_point - e->y0)); // use z->dx so when we offset later it's by the same amount + z->x -= off_x * STBTT_FIX; + + z->ey = e->y1; + z->next = 0; + z->direction = e->invert ? 1 : -1; + return z; +} +#elif STBTT_RASTERIZER_VERSION == 2 +static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) stbtt__hheap_alloc(hh, sizeof(*z), userdata); + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(z != NULL); + //STBTT_assert(e->y0 <= start_point); + if (!z) return z; + z->fdx = dxdy; + z->fdy = dxdy != 0.0f ? (1.0f/dxdy) : 0.0f; + z->fx = e->x0 + dxdy * (start_point - e->y0); + z->fx -= off_x; + z->direction = e->invert ? 1.0f : -1.0f; + z->sy = e->y0; + z->ey = e->y1; + z->next = 0; + return z; +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#if STBTT_RASTERIZER_VERSION == 1 +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->direction; + } else { + int x1 = e->x; w += e->direction; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> STBTT_FIXSHIFT; + int j = x1 >> STBTT_FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> STBTT_FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((STBTT_FIX - (x0 & STBTT_FIXMASK)) * max_weight) >> STBTT_FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & STBTT_FIXMASK) * max_weight) >> STBTT_FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y, userdata); + if (z != NULL) { + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +#elif STBTT_RASTERIZER_VERSION == 2 + +// the edge passed in here does not cross the vertical line at x or the vertical line at x+1 +// (i.e. it has already been clipped to those) +static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edge *e, float x0, float y0, float x1, float y1) +{ + if (y0 == y1) return; + STBTT_assert(y0 < y1); + STBTT_assert(e->sy <= e->ey); + if (y0 > e->ey) return; + if (y1 < e->sy) return; + if (y0 < e->sy) { + x0 += (x1-x0) * (e->sy - y0) / (y1-y0); + y0 = e->sy; + } + if (y1 > e->ey) { + x1 += (x1-x0) * (e->ey - y1) / (y1-y0); + y1 = e->ey; + } + + if (x0 == x) + STBTT_assert(x1 <= x+1); + else if (x0 == x+1) + STBTT_assert(x1 >= x); + else if (x0 <= x) + STBTT_assert(x1 <= x); + else if (x0 >= x+1) + STBTT_assert(x1 >= x+1); + else + STBTT_assert(x1 >= x && x1 <= x+1); + + if (x0 <= x && x1 <= x) + scanline[x] += e->direction * (y1-y0); + else if (x0 >= x+1 && x1 >= x+1) + ; + else { + STBTT_assert(x0 >= x && x0 <= x+1 && x1 >= x && x1 <= x+1); + scanline[x] += e->direction * (y1-y0) * (1-((x0-x)+(x1-x))/2); // coverage = 1 - average x position + } +} + +static float stbtt__sized_trapezoid_area(float height, float top_width, float bottom_width) +{ + STBTT_assert(top_width >= 0); + STBTT_assert(bottom_width >= 0); + return (top_width + bottom_width) / 2.0f * height; +} + +static float stbtt__position_trapezoid_area(float height, float tx0, float tx1, float bx0, float bx1) +{ + return stbtt__sized_trapezoid_area(height, tx1 - tx0, bx1 - bx0); +} + +static float stbtt__sized_triangle_area(float height, float width) +{ + return height * width / 2; +} + +static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) +{ + float y_bottom = y_top+1; + + while (e) { + // brute force every pixel + + // compute intersection points with top & bottom + STBTT_assert(e->ey >= y_top); + + if (e->fdx == 0) { + float x0 = e->fx; + if (x0 < len) { + if (x0 >= 0) { + stbtt__handle_clipped_edge(scanline,(int) x0,e, x0,y_top, x0,y_bottom); + stbtt__handle_clipped_edge(scanline_fill-1,(int) x0+1,e, x0,y_top, x0,y_bottom); + } else { + stbtt__handle_clipped_edge(scanline_fill-1,0,e, x0,y_top, x0,y_bottom); + } + } + } else { + float x0 = e->fx; + float dx = e->fdx; + float xb = x0 + dx; + float x_top, x_bottom; + float sy0,sy1; + float dy = e->fdy; + STBTT_assert(e->sy <= y_bottom && e->ey >= y_top); + + // compute endpoints of line segment clipped to this scanline (if the + // line segment starts on this scanline. x0 is the intersection of the + // line with y_top, but that may be off the line segment. + if (e->sy > y_top) { + x_top = x0 + dx * (e->sy - y_top); + sy0 = e->sy; + } else { + x_top = x0; + sy0 = y_top; + } + if (e->ey < y_bottom) { + x_bottom = x0 + dx * (e->ey - y_top); + sy1 = e->ey; + } else { + x_bottom = xb; + sy1 = y_bottom; + } + + if (x_top >= 0 && x_bottom >= 0 && x_top < len && x_bottom < len) { + // from here on, we don't have to range check x values + + if ((int) x_top == (int) x_bottom) { + float height; + // simple case, only spans one pixel + int x = (int) x_top; + height = (sy1 - sy0) * e->direction; + STBTT_assert(x >= 0 && x < len); + scanline[x] += stbtt__position_trapezoid_area(height, x_top, x+1.0f, x_bottom, x+1.0f); + scanline_fill[x] += height; // everything right of this pixel is filled + } else { + int x,x1,x2; + float y_crossing, y_final, step, sign, area; + // covers 2+ pixels + if (x_top > x_bottom) { + // flip scanline vertically; signed area is the same + float t; + sy0 = y_bottom - (sy0 - y_top); + sy1 = y_bottom - (sy1 - y_top); + t = sy0, sy0 = sy1, sy1 = t; + t = x_bottom, x_bottom = x_top, x_top = t; + dx = -dx; + dy = -dy; + t = x0, x0 = xb, xb = t; + } + STBTT_assert(dy >= 0); + STBTT_assert(dx >= 0); + + x1 = (int) x_top; + x2 = (int) x_bottom; + // compute intersection with y axis at x1+1 + y_crossing = y_top + dy * (x1+1 - x0); + + // compute intersection with y axis at x2 + y_final = y_top + dy * (x2 - x0); + + // x1 x_top x2 x_bottom + // y_top +------|-----+------------+------------+--------|---+------------+ + // | | | | | | + // | | | | | | + // sy0 | Txxxxx|............|............|............|............| + // y_crossing | *xxxxx.......|............|............|............| + // | | xxxxx..|............|............|............| + // | | /- xx*xxxx........|............|............| + // | | dy < | xxxxxx..|............|............| + // y_final | | \- | xx*xxx.........|............| + // sy1 | | | | xxxxxB...|............| + // | | | | | | + // | | | | | | + // y_bottom +------------+------------+------------+------------+------------+ + // + // goal is to measure the area covered by '.' in each pixel + + // if x2 is right at the right edge of x1, y_crossing can blow up, github #1057 + // @TODO: maybe test against sy1 rather than y_bottom? + if (y_crossing > y_bottom) + y_crossing = y_bottom; + + sign = e->direction; + + // area of the rectangle covered from sy0..y_crossing + area = sign * (y_crossing-sy0); + + // area of the triangle (x_top,sy0), (x1+1,sy0), (x1+1,y_crossing) + scanline[x1] += stbtt__sized_triangle_area(area, x1+1 - x_top); + + // check if final y_crossing is blown up; no test case for this + if (y_final > y_bottom) { + int denom = (x2 - (x1+1)); + y_final = y_bottom; + if (denom != 0) { // [DEAR IMGUI] Avoid div by zero (https://github.com/nothings/stb/issues/1316) + dy = (y_final - y_crossing ) / denom; // if denom=0, y_final = y_crossing, so y_final <= y_bottom + } + } + + // in second pixel, area covered by line segment found in first pixel + // is always a rectangle 1 wide * the height of that line segment; this + // is exactly what the variable 'area' stores. it also gets a contribution + // from the line segment within it. the THIRD pixel will get the first + // pixel's rectangle contribution, the second pixel's rectangle contribution, + // and its own contribution. the 'own contribution' is the same in every pixel except + // the leftmost and rightmost, a trapezoid that slides down in each pixel. + // the second pixel's contribution to the third pixel will be the + // rectangle 1 wide times the height change in the second pixel, which is dy. + + step = sign * dy * 1; // dy is dy/dx, change in y for every 1 change in x, + // which multiplied by 1-pixel-width is how much pixel area changes for each step in x + // so the area advances by 'step' every time + + for (x = x1+1; x < x2; ++x) { + scanline[x] += area + step/2; // area of trapezoid is 1*step/2 + area += step; + } + STBTT_assert(STBTT_fabs(area) <= 1.01f); // accumulated error from area += step unless we round step down + STBTT_assert(sy1 > y_final-0.01f); + + // area covered in the last pixel is the rectangle from all the pixels to the left, + // plus the trapezoid filled by the line segment in this pixel all the way to the right edge + scanline[x2] += area + sign * stbtt__position_trapezoid_area(sy1-y_final, (float) x2, x2+1.0f, x_bottom, x2+1.0f); + + // the rest of the line is filled based on the total height of the line segment in this pixel + scanline_fill[x2] += sign * (sy1-sy0); + } + } else { + // if edge goes outside of box we're drawing, we require + // clipping logic. since this does not match the intended use + // of this library, we use a different, very slow brute + // force implementation + // note though that this does happen some of the time because + // x_top and x_bottom can be extrapolated at the top & bottom of + // the shape and actually lie outside the bounding box + int x; + for (x=0; x < len; ++x) { + // cases: + // + // there can be up to two intersections with the pixel. any intersection + // with left or right edges can be handled by splitting into two (or three) + // regions. intersections with top & bottom do not necessitate case-wise logic. + // + // the old way of doing this found the intersections with the left & right edges, + // then used some simple logic to produce up to three segments in sorted order + // from top-to-bottom. however, this had a problem: if an x edge was epsilon + // across the x border, then the corresponding y position might not be distinct + // from the other y segment, and it might ignored as an empty segment. to avoid + // that, we need to explicitly produce segments based on x positions. + + // rename variables to clearly-defined pairs + float y0 = y_top; + float x1 = (float) (x); + float x2 = (float) (x+1); + float x3 = xb; + float y3 = y_bottom; + + // x = e->x + e->dx * (y-y_top) + // (y-y_top) = (x - e->x) / e->dx + // y = (x - e->x) / e->dx + y_top + float y1 = (x - x0) / dx + y_top; + float y2 = (x+1 - x0) / dx + y_top; + + if (x0 < x1 && x3 > x2) { // three segments descending down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x1 && x0 > x2) { // three segments descending down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x1 && x3 > x1) { // two segments across x, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x3 < x1 && x0 > x1) { // two segments across x, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x1,y1); + stbtt__handle_clipped_edge(scanline,x,e, x1,y1, x3,y3); + } else if (x0 < x2 && x3 > x2) { // two segments across x+1, down-right + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else if (x3 < x2 && x0 > x2) { // two segments across x+1, down-left + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x2,y2); + stbtt__handle_clipped_edge(scanline,x,e, x2,y2, x3,y3); + } else { // one segment + stbtt__handle_clipped_edge(scanline,x,e, x0,y0, x3,y3); + } + } + } + } + e = e->next; + } +} + +// directly AA rasterize edges w/o supersampling +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__hheap hh = { 0, 0, 0 }; + stbtt__active_edge *active = NULL; + int y,j=0, i; + float scanline_data[129], *scanline, *scanline2; + + STBTT__NOTUSED(vsubsample); + + if (result->w > 64) + scanline = (float *) STBTT_malloc((result->w*2+1) * sizeof(float), userdata); + else + scanline = scanline_data; + + scanline2 = scanline + result->w; + + y = off_y; + e[n].y0 = (float) (off_y + result->h) + 1; + + while (j < result->h) { + // find center of pixel for this scanline + float scan_y_top = y + 0.0f; + float scan_y_bottom = y + 1.0f; + stbtt__active_edge **step = &active; + + STBTT_memset(scanline , 0, result->w*sizeof(scanline[0])); + STBTT_memset(scanline2, 0, (result->w+1)*sizeof(scanline[0])); + + // update all active edges; + // remove all active edges that terminate before the top of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y_top) { + *step = z->next; // delete from list + STBTT_assert(z->direction); + z->direction = 0; + stbtt__hheap_free(&hh, z); + } else { + step = &((*step)->next); // advance through list + } + } + + // insert all edges that start before the bottom of this scanline + while (e->y0 <= scan_y_bottom) { + if (e->y0 != e->y1) { + stbtt__active_edge *z = stbtt__new_active(&hh, e, off_x, scan_y_top, userdata); + if (z != NULL) { + if (j == 0 && off_y != 0) { + if (z->ey < scan_y_top) { + // this can happen due to subpixel positioning and some kind of fp rounding error i think + z->ey = scan_y_top; + } + } + STBTT_assert(z->ey >= scan_y_top); // if we get really unlucky a tiny bit of an edge can be out of bounds + // insert at front + z->next = active; + active = z; + } + } + ++e; + } + + // now process all active edges + if (active) + stbtt__fill_active_edges_new(scanline, scanline2+1, result->w, active, scan_y_top); + + { + float sum = 0; + for (i=0; i < result->w; ++i) { + float k; + int m; + sum += scanline2[i]; + k = scanline[i] + sum; + k = (float) STBTT_fabs(k)*255 + 0.5f; + m = (int) k; + if (m > 255) m = 255; + result->pixels[j*result->stride + i] = (unsigned char) m; + } + } + // advance all the edges + step = &active; + while (*step) { + stbtt__active_edge *z = *step; + z->fx += z->fdx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + + ++y; + ++j; + } + + stbtt__hheap_cleanup(&hh, userdata); + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} +#else +#error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + +#define STBTT__COMPARE(a,b) ((a)->y0 < (b)->y0) + +static void stbtt__sort_edges_ins_sort(stbtt__edge *p, int n) +{ + int i,j; + for (i=1; i < n; ++i) { + stbtt__edge t = p[i], *a = &t; + j = i; + while (j > 0) { + stbtt__edge *b = &p[j-1]; + int c = STBTT__COMPARE(a,b); + if (!c) break; + p[j] = p[j-1]; + --j; + } + if (i != j) + p[j] = t; + } +} + +static void stbtt__sort_edges_quicksort(stbtt__edge *p, int n) +{ + /* threshold for transitioning to insertion sort */ + while (n > 12) { + stbtt__edge t; + int c01,c12,c,m,i,j; + + /* compute median of three */ + m = n >> 1; + c01 = STBTT__COMPARE(&p[0],&p[m]); + c12 = STBTT__COMPARE(&p[m],&p[n-1]); + /* if 0 >= mid >= end, or 0 < mid < end, then use mid */ + if (c01 != c12) { + /* otherwise, we'll need to swap something else to middle */ + int z; + c = STBTT__COMPARE(&p[0],&p[n-1]); + /* 0>mid && midn => n; 0 0 */ + /* 0n: 0>n => 0; 0 n */ + z = (c == c12) ? 0 : n-1; + t = p[z]; + p[z] = p[m]; + p[m] = t; + } + /* now p[m] is the median-of-three */ + /* swap it to the beginning so it won't move around */ + t = p[0]; + p[0] = p[m]; + p[m] = t; + + /* partition loop */ + i=1; + j=n-1; + for(;;) { + /* handling of equality is crucial here */ + /* for sentinels & efficiency with duplicates */ + for (;;++i) { + if (!STBTT__COMPARE(&p[i], &p[0])) break; + } + for (;;--j) { + if (!STBTT__COMPARE(&p[0], &p[j])) break; + } + /* make sure we haven't crossed */ + if (i >= j) break; + t = p[i]; + p[i] = p[j]; + p[j] = t; + + ++i; + --j; + } + /* recurse on smaller side, iterate on larger */ + if (j < (n-i)) { + stbtt__sort_edges_quicksort(p,j); + p = p+i; + n = n-i; + } else { + stbtt__sort_edges_quicksort(p+i, n-i); + n = j; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; +#if STBTT_RASTERIZER_VERSION == 1 + int vsubsample = result->h < 8 ? 15 : 5; +#elif STBTT_RASTERIZER_VERSION == 2 + int vsubsample = 1; +#else + #error "Unrecognized value of STBTT_RASTERIZER_VERSION" +#endif + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = (p[a].y * y_scale_inv + shift_y) * vsubsample; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = (p[b].y * y_scale_inv + shift_y) * vsubsample; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + //STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + stbtt__sort_edges(e, n); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tessellate until threshold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +static void stbtt__tesselate_cubic(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3, float objspace_flatness_squared, int n) +{ + // @TODO this "flatness" calculation is just made-up nonsense that seems to work well enough + float dx0 = x1-x0; + float dy0 = y1-y0; + float dx1 = x2-x1; + float dy1 = y2-y1; + float dx2 = x3-x2; + float dy2 = y3-y2; + float dx = x3-x0; + float dy = y3-y0; + float longlen = (float) (STBTT_sqrt(dx0*dx0+dy0*dy0)+STBTT_sqrt(dx1*dx1+dy1*dy1)+STBTT_sqrt(dx2*dx2+dy2*dy2)); + float shortlen = (float) STBTT_sqrt(dx*dx+dy*dy); + float flatness_squared = longlen*longlen-shortlen*shortlen; + + if (n > 16) // 65536 segments on one curve better be enough! + return; + + if (flatness_squared > objspace_flatness_squared) { + float x01 = (x0+x1)/2; + float y01 = (y0+y1)/2; + float x12 = (x1+x2)/2; + float y12 = (y1+y2)/2; + float x23 = (x2+x3)/2; + float y23 = (y2+y3)/2; + + float xa = (x01+x12)/2; + float ya = (y01+y12)/2; + float xb = (x12+x23)/2; + float yb = (y12+y23)/2; + + float mx = (xa+xb)/2; + float my = (ya+yb)/2; + + stbtt__tesselate_cubic(points, num_points, x0,y0, x01,y01, xa,ya, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_cubic(points, num_points, mx,my, xb,yb, x23,y23, x3,y3, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x3,y3); + *num_points = *num_points+1; + } +} + +// returns number of contours +static stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + case STBTT_vcubic: + stbtt__tesselate_cubic(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].cx1, vertices[i].cy1, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +STBTT_DEF void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count = 0; + int *winding_lengths = NULL; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) { + STBTT_free(vertices, info->userdata); + return NULL; + } + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixelPrefilter(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, oversample_x, oversample_y, sub_x, sub_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +static int stbtt_BakeFontBitmap_internal(unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + f.userdata = NULL; + if (!stbtt_InitFont(&f, data, offset)) + return -1; + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 1; + if (y+gh+1 > bottom_y) + bottom_y = y+gh+1; + } + return bottom_y; +} + +STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5f); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5f); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// rectangle packing replacement routines if you don't have stb_rect_pack.h +// + +#ifndef STB_RECT_PACK_VERSION + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + stbrp_coord x,y; + int id,w,h,was_packed; +}; + +static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *nodes, int num_nodes) +{ + con->width = pw; + con->height = ph; + con->x = 0; + con->y = 0; + con->bottom_y = 0; + STBTT__NOTUSED(nodes); + STBTT__NOTUSED(num_nodes); +} + +static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) +{ + int i; + for (i=0; i < num_rects; ++i) { + if (con->x + rects[i].w > con->width) { + con->x = 0; + con->y = con->bottom_y; + } + if (con->y + rects[i].h > con->height) + break; + rects[i].x = con->x; + rects[i].y = con->y; + rects[i].was_packed = 1; + con->x += rects[i].w; + if (con->y + rects[i].h > con->bottom_y) + con->bottom_y = con->y + rects[i].h; + } + for ( ; i < num_rects; ++i) + rects[i].was_packed = 0; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, int pw, int ph, int stride_in_bytes, int padding, void *alloc_context) +{ + stbrp_context *context = (stbrp_context *) STBTT_malloc(sizeof(*context) ,alloc_context); + int num_nodes = pw - padding; + stbrp_node *nodes = (stbrp_node *) STBTT_malloc(sizeof(*nodes ) * num_nodes,alloc_context); + + if (context == NULL || nodes == NULL) { + if (context != NULL) STBTT_free(context, alloc_context); + if (nodes != NULL) STBTT_free(nodes , alloc_context); + return 0; + } + + spc->user_allocator_context = alloc_context; + spc->width = pw; + spc->height = ph; + spc->pixels = pixels; + spc->pack_info = context; + spc->nodes = nodes; + spc->padding = padding; + spc->stride_in_bytes = stride_in_bytes != 0 ? stride_in_bytes : pw; + spc->h_oversample = 1; + spc->v_oversample = 1; + spc->skip_missing = 0; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +STBTT_DEF void stbtt_PackEnd (stbtt_pack_context *spc) +{ + STBTT_free(spc->nodes , spc->user_allocator_context); + STBTT_free(spc->pack_info, spc->user_allocator_context); +} + +STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h_oversample, unsigned int v_oversample) +{ + STBTT_assert(h_oversample <= STBTT_MAX_OVERSAMPLE); + STBTT_assert(v_oversample <= STBTT_MAX_OVERSAMPLE); + if (h_oversample <= STBTT_MAX_OVERSAMPLE) + spc->h_oversample = h_oversample; + if (v_oversample <= STBTT_MAX_OVERSAMPLE) + spc->v_oversample = v_oversample; +} + +STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip) +{ + spc->skip_missing = skip; +} + +#define STBTT__OVER_MASK (STBTT_MAX_OVERSAMPLE-1) + +static void stbtt__h_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_w = w - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < h; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_w; ++i) { + total += pixels[i] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i]; + pixels[i] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < w; ++i) { + STBTT_assert(pixels[i] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i] = (unsigned char) (total / kernel_width); + } + + pixels += stride_in_bytes; + } +} + +static void stbtt__v_prefilter(unsigned char *pixels, int w, int h, int stride_in_bytes, unsigned int kernel_width) +{ + unsigned char buffer[STBTT_MAX_OVERSAMPLE]; + int safe_h = h - kernel_width; + int j; + STBTT_memset(buffer, 0, STBTT_MAX_OVERSAMPLE); // suppress bogus warning from VS2013 -analyze + for (j=0; j < w; ++j) { + int i; + unsigned int total; + STBTT_memset(buffer, 0, kernel_width); + + total = 0; + + // make kernel_width a constant in common cases so compiler can optimize out the divide + switch (kernel_width) { + case 2: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 2); + } + break; + case 3: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 3); + } + break; + case 4: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 4); + } + break; + case 5: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / 5); + } + break; + default: + for (i=0; i <= safe_h; ++i) { + total += pixels[i*stride_in_bytes] - buffer[i & STBTT__OVER_MASK]; + buffer[(i+kernel_width) & STBTT__OVER_MASK] = pixels[i*stride_in_bytes]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + break; + } + + for (; i < h; ++i) { + STBTT_assert(pixels[i*stride_in_bytes] == 0); + total -= buffer[i & STBTT__OVER_MASK]; + pixels[i*stride_in_bytes] = (unsigned char) (total / kernel_width); + } + + pixels += 1; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + if (!oversample) + return 0.0f; + + // The prefilter is a box filter of width "oversample", + // which shifts phase by (oversample - 1)/2 pixels in + // oversampled space. We want to shift in the opposite + // direction to counter this. + return (float)-(oversample - 1) / (2.0f * (float)oversample); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k; + int missing_glyph_added = 0; + + k=0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + ranges[i].h_oversample = (unsigned char) spc->h_oversample; + ranges[i].v_oversample = (unsigned char) spc->v_oversample; + for (j=0; j < ranges[i].num_chars; ++j) { + int x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + if (glyph == 0 && (spc->skip_missing || missing_glyph_added)) { + rects[k].w = rects[k].h = 0; + } else { + stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + &x0,&y0,&x1,&y1); + rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); + rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + if (glyph == 0) + missing_glyph_added = 1; + } + ++k; + } + } + + return k; +} + +STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int prefilter_x, int prefilter_y, float *sub_x, float *sub_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, + output, + out_w - (prefilter_x - 1), + out_h - (prefilter_y - 1), + out_stride, + scale_x, + scale_y, + shift_x, + shift_y, + glyph); + + if (prefilter_x > 1) + stbtt__h_prefilter(output, out_w, out_h, out_stride, prefilter_x); + + if (prefilter_y > 1) + stbtt__v_prefilter(output, out_w, out_h, out_stride, prefilter_y); + + *sub_x = stbtt__oversample_shift(prefilter_x); + *sub_y = stbtt__oversample_shift(prefilter_y); +} + +// rects array must be big enough to accommodate all characters in the given ranges +STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) +{ + int i,j,k, missing_glyph = -1, return_value = 1; + + // save current values + int old_h_over = spc->h_oversample; + int old_v_over = spc->v_oversample; + + k = 0; + for (i=0; i < num_ranges; ++i) { + float fh = ranges[i].font_size; + float scale = fh > 0 ? stbtt_ScaleForPixelHeight(info, fh) : stbtt_ScaleForMappingEmToPixels(info, -fh); + float recip_h,recip_v,sub_x,sub_y; + spc->h_oversample = ranges[i].h_oversample; + spc->v_oversample = ranges[i].v_oversample; + recip_h = 1.0f / spc->h_oversample; + recip_v = 1.0f / spc->v_oversample; + sub_x = stbtt__oversample_shift(spc->h_oversample); + sub_y = stbtt__oversample_shift(spc->v_oversample); + for (j=0; j < ranges[i].num_chars; ++j) { + stbrp_rect *r = &rects[k]; + if (r->was_packed && r->w != 0 && r->h != 0) { + stbtt_packedchar *bc = &ranges[i].chardata_for_range[j]; + int advance, lsb, x0,y0,x1,y1; + int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; + int glyph = stbtt_FindGlyphIndex(info, codepoint); + stbrp_coord pad = (stbrp_coord) spc->padding; + + // pad on left and top + r->x += pad; + r->y += pad; + r->w -= pad; + r->h -= pad; + stbtt_GetGlyphHMetrics(info, glyph, &advance, &lsb); + stbtt_GetGlyphBitmapBox(info, glyph, + scale * spc->h_oversample, + scale * spc->v_oversample, + &x0,&y0,&x1,&y1); + stbtt_MakeGlyphBitmapSubpixel(info, + spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w - spc->h_oversample+1, + r->h - spc->v_oversample+1, + spc->stride_in_bytes, + scale * spc->h_oversample, + scale * spc->v_oversample, + 0,0, + glyph); + + if (spc->h_oversample > 1) + stbtt__h_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->h_oversample); + + if (spc->v_oversample > 1) + stbtt__v_prefilter(spc->pixels + r->x + r->y*spc->stride_in_bytes, + r->w, r->h, spc->stride_in_bytes, + spc->v_oversample); + + bc->x0 = (stbtt_int16) r->x; + bc->y0 = (stbtt_int16) r->y; + bc->x1 = (stbtt_int16) (r->x + r->w); + bc->y1 = (stbtt_int16) (r->y + r->h); + bc->xadvance = scale * advance; + bc->xoff = (float) x0 * recip_h + sub_x; + bc->yoff = (float) y0 * recip_v + sub_y; + bc->xoff2 = (x0 + r->w) * recip_h + sub_x; + bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + + if (glyph == 0) + missing_glyph = j; + } else if (spc->skip_missing) { + return_value = 0; + } else if (r->was_packed && r->w == 0 && r->h == 0 && missing_glyph >= 0) { + ranges[i].chardata_for_range[j] = ranges[i].chardata_for_range[missing_glyph]; + } else { + return_value = 0; // if any fail, report failure + } + + ++k; + } + } + + // restore original values + spc->h_oversample = old_h_over; + spc->v_oversample = old_v_over; + + return return_value; +} + +STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect *rects, int num_rects) +{ + stbrp_pack_rects((stbrp_context *) spc->pack_info, rects, num_rects); +} + +STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) +{ + stbtt_fontinfo info; + int i, j, n, return_value; // [DEAR IMGUI] removed = 1; + //stbrp_context *context = (stbrp_context *) spc->pack_info; + stbrp_rect *rects; + + // flag all characters as NOT packed + for (i=0; i < num_ranges; ++i) + for (j=0; j < ranges[i].num_chars; ++j) + ranges[i].chardata_for_range[j].x0 = + ranges[i].chardata_for_range[j].y0 = + ranges[i].chardata_for_range[j].x1 = + ranges[i].chardata_for_range[j].y1 = 0; + + n = 0; + for (i=0; i < num_ranges; ++i) + n += ranges[i].num_chars; + + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); + if (rects == NULL) + return 0; + + info.userdata = spc->user_allocator_context; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata,font_index)); + + n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); + + stbtt_PackFontRangesPackRects(spc, rects, n); + + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); + + STBTT_free(rects, spc->user_allocator_context); + return return_value; +} + +STBTT_DEF int stbtt_PackFontRange(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, float font_size, + int first_unicode_codepoint_in_range, int num_chars_in_range, stbtt_packedchar *chardata_for_range) +{ + stbtt_pack_range range; + range.first_unicode_codepoint_in_range = first_unicode_codepoint_in_range; + range.array_of_unicode_codepoints = NULL; + range.num_chars = num_chars_in_range; + range.chardata_for_range = chardata_for_range; + range.font_size = font_size; + return stbtt_PackFontRanges(spc, fontdata, font_index, &range, 1); +} + +STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap) +{ + int i_ascent, i_descent, i_lineGap; + float scale; + stbtt_fontinfo info; + stbtt_InitFont(&info, fontdata, stbtt_GetFontOffsetForIndex(fontdata, index)); + scale = size > 0 ? stbtt_ScaleForPixelHeight(&info, size) : stbtt_ScaleForMappingEmToPixels(&info, -size); + stbtt_GetFontVMetrics(&info, &i_ascent, &i_descent, &i_lineGap); + *ascent = (float) i_ascent * scale; + *descent = (float) i_descent * scale; + *lineGap = (float) i_lineGap * scale; +} + +STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int align_to_integer) +{ + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_packedchar *b = chardata + char_index; + + if (align_to_integer) { + float x = (float) STBTT_ifloor((*xpos + b->xoff) + 0.5f); + float y = (float) STBTT_ifloor((*ypos + b->yoff) + 0.5f); + q->x0 = x; + q->y0 = y; + q->x1 = x + b->xoff2 - b->xoff; + q->y1 = y + b->yoff2 - b->yoff; + } else { + q->x0 = *xpos + b->xoff; + q->y0 = *ypos + b->yoff; + q->x1 = *xpos + b->xoff2; + q->y1 = *ypos + b->yoff2; + } + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// sdf computation +// + +#define STBTT_min(a,b) ((a) < (b) ? (a) : (b)) +#define STBTT_max(a,b) ((a) < (b) ? (b) : (a)) + +static int stbtt__ray_intersect_bezier(float orig[2], float ray[2], float q0[2], float q1[2], float q2[2], float hits[2][2]) +{ + float q0perp = q0[1]*ray[0] - q0[0]*ray[1]; + float q1perp = q1[1]*ray[0] - q1[0]*ray[1]; + float q2perp = q2[1]*ray[0] - q2[0]*ray[1]; + float roperp = orig[1]*ray[0] - orig[0]*ray[1]; + + float a = q0perp - 2*q1perp + q2perp; + float b = q1perp - q0perp; + float c = q0perp - roperp; + + float s0 = 0., s1 = 0.; + int num_s = 0; + + if (a != 0.0) { + float discr = b*b - a*c; + if (discr > 0.0) { + float rcpna = -1 / a; + float d = (float) STBTT_sqrt(discr); + s0 = (b+d) * rcpna; + s1 = (b-d) * rcpna; + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + if (d > 0.0 && s1 >= 0.0 && s1 <= 1.0) { + if (num_s == 0) s0 = s1; + ++num_s; + } + } + } else { + // 2*b*s + c = 0 + // s = -c / (2*b) + s0 = c / (-2 * b); + if (s0 >= 0.0 && s0 <= 1.0) + num_s = 1; + } + + if (num_s == 0) + return 0; + else { + float rcp_len2 = 1 / (ray[0]*ray[0] + ray[1]*ray[1]); + float rayn_x = ray[0] * rcp_len2, rayn_y = ray[1] * rcp_len2; + + float q0d = q0[0]*rayn_x + q0[1]*rayn_y; + float q1d = q1[0]*rayn_x + q1[1]*rayn_y; + float q2d = q2[0]*rayn_x + q2[1]*rayn_y; + float rod = orig[0]*rayn_x + orig[1]*rayn_y; + + float q10d = q1d - q0d; + float q20d = q2d - q0d; + float q0rd = q0d - rod; + + hits[0][0] = q0rd + s0*(2.0f - 2.0f*s0)*q10d + s0*s0*q20d; + hits[0][1] = a*s0+b; + + if (num_s > 1) { + hits[1][0] = q0rd + s1*(2.0f - 2.0f*s1)*q10d + s1*s1*q20d; + hits[1][1] = a*s1+b; + return 2; + } else { + return 1; + } + } +} + +static int equal(float *a, float *b) +{ + return (a[0] == b[0] && a[1] == b[1]); +} + +static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex *verts) +{ + int i; + float orig[2], ray[2] = { 1, 0 }; + float y_frac; + int winding = 0; + + // make sure y never passes through a vertex of the shape + y_frac = (float) STBTT_fmod(y, 1.0f); + if (y_frac < 0.01f) + y += 0.01f; + else if (y_frac > 0.99f) + y -= 0.01f; + + orig[0] = x; + orig[1] = y; + + // test a ray from (-infinity,y) to (x,y) + for (i=0; i < nverts; ++i) { + if (verts[i].type == STBTT_vline) { + int x0 = (int) verts[i-1].x, y0 = (int) verts[i-1].y; + int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } + if (verts[i].type == STBTT_vcurve) { + int x0 = (int) verts[i-1].x , y0 = (int) verts[i-1].y ; + int x1 = (int) verts[i ].cx, y1 = (int) verts[i ].cy; + int x2 = (int) verts[i ].x , y2 = (int) verts[i ].y ; + int ax = STBTT_min(x0,STBTT_min(x1,x2)), ay = STBTT_min(y0,STBTT_min(y1,y2)); + int by = STBTT_max(y0,STBTT_max(y1,y2)); + if (y > ay && y < by && x > ax) { + float q0[2],q1[2],q2[2]; + float hits[2][2]; + q0[0] = (float)x0; + q0[1] = (float)y0; + q1[0] = (float)x1; + q1[1] = (float)y1; + q2[0] = (float)x2; + q2[1] = (float)y2; + if (equal(q0,q1) || equal(q1,q2)) { + x0 = (int)verts[i-1].x; + y0 = (int)verts[i-1].y; + x1 = (int)verts[i ].x; + y1 = (int)verts[i ].y; + if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { + float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; + if (x_inter < x) + winding += (y0 < y1) ? 1 : -1; + } + } else { + int num_hits = stbtt__ray_intersect_bezier(orig, ray, q0, q1, q2, hits); + if (num_hits >= 1) + if (hits[0][0] < 0) + winding += (hits[0][1] < 0 ? -1 : 1); + if (num_hits >= 2) + if (hits[1][0] < 0) + winding += (hits[1][1] < 0 ? -1 : 1); + } + } + } + } + return winding; +} + +static float stbtt__cuberoot( float x ) +{ + if (x<0) + return -(float) STBTT_pow(-x,1.0f/3.0f); + else + return (float) STBTT_pow( x,1.0f/3.0f); +} + +// x^3 + a*x^2 + b*x + c = 0 +static int stbtt__solve_cubic(float a, float b, float c, float* r) +{ + float s = -a / 3; + float p = b - a*a / 3; + float q = a * (2*a*a - 9*b) / 27 + c; + float p3 = p*p*p; + float d = q*q + 4*p3 / 27; + if (d >= 0) { + float z = (float) STBTT_sqrt(d); + float u = (-q + z) / 2; + float v = (-q - z) / 2; + u = stbtt__cuberoot(u); + v = stbtt__cuberoot(v); + r[0] = s + u + v; + return 1; + } else { + float u = (float) STBTT_sqrt(-p/3); + float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative + float m = (float) STBTT_cos(v); + float n = (float) STBTT_cos(v-3.141592/2)*1.732050808f; + r[0] = s + u * 2 * m; + r[1] = s - u * (m + n); + r[2] = s - u * (m - n); + + //STBTT_assert( STBTT_fabs(((r[0]+a)*r[0]+b)*r[0]+c) < 0.05f); // these asserts may not be safe at all scales, though they're in bezier t parameter units so maybe? + //STBTT_assert( STBTT_fabs(((r[1]+a)*r[1]+b)*r[1]+c) < 0.05f); + //STBTT_assert( STBTT_fabs(((r[2]+a)*r[2]+b)*r[2]+c) < 0.05f); + return 3; + } +} + +STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float scale, int glyph, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + float scale_x = scale, scale_y = scale; + int ix0,iy0,ix1,iy1; + int w,h; + unsigned char *data; + + if (scale == 0) return NULL; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale, scale, 0.0f,0.0f, &ix0,&iy0,&ix1,&iy1); + + // if empty, return NULL + if (ix0 == ix1 || iy0 == iy1) + return NULL; + + ix0 -= padding; + iy0 -= padding; + ix1 += padding; + iy1 += padding; + + w = (ix1 - ix0); + h = (iy1 - iy0); + + if (width ) *width = w; + if (height) *height = h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + // invert for y-downwards bitmaps + scale_y = -scale_y; + + { + int x,y,i,j; + float *precompute; + stbtt_vertex *verts; + int num_verts = stbtt_GetGlyphShape(info, glyph, &verts); + data = (unsigned char *) STBTT_malloc(w * h, info->userdata); + precompute = (float *) STBTT_malloc(num_verts * sizeof(float), info->userdata); + + for (i=0,j=num_verts-1; i < num_verts; j=i++) { + if (verts[i].type == STBTT_vline) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; + float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); + precompute[i] = (dist == 0) ? 0.0f : 1.0f / dist; + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; + float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; + float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float len2 = bx*bx + by*by; + if (len2 != 0.0f) + precompute[i] = 1.0f / (bx*bx + by*by); + else + precompute[i] = 0.0f; + } else + precompute[i] = 0.0f; + } + + for (y=iy0; y < iy1; ++y) { + for (x=ix0; x < ix1; ++x) { + float val; + float min_dist = 999999.0f; + float sx = (float) x + 0.5f; + float sy = (float) y + 0.5f; + float x_gspace = (sx / scale_x); + float y_gspace = (sy / scale_y); + + int winding = stbtt__compute_crossings_x(x_gspace, y_gspace, num_verts, verts); // @OPTIMIZE: this could just be a rasterization, but needs to be line vs. non-tesselated curves so a new path + + for (i=0; i < num_verts; ++i) { + float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; + + if (verts[i].type == STBTT_vline && precompute[i] != 0.0f) { + float x1 = verts[i-1].x*scale_x, y1 = verts[i-1].y*scale_y; + + float dist,dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + // coarse culling against bbox + //if (sx > STBTT_min(x0,x1)-min_dist && sx < STBTT_max(x0,x1)+min_dist && + // sy > STBTT_min(y0,y1)-min_dist && sy < STBTT_max(y0,y1)+min_dist) + dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; + STBTT_assert(i != 0); + if (dist < min_dist) { + // check position along line + // x' = x0 + t*(x1-x0), y' = y0 + t*(y1-y0) + // minimize (x'-sx)*(x'-sx)+(y'-sy)*(y'-sy) + float dx = x1-x0, dy = y1-y0; + float px = x0-sx, py = y0-sy; + // minimize (px+t*dx)^2 + (py+t*dy)^2 = px*px + 2*px*dx*t + t^2*dx*dx + py*py + 2*py*dy*t + t^2*dy*dy + // derivative: 2*px*dx + 2*py*dy + (2*dx*dx+2*dy*dy)*t, set to 0 and solve + float t = -(px*dx + py*dy) / (dx*dx + dy*dy); + if (t >= 0.0f && t <= 1.0f) + min_dist = dist; + } + } else if (verts[i].type == STBTT_vcurve) { + float x2 = verts[i-1].x *scale_x, y2 = verts[i-1].y *scale_y; + float x1 = verts[i ].cx*scale_x, y1 = verts[i ].cy*scale_y; + float box_x0 = STBTT_min(STBTT_min(x0,x1),x2); + float box_y0 = STBTT_min(STBTT_min(y0,y1),y2); + float box_x1 = STBTT_max(STBTT_max(x0,x1),x2); + float box_y1 = STBTT_max(STBTT_max(y0,y1),y2); + // coarse culling against bbox to avoid computing cubic unnecessarily + if (sx > box_x0-min_dist && sx < box_x1+min_dist && sy > box_y0-min_dist && sy < box_y1+min_dist) { + int num=0; + float ax = x1-x0, ay = y1-y0; + float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; + float mx = x0 - sx, my = y0 - sy; + float res[3] = {0.f,0.f,0.f}; + float px,py,t,it,dist2; + float a_inv = precompute[i]; + if (a_inv == 0.0) { // if a_inv is 0, it's 2nd degree so use quadratic formula + float a = 3*(ax*bx + ay*by); + float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); + float c = mx*ax+my*ay; + if (a == 0.0) { // if a is 0, it's linear + if (b != 0.0) { + res[num++] = -c/b; + } + } else { + float discriminant = b*b - 4*a*c; + if (discriminant < 0) + num = 0; + else { + float root = (float) STBTT_sqrt(discriminant); + res[0] = (-b - root)/(2*a); + res[1] = (-b + root)/(2*a); + num = 2; // don't bother distinguishing 1-solution case, as code below will still work + } + } + } else { + float b = 3*(ax*bx + ay*by) * a_inv; // could precompute this as it doesn't depend on sample point + float c = (2*(ax*ax + ay*ay) + (mx*bx+my*by)) * a_inv; + float d = (mx*ax+my*ay) * a_inv; + num = stbtt__solve_cubic(b, c, d, res); + } + dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + + if (num >= 1 && res[0] >= 0.0f && res[0] <= 1.0f) { + t = res[0], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 2 && res[1] >= 0.0f && res[1] <= 1.0f) { + t = res[1], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + if (num >= 3 && res[2] >= 0.0f && res[2] <= 1.0f) { + t = res[2], it = 1.0f - t; + px = it*it*x0 + 2*t*it*x1 + t*t*x2; + py = it*it*y0 + 2*t*it*y1 + t*t*y2; + dist2 = (px-sx)*(px-sx) + (py-sy)*(py-sy); + if (dist2 < min_dist * min_dist) + min_dist = (float) STBTT_sqrt(dist2); + } + } + } + } + if (winding == 0) + min_dist = -min_dist; // if outside the shape, value is negative + val = onedge_value + pixel_dist_scale * min_dist; + if (val < 0) + val = 0; + else if (val > 255) + val = 255; + data[(y-iy0)*w+(x-ix0)] = (unsigned char) val; + } + } + STBTT_free(precompute, info->userdata); + STBTT_free(verts, info->userdata); + } + return data; +} + +STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphSDF(info, scale, stbtt_FindGlyphIndex(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff); +} + +STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +STBTT_DEF const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8); + stbtt_int32 off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + slen = ttUSHORT(fc+loc+12+8); + off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian_internal((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +static int stbtt_FindMatchingFont_internal(unsigned char *font_collection, char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + +STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, + float pixel_height, unsigned char *pixels, int pw, int ph, + int first_char, int num_chars, stbtt_bakedchar *chardata) +{ + return stbtt_BakeFontBitmap_internal((unsigned char *) data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata); +} + +STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) +{ + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); +} + +STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) +{ + return stbtt_GetNumberOfFonts_internal((unsigned char *) data); +} + +STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset) +{ + return stbtt_InitFont_internal(info, (unsigned char *) data, offset); +} + +STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags) +{ + return stbtt_FindMatchingFont_internal((unsigned char *) fontdata, (char *) name, flags); +} + +STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return stbtt_CompareUTF8toUTF16_bigendian_internal((char *) s1, len1, (char *) s2, len2); +} + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() +// 1.19 (2018-02-11) OpenType GPOS kerning (horizontal only), STBTT_fmod +// 1.18 (2018-01-29) add missing function +// 1.17 (2017-07-23) make more arguments const; doc fix +// 1.16 (2017-07-12) SDF support +// 1.15 (2017-03-03) make more arguments const +// 1.14 (2017-01-16) num-fonts-in-TTC function +// 1.13 (2017-01-02) support OpenType fonts, certain Apple fonts +// 1.12 (2016-10-25) suppress warnings about casting away const with -Wcast-qual +// 1.11 (2016-04-02) fix unused-variable warning +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/thirdparty/raylib b/thirdparty/raylib new file mode 160000 index 00000000..c1ab645c --- /dev/null +++ b/thirdparty/raylib @@ -0,0 +1 @@ +Subproject commit c1ab645ca298a2801097931d1079b10ff7eb9df8