From af997a86bd3ace2cdd597143c1564acb053cbc4a Mon Sep 17 00:00:00 2001 From: ratheron Date: Tue, 22 Sep 2026 10:15:58 +0200 Subject: [PATCH 1/6] Add drone renders --- docs/gen_drone_renders.py | 91 ++++++++++++ docs/index.md | 31 ++-- docs/javascripts/showcase.js | 59 +++++--- docs/stylesheets/extra.css | 28 ++-- docs/user-guide/adding-drones.md | 2 + pixi.lock | 239 ++++++++++++++++++++++++++++++- properdocs.yml | 1 + pyproject.toml | 5 + 8 files changed, 412 insertions(+), 44 deletions(-) create mode 100644 docs/gen_drone_renders.py diff --git a/docs/gen_drone_renders.py b/docs/gen_drone_renders.py new file mode 100644 index 000000000..09ea0e3ed --- /dev/null +++ b/docs/gen_drone_renders.py @@ -0,0 +1,91 @@ +"""Render one image of every supported drone for the documentation. + +This script is executed by the mkdocs-gen-files plugin during ``mkdocs build`` or +``mkdocs serve``. The images are written to ``img/drones/.png`` in the virtual docs tree +and are never committed. Rendering is headless via EGL on Linux and CGL on macOS unless +``MUJOCO_GL`` is already set. +""" + +import logging +import os +import sys +from pathlib import Path + +if sys.platform == "linux": + os.environ.setdefault("MUJOCO_GL", "egl") +elif sys.platform == "darwin": + os.environ.setdefault("MUJOCO_GL", "cgl") + +try: + import mkdocs_gen_files +except ImportError: + pass # not running in a docs environment — nothing to generate +else: + import imageio.v3 as iio + import mujoco + import numpy as np + + from crazyflow.drones import Drone + + log = logging.getLogger("mkdocs.plugins.gen_drone_renders") + + DRONE_DIR = Path("crazyflow/drones") + WIDTH, HEIGHT = 2000, 1500 + MARGIN = 80 + + def render(name: str) -> np.ndarray: + """Render a drone in front of a transparent background, cropped to its bounding box.""" + spec = mujoco.MjSpec.from_file(str(DRONE_DIR / f"{name}.xml")) + for body in list(spec.worldbody.bodies): + if body.name != "drone": + spec.delete(body) + for material in spec.materials: + if 0 < material.rgba[3] < 1: + material.rgba[3] = 1 + spec.visual.global_.offwidth = WIDTH + spec.visual.global_.offheight = HEIGHT + spec.visual.quality.shadowsize = 8192 + spec.visual.quality.offsamples = 16 + spec.visual.headlight.ambient = [0.35, 0.35, 0.35] + spec.visual.headlight.diffuse = [0.5, 0.5, 0.5] + spec.worldbody.add_light( + pos=[0.4, -0.3, 0.6], dir=[-0.55, 0.4, -0.8], diffuse=[0.6, 0.6, 0.6], castshadow=True + ) + spec.worldbody.add_light( + pos=[-0.4, 0.3, 0.3], dir=[0.6, -0.45, -0.45], diffuse=[0.3, 0.3, 0.3], castshadow=False + ) + model = spec.compile() + data = mujoco.MjData(model) + mujoco.mj_forward(model, data) + + camera = mujoco.MjvCamera() + camera.lookat[:] = model.body("drone").pos + camera.distance, camera.azimuth, camera.elevation = 0.28, 135, -28 + option = mujoco.MjvOption() + option.geomgroup[:] = 0 + option.geomgroup[2] = 1 + + renderer = mujoco.Renderer(model, height=HEIGHT, width=WIDTH) + try: + renderer.update_scene(data, camera, option) + rgb = renderer.render() + renderer.enable_segmentation_rendering() + renderer.update_scene(data, camera, option) + mask = renderer.render()[..., 0] != -1 + finally: + renderer.close() + + rows, cols = np.nonzero(mask) + y0, y1 = max(rows.min() - MARGIN, 0), min(rows.max() + MARGIN, HEIGHT) + x0, x1 = max(cols.min() - MARGIN, 0), min(cols.max() + MARGIN, WIDTH) + alpha = mask.astype(np.uint8) * 255 + return np.dstack([rgb, alpha])[y0:y1, x0:x1] + + for name in Drone: + try: + image = render(name) + except Exception as e: + log.warning(f"Could not render drone '{name}': {e!r}") + continue + with mkdocs_gen_files.open(f"img/drones/{name}.png", "wb") as fd: + fd.write(iio.imwrite("", image, extension=".png")) diff --git a/docs/index.md b/docs/index.md index bc2b5b1c5..501796a1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,18 +85,33 @@ Crazyflow is a research simulator for Crazyflie-style quadrotors that runs milli ## Supported drones - All available drones are members of `crazyflow.Drone`. See [Available drone configurations](user-guide/dynamics/parametrize.md#available-drone-configurations) for the full list and how to select one. diff --git a/docs/javascripts/showcase.js b/docs/javascripts/showcase.js index 91cab3f75..740f8e02a 100644 --- a/docs/javascripts/showcase.js +++ b/docs/javascripts/showcase.js @@ -1,7 +1,8 @@ -function initializeCarousel() { - const carouselContainer = document.querySelector(".carousel-container"); +const carousels = []; +let focusedCarousel = null; - if (!carouselContainer || carouselContainer.dataset.carouselReady === "true") { +function initializeCarousel(carouselContainer) { + if (carouselContainer.dataset.carouselReady === "true") { return; } @@ -66,17 +67,12 @@ function initializeCarousel() { nextBtn.addEventListener("click", nextSlide); } - document.addEventListener("keydown", (event) => { - if (!document.querySelector(".carousel-container")) { - return; - } - - if (event.key === "ArrowLeft") { - prevSlide(); - } else if (event.key === "ArrowRight") { - nextSlide(); - } - }); + const api = { container: carouselContainer, nextSlide, prevSlide }; + const focus = () => { + focusedCarousel = api; + }; + carouselContainer.addEventListener("pointerenter", focus); + carouselContainer.addEventListener("focusin", focus); let touchStartX = 0; let touchEndX = 0; @@ -84,9 +80,10 @@ function initializeCarousel() { carousel.addEventListener( "touchstart", (event) => { + focus(); touchStartX = event.changedTouches[0].screenX; }, - false + { passive: true } ); carousel.addEventListener( @@ -95,7 +92,7 @@ function initializeCarousel() { touchEndX = event.changedTouches[0].screenX; handleSwipe(); }, - false + { passive: true } ); function handleSwipe() { @@ -113,13 +110,37 @@ function initializeCarousel() { } } + carousels.push(api); goToSlide(0); } +function initializeCarousels() { + for (let i = carousels.length - 1; i >= 0; i--) { + if (!carousels[i].container.isConnected) { + carousels.splice(i, 1); + } + } + document.querySelectorAll(".carousel-container").forEach(initializeCarousel); +} + +document.addEventListener("keydown", (event) => { + if (carousels.length === 0 || (event.key !== "ArrowLeft" && event.key !== "ArrowRight")) { + return; + } + + const target = carousels.includes(focusedCarousel) ? focusedCarousel : carousels[0]; + + if (event.key === "ArrowLeft") { + target.prevSlide(); + } else { + target.nextSlide(); + } +}); + if (typeof document$ !== "undefined") { - document$.subscribe(initializeCarousel); + document$.subscribe(initializeCarousels); } else if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", initializeCarousel); + document.addEventListener("DOMContentLoaded", initializeCarousels); } else { - initializeCarousel(); + initializeCarousels(); } diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 29da1f7f5..127f3bca5 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -102,6 +102,24 @@ text-align: center; } +.carousel-slide-image { + background: linear-gradient(180deg, #fafafa, #d9d9d9); +} + +.carousel-slide img { + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 1.5rem 1.5rem 3rem; + display: block; + object-fit: contain; +} + +.carousel-slide-image .carousel-caption { + background: none; + color: #1a1a1a; +} + .carousel-btn { position: absolute; top: 50%; @@ -219,16 +237,6 @@ } } -/* Drone grid */ -.drone-grid table { - margin: 0 auto; -} - -.drone-grid td { - vertical-align: middle; - text-align: center; -} - /* Example page media */ .example-media, .example-media-grid { diff --git a/docs/user-guide/adding-drones.md b/docs/user-guide/adding-drones.md index 1689abd22..4f60e9654 100644 --- a/docs/user-guide/adding-drones.md +++ b/docs/user-guide/adding-drones.md @@ -34,6 +34,8 @@ Add `[.core]`, `[.state2attitude]`, `[.attitude2force_torque]` Add the platform to the table in [Parametrization](dynamics/parametrize.md#available-drone-configurations). +The docs build renders every `Drone` member from its MJCF file, so a new drone gets an image at `img/drones/.png` without further work. Add a slide for it to the carousel on the [home page](../index.md#supported-drones). + ## 5. Run the tests The test suite parametrizes over `Drone` and over the supported drone-dynamics pairs, so the new drone is tested without any changes to the tests. diff --git a/pixi.lock b/pixi.lock index d7dc07afd..60a30ca13 100644 --- a/pixi.lock +++ b/pixi.lock @@ -200,7 +200,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-9_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-9_h0358290_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-hd45a770_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda @@ -427,7 +427,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-9_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-9_h0358290_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-hd45a770_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda @@ -698,19 +698,27 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-9_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-9_h0358290_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-hd45a770_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.129-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.1.0-ha9f2e26_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.1.0-h69a702a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.1.0-h79bb938_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.1.0-he0feb66_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h0cb94f2_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-9_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm23-23.1.3-h474f4eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvmspirv23-23.1.2-h7ef34fa_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h922cc85_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.1.0-h934c35e_2.conda @@ -718,8 +726,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.2-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-hb83e432_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.4-hf3af7cc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.4-h7df9aa5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-lavapipe-26.2.4-h1d927ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-libgallium-26.2.4-h1d927ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-llvmpipe-26.2.4-h62484d9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mesalib-26.2.4-h31ca1b8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.2-py314h2b28147_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda @@ -730,10 +744,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.16.3-hc991c3d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/spirv-tools-2026.4-h7148c6a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py314h9e666f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.5-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxshmfence-1.3.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.7-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-hebe6cf0_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hce19668_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda @@ -1352,7 +1373,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-9_h4a7cf45_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-9_h0358290_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-hd45a770_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda @@ -1648,7 +1669,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h373387f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda @@ -1687,8 +1708,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-hb83e432_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-h51789e4_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.4-hf3af7cc_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.4-h7df9aa5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.1-py314h815e797_2.conda @@ -2569,6 +2590,20 @@ packages: run_exports: {} size: 77856 timestamp: 1781203599810 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.5-hd2095e1_0.conda + sha256: 6de73033a7c5f712c9c5a54124e4581e025691029835e5bceaf10a333a7e7bc0 + md5: 72cfd2b95b0540247220b42f58a148d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + constrains: + - expat 2.8.5.* + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 77893 + timestamp: 1790346247149 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h3435931_0.conda sha256: ac38603008bf1e99b8ed379b1a656a67a70e2841f2b6a069c630cdf6316012d2 md5: 0abe40a9880086ca4d2e5daf09dceff9 @@ -2844,6 +2879,42 @@ packages: - libllvm22 >=22.1.8,<22.2.0a0 size: 44320272 timestamp: 1781788728739 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm23-23.1.3-h474f4eb_0.conda + sha256: 7ce347e2da1502442d4df406101cfaf429927b7a9410badadd3f147d3cbf2d00 + md5: cd6352703382608abc83ad5120abddd5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - libstdcxx >=15 + - libxml2 + - libxml2-16 >=2.15.4 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + run_exports: + weak: + - libllvm23 >=23.1.3,<23.2.0a0 + size: 45082185 + timestamp: 1791345280243 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libllvmspirv23-23.1.2-h7ef34fa_0.conda + sha256: 6a49c3dd3b4b13705babcd51a8a7eb60d060a893a71c381504e1e817158022c0 + md5: c57e3c90726879738e06da536ddfe720 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - libllvm23 >=23.1.2,<23.2.0a0 + - libstdcxx >=15 + constrains: + - llvm-spirv 23.1.2.* *_0 + - llvmdev 23.1.* + license: NCSA + license_family: BSD + purls: [] + run_exports: {} + size: 1297050 + timestamp: 1791335048656 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda sha256: 9787df8c22a59c9a70d3e5a10db9ad663485e75e9ccc3f09bd092cb7b95e0dab md5: 1390b7c5ac0b1d8e447bc5efa6d3c8c2 @@ -3155,6 +3226,24 @@ packages: run_exports: {} size: 559775 timestamp: 1776376739004 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.4-hf3af7cc_1.conda + sha256: d1906463070c21f3215cd5de93164f31c44d5bac6ae9b5d8f5adf42b1e304039 + md5: 046d9eada488e1a1657c80b55c23da52 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=15 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libzlib >=1.3.2,<2.0a0 + constrains: + - libxml2 2.15.4 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 569062 + timestamp: 1791465631497 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda sha256: 3bc5551720c58591f6ea1146f7d1539c734ed1c40e7b9f5cb8cb7e900c509aba md5: 995d8c8bad2a3cc8db14675a153dec2b @@ -3175,6 +3264,26 @@ packages: - libxml2-16 >=2.15.3 size: 46810 timestamp: 1776376751152 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.4-h7df9aa5_1.conda + sha256: 8c32be9e8720bc413d2b4267517459d7d71fad4a5f398a2aa8319b6d7b8a00e6 + md5: 51e5162f95bd6840f6b2b3169125fff5 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.3,<79.0a0 + - libgcc >=15 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.3,<6.0a0 + - libxml2-16 2.15.4 hf3af7cc_1 + - libzlib >=1.3.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libxml2 + - libxml2-16 >=2.15.4 + size: 46570 + timestamp: 1791465635923 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda sha256: 0694760a3e62bdc659d90a14ae9c6e132b525a7900e59785b18a08bb52a5d7e5 md5: 87e6096ec6d542d1c1f8b33245fe8300 @@ -3270,6 +3379,91 @@ packages: run_exports: {} size: 9068707 timestamp: 1785211110030 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-lavapipe-26.2.4-h1d927ee_0.conda + sha256: b2682aa9114d12ab30d87607c73e62c2412647cd244ac2fdd53c2d47b13b03aa + md5: 395c964e984a8c4d59edf6a8a251a892 + depends: + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=15 + - libdrm >=2.4.129,<2.5.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libxcb >=1.17.0,<2.0a0 + - xorg-libxrandr >=1.5.5,<2.0a0 + - xorg-libxshmfence >=1.3.3,<2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libllvm23 >=23.1.2,<23.2.0a0 + - libexpat >=2.8.5,<3.0a0 + - libllvmspirv23 >=23.1.1,<24.0a0 + - spirv-tools >=2026,<2027.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - mesa-lavapipe >=26.2.4,<26.3.0a0 + size: 2972566 + timestamp: 1790880291644 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-libgallium-26.2.4-h1d927ee_0.conda + sha256: 0066898c8c32ecd6652453649cd407a4d8e2b7d48f68d220ce528764a27be5ea + md5: 467c0e95420cc83aadcd70cdc619ec4e + depends: + - libgcc >=15 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=15 + - libxcb >=1.17.0,<2.0a0 + - xorg-libxxf86vm >=1.1.7,<2.0a0 + - spirv-tools >=2026,<2027.0a0 + - xorg-libxrandr >=1.5.5,<2.0a0 + - xorg-libxshmfence >=1.3.3,<2.0a0 + - libegl >=1.7.0,<2.0a0 + - libllvmspirv23 >=23.1.1,<24.0a0 + - libglvnd >=1.7.0,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.2,<2.0a0 + - libglx >=1.7.0,<2.0a0 + - xorg-libx11 >=1.8.13,<2.0a0 + - xorg-libxext >=1.3.7,<2.0a0 + - libdrm >=2.4.129,<2.5.0a0 + - libllvm23 >=23.1.2,<23.2.0a0 + - libexpat >=2.8.5,<3.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - mesa-libgallium >=26.2.4,<26.3.0a0 + size: 11116114 + timestamp: 1790880291644 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mesa-llvmpipe-26.2.4-h62484d9_0.conda + sha256: 77f47db81dabcc979c9aec8b524206e124e8db7349f0cc528b221841f07d8e03 + md5: 6ae935568e11782fd1c5704b273ede8c + depends: + - mesa-libgallium ==26.2.4 h1d927ee_0 + - mesa-libgallium >=26.2.4,<26.3.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - mesa-llvmpipe >=26.2.4,<26.3.0a0 + size: 16678 + timestamp: 1790880291644 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mesalib-26.2.4-h31ca1b8_0.conda + sha256: 89ec4794588070bae443570b527a4b414808b387eaa145c86a590bb93a9c25bc + md5: 3e4c0e42baa15e86872b9d73537449d9 + depends: + - mesa-lavapipe ==26.2.4 h1d927ee_0 + - mesa-llvmpipe ==26.2.4 h62484d9_0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - mesalib >=26.2.4,<26.3.0a0 + size: 12451 + timestamp: 1790880291644 - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda sha256: 5d46557214ed184381dafe835b7c94a474a1c3b307a08a250b1ea4779b44ffb3 md5: ee6c0cd80a60961a1f48aa3e0b91f986 @@ -3621,6 +3815,23 @@ packages: run_exports: {} size: 9549002 timestamp: 1786872517294 +- conda: https://conda.anaconda.org/conda-forge/linux-64/spirv-tools-2026.4-h7148c6a_0.conda + sha256: a24d01b6ebb6050734eb305ed08ba194b40f0d6682244485bb94f9c5f3588095 + md5: cb82bffc815f76d0053330d7cb17ebfc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - libstdcxx >=15 + constrains: + - spirv-headers >=1.4.363.0,<1.4.363.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - spirv-tools >=2026,<2027.0a0 + size: 2413550 + timestamp: 1791365453317 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda build_number: 103 sha256: 43624eab22f5f29df7d6ffe914cf442f28fd559b55b290906255492826e636e8 @@ -4004,6 +4215,20 @@ packages: - xorg-libxrender >=0.9.12,<0.10.0a0 size: 34645 timestamp: 1787100191192 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxshmfence-1.3.3-hb9d3cd8_0.conda + sha256: c0830fe9fa78d609cd9021f797307e7e0715ef5122be3f784765dad1b4d8a193 + md5: 9a809ce9f65460195777f2f2116bae02 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - xorg-libxshmfence >=1.3.3,<2.0a0 + size: 12302 + timestamp: 1734168591429 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f diff --git a/properdocs.yml b/properdocs.yml index a92f12fca..d69f47baa 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -90,6 +90,7 @@ plugins: - gen-files: scripts: - docs/gen_ref_pages.py + - docs/gen_drone_renders.py - literate-nav: nav_file: SUMMARY.md - section-index diff --git a/pyproject.toml b/pyproject.toml index f356d9df2..2c2aa85bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -204,6 +204,11 @@ mkdocs-literate-nav = ">=0.6.0" mkdocs-section-index = ">=0.3.0" mkdocs-charts-plugin = ">=0.0.10" +# Headless OpenGL for rendering the drone images during the docs build +[tool.pixi.feature.docs.target.linux-64.dependencies] +libegl = "*" +mesalib = "*" + [tool.pixi.feature.docs.tasks] docs-build = { cmd = "properdocs build --strict", description = "Build docs" } docs-serve = { cmd = "properdocs serve --livereload", description = "Serve docs locally" } From c53b074374aca6f9eb3a04c59568edeaf426bae9 Mon Sep 17 00:00:00 2001 From: ratheron Date: Tue, 22 Sep 2026 13:53:46 +0200 Subject: [PATCH 2/6] Fix rendering script --- docs/gen_drone_renders.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/gen_drone_renders.py b/docs/gen_drone_renders.py index 09ea0e3ed..c84e694b7 100644 --- a/docs/gen_drone_renders.py +++ b/docs/gen_drone_renders.py @@ -13,6 +13,13 @@ if sys.platform == "linux": os.environ.setdefault("MUJOCO_GL", "egl") + # libglvnd only searches /usr/share/glvnd, which does not exist on a minimal CI runner. Append + # the Mesa driver of this environment so EGL still finds a (software) device there. + _vendors = Path(sys.prefix, "share/glvnd/egl_vendor.d") + if _vendors.is_dir(): + os.environ.setdefault( + "__EGL_VENDOR_LIBRARY_DIRS", f"/usr/share/glvnd/egl_vendor.d:{_vendors}" + ) elif sys.platform == "darwin": os.environ.setdefault("MUJOCO_GL", "cgl") @@ -81,11 +88,21 @@ def render(name: str) -> np.ndarray: alpha = mask.astype(np.uint8) * 255 return np.dstack([rgb, alpha])[y0:y1, x0:x1] - for name in Drone: + def gl_available() -> bool: + """Check for a usable OpenGL context, so an unusable one is reported only once.""" try: - image = render(name) + mujoco.Renderer(mujoco.MjModel.from_xml_string(""), 4, 4).close() except Exception as e: - log.warning(f"Could not render drone '{name}': {e!r}") - continue - with mkdocs_gen_files.open(f"img/drones/{name}.png", "wb") as fd: - fd.write(iio.imwrite("", image, extension=".png")) + log.warning(f"Could not create an OpenGL context, skipping the drone renders: {e!r}") + return False + return True + + if gl_available(): + for drone in Drone: + try: + image = render(drone) + except Exception as e: + log.warning(f"Could not render drone '{drone}': {e!r}") + continue + with mkdocs_gen_files.open(f"img/drones/{drone}.png", "wb") as fd: + fd.write(iio.imwrite("", image, extension=".png")) From 717599429f2ac2e4c52dc47e187512adc478af53 Mon Sep 17 00:00:00 2001 From: ratheron Date: Fri, 9 Oct 2026 12:03:04 +0200 Subject: [PATCH 3/6] Fix rendering and add video --- docs/gen_drone_renders.py | 3 ++- docs/index.md | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/gen_drone_renders.py b/docs/gen_drone_renders.py index c84e694b7..d550b4569 100644 --- a/docs/gen_drone_renders.py +++ b/docs/gen_drone_renders.py @@ -67,7 +67,8 @@ def render(name: str) -> np.ndarray: camera = mujoco.MjvCamera() camera.lookat[:] = model.body("drone").pos - camera.distance, camera.azimuth, camera.elevation = 0.28, 135, -28 + camera.distance = max(0.28, 1.1 * model.stat.extent) + camera.azimuth, camera.elevation = 135, -28 option = mujoco.MjvOption() option.geomgroup[:] = 0 option.geomgroup[2] = 1 diff --git a/docs/index.md b/docs/index.md index 501796a1f..2778f96da 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,16 @@ Crazyflow is a research simulator for Crazyflie-style quadrotors that runs milli > +