scimesh Changes
===============

Version 0.4.0 -- Batch primitives, tubes, line layers and text labels
-------------------------------------------------------------------

- NEW: polyline tubes.  `generate_tube()` sweeps a circular cross-section
  along a path of points, and `generate_multi_tubes()` batches many such paths
  into a single mesh.  The cross-section frames are computed by parallel
  transport (rotation-minimizing frames), so tubes do not twist around their own
  axis, and mitered joints keep the segments watertight.  R: `generate_tube()`
  and `generate_tubes()`.  Useful for curved edges, streamlines and any shape
  that is not a straight cylinder.
- NEW: spline sampling — smooth curves through ordered 3D points.  A new
  header-only module (`src/core/scimesh/spline.h`) turns a coarse list of
  waypoints into a dense path, which is what the path-taking features of the
  library need and what callers previously had to bring themselves (the
  `generate_tube()` documentation promised "arcs, Bezier samples, streamlines"
  without offering a way to produce one).  `catmull_rom_path()` interpolates the
  input points and is the default choice; it is **centripetal** by default,
  which is what keeps unevenly spaced points (the norm for measured data) from
  producing cusps, loops and self-intersections, and `alpha = 0` recovers the
  textbook uniform curve.  `hermite_path()` takes caller-supplied tangents
  instead of deriving them, `bspline_path()` approximates the points (for
  smoothing noisy input, at the cost of not passing through them) and
  `bezier_path()` samples a control polygon with de Casteljau.  Around those,
  `resample_by_arclength()` evens out the spacing along a path — the sweep
  places exactly one cross-section per path point, so uniform *arc length*
  spacing is what makes a tube look evenly subdivided — and `path_length()`,
  `path_tangents()` and `path_curvature()` are the queries that drive it.
  `path_curvature()` is the one worth knowing about: sweeping a tube of radius
  `r` along a curve whose curvature reaches `kappa` folds the tube inside out,
  so `1 / max(path_curvature(path))` is the largest radius a path can take.
  Open curves get a reflected phantom neighbour at both ends (so they start and
  end exactly at the first and last point, along the outer segment), closed
  curves use their wrapped neighbours, and both `closed = true` cases end on a
  copy of the first point, so a swept tube closes on itself; the arc-length
  resampling of a closed path is covered in a whole number of steps so the seam
  is a regular step instead of a leftover gap.  This is pure geometry on
  `std::vector<Vec3>` — no mesh, no renderer, no I/O, no new dependency, and
  not a domain-specific feature — so it composes with primitives.h, lines.h and
  your own code alike, including camera paths.  R: `spline_path()`,
  `bezier_path()`, `resample_path()`, `path_length()` and `path_curvature()`.
  `examples/cpp/spline_tube/` and `examples/R/spline_tube/` render the same
  waypoints as a faceted polyline tube and as a smooth spline tube side by side,
  plus a closed trefoil knot.
- NEW: line layers — screen-space lines as first-class scene primitives.  A
  `LineLayer` stores segments, per-segment colors and a width **in pixels** and
  creates no geometry, which makes it the cheap way to draw thousands of thin
  lines (wireframes, graph or connectome edges, trajectories).  Line layers are
  drawn by `Renderer::render_scene()` in the same pass as the meshes (same
  camera, same depth buffer, back-to-front blending for translucent lines), they
  contribute to `Scene::compute_bounding_box()` unless their `affects_bounds`
  flag is false (see below), and the mesh exporters skip them
  (`scimesh_write_gltf()` warns).  R: `line_layer()`, the `lines` argument of
  `scene()`, and `render_segments()` for free-standing lines.
- NEW: line layers are content by default, i.e. they define the bounding box of
  their scene exactly like a mesh does, and the camera fitted to the scene
  covers them.  This is what makes a scene that contains only lines drawable at
  all: a tractogram or a connectome without a brain surface used to have no
  geometry to derive a camera from.  Lines that are decoration rather than
  content (a leader line pointing at a label, an axis cross, a scale bar drawn
  as segments) can opt out with `LineLayer::affects_bounds = false` (R:
  `line_layer(..., affects_bounds = FALSE)`, and
  `scene_set_line_affects_bounds(scene, index/name, affects_bounds)` for a layer
  that is already part of a scene), so that adding such a layer can never push
  the camera away from the data.  A scene without meshes is framed by its lines
  even if they all opted out, since there is nothing else to fit.  Note that
  text layers are still ignored by the bounding box, because the extent of a
  label depends on the font and the output size rather than on world space.
- NEW: R function `camera_fit_scene()` fits a camera to a whole scene, i.e. to
  its meshes together with the line layers that affect the bounds.  It is the
  scene counterpart of `camera_auto()` (which takes meshes), and the two share
  the same framing convention (bounding box center, distance from the extent).
  `camera_auto()` now also documents that it does not consider scene contents.
- NEW: `Renderer::render_lines_raw()` renders line segments directly, and
  `Rasterizer::rasterize_line()` rasterizes a segment with a screen-space width
  (flat/unlit by default, like hardware line rendering).
- NEW: R functions `generate_multi_spheres()` and `generate_multi_cylinders()`
  expose the batched primitive generators (one mesh for thousands of spheres or
  cylinders), which previously existed only in C++.
- NEW: `caps` argument for `generate_cylinder()` and `generate_multi_cylinders()`
  (R and C++).  Open cylinders/tubes use about half the vertices and triangles,
  which matters for large edge bundles whose ends are hidden inside node
  spheres.  The default is unchanged (`caps = TRUE`).
- PERF: the batched generators (`generate_multi_spheres()`,
  `generate_multi_cylinders()`, `generate_multi_tubes()`) reserve the exact
  final size before merging, instead of growing the arrays incrementally.
- FIX: the batched generators no longer read out of bounds when the `radii` or
  `colors` array is empty (missing entries are recycled from the first entry;
  an empty array now means "use the default", i.e. radius 1.0 and white).
- FIX: `Rasterizer::rasterize_line()` qualified the shadowed `width`/`height`
  members in its pixel bounds check (the `width` parameter of the function
  shadowed `Rasterizer::width`), which made the line invisible.
- FIX: the C++ core now compiles with libc++ (clang on macOS).
  `src/core/primitives.cpp` used `std::array` (pyramid and tetrahedron faces)
  without including `<array>`, and `std::swap` without `<utility>`.  libstdc++
  (GCC, i.e. the Linux and Windows builds) pulls both in transitively, so the
  bug was invisible there; libc++ does not, so the package failed to install on
  macOS with "implicit instantiation of undefined template 'std::array<...>'".
  libc++ only forward-declares `std::array` in `<__tuple>` (for `tuple_size`
  and `tuple_element`), which is why the message says "undefined template"
  instead of "no member named array".  This is the installation ERROR reported
  by the CRAN checks for 0.3.4 on r-release-macos-x86_64, r-oldrel-macos-x86_64
  and r-oldrel-macos-arm64.  All source files are now verified to compile with
  libc++ 14 (the toolchain used by those three flavors) and libc++ 18, as well
  as with libstdc++.
- NEW: global anti-aliasing default.  `render_options()` now takes
  `aa_samples = NULL` and resolves that `NULL` from the global option
  `scimesh.aa_samples` (default 1, i.e. no AA).  Setting
  `options(scimesh.aa_samples = 2)` therefore switches on 2x2 supersampling for
  every render call that does not pass `aa_samples` explicitly (2 for 2x2, 4 for
  4x4 SSAA).  This is mainly interesting for screen-space lines and points,
  whose hard edges become smooth boundaries under supersampling; the cost is
  proportional to `aa_samples^2` in render time and memory.  Passing a number
  explicitly still overrides the option, and invalid values (including an
  invalid global option) are reported as errors instead of being ignored.
- NEW: text labels — annotations for figures, without any geometry.  A
  `TextLayer` stores strings with anchor positions, a font size **in pixels**,
  colors and optional halos, and is drawn by `Renderer::render_scene()` after
  the meshes and the lines, so labels end up on top of the geometry.  Positions
  are either **world space** (projected with the scene camera, so a label sticks
  to the brain region or atom it annotates) or **screen space** (output pixels,
  which is what titles, panel tags and captions need).  Labels are billboards:
  they always face the camera and keep their pixel size, and they are hidden by
  geometry in front of their anchor unless `depth_test = FALSE`, so a world-space
  label behaves like an annotation *of* a surface instead of shining through the
  model.  `adj` places the anchor on the text box (as in `rgl`), pixel offsets
  nudge a label next to its anchor, multi-line labels are supported, and halos
  keep text readable on dark or busy geometry.  Glyphs are rasterized with
  `stb_truetype` (vendored, public domain) from the bundled
  `inst/extdata/Inter-Regular.ttf` (SIL OFL 1.1), which can be replaced per
  label or by setting the `SCIMESH_FONT` environment variable.  R:
  `text_layer()`, the `texts` argument of `scene()` and `render_text()` for
  free-standing labels; `text_extent()` measures labels for layout.  Labels can
  be rotated (`rotation`, in degrees counter-clockwise about the anchor, in the
  image plane): 90 turns a label into a vertical axis label, 180 into an
  upside-down one, and arbitrary angles let a label follow an annotation line.
  Unrotated labels keep the exact, integer-aligned rasterization path, so
  existing output is unchanged.
- NEW: the low-level font API in C++ — `Font` (load a `.ttf`, metrics, glyph
  coverage bitmaps, text measurement), `draw_text()` to draw text into any
  `Image` (including rotated, via `TextDrawStyle::rotation`), and
  `measure_text()` for layout decisions.  `Font::decode_utf8()`
  means UTF-8 labels ("°C", accented names) work.
- NEW: `world_to_screen()` (C++ and R) projects world coordinates to the pixels
  of a rendered image using the same matrices as the renderer, which is what
  screen-space annotations and callout lines need.
- NEW: CLI text options — `--text`, `--text-at`, `--text-size`, `--text-color`,
  `--text-halo`, `--text-halo-width`, `--text-font`, `--text-screen`,
  `--text-no-depth`, `--text-adj`, `--text-offset` and `--text-rotation`, plus
  a `[text]` section in `config.toml`, so figures can be annotated without
  writing any code.
- NEW: `flip_uvs()` (C++ and R) mirrors the `v` texture coordinate of a mesh
  (`v -> 1 - v`), converting UVs that use the bottom-left origin to scimesh's
  image-space UVs.  This is the documented one-liner for UVs that come from OBJ,
  PLY, rgl, OpenGL or Blender; `examples/cpp/spot_cow/` uses it.
- DOC: the texture-mapping documentation now states the UV convention instead of
  leaving it to be discovered.  scimesh UVs (`Mesh::uvs`, the R `uv` argument)
  are **image space**, like every other coordinate in the package: `v = 0` is
  the *top* row of the texture image, matching `Image::sample_bilinear()`, which
  the renderer calls with the UVs unchanged.  UVs from file formats and other
  tools (v = 0 at the bottom) have to be converted with `flip_uvs()`; the vignette
  and the `uv`/`texture` documentation say so, and a renderer test pins the
  orientation so it cannot drift silently.
- DOC/FIX: the vignette claimed that `read_obj()` extracts UVs and showed a
  textured-OBJ recipe on that basis.  No scimesh reader returns texture
  coordinates (`obj_io::read_obj()` and `ply_io::read_ply()` drop them), so the
  vignette, the `read_obj()`/`read_ply()` and `render_mesh()` documentation now
  say what actually happens and how to supply UVs yourself.  `obj_io.h` also
  promised `vn`/`vt` in its summary while listing them as unsupported.

Breaking changes
----------------
- BRK: clip planes are now defined in **world space** by default.  In 0.3.4 and
  earlier the `normal` and `offset` were interpreted in eye (camera) space, so
  the cut travelled with the camera and had to be re-tuned for every viewpoint.
  For the old behaviour, pass `PlaneSpace::EYE` (C++) or
  `clip_plane(..., space = "eye")` (R).  Note that the old behaviour was also
  documented incorrectly: the docs described a world-space plane.  See
  ClipPlane::space and ?clip_plane.
- BRK: fog distances (`fog_start`/`fog_end`) are now given in **world units**
  (distance from the camera) by default, and are therefore independent of the
  near/far planes and of the projection type.  The legacy interpretation as raw
  normalized device depth is still available via `FogSpace::NDC` (C++) or
  `fog_space = "ndc"` (R).  Previously the C++ docs claimed world units (which
  they were not) and the R docs claimed normalized depth in [0, 1] (the values
  were actually raw depth buffer values in [-1, 1]).
- NEW: R function `clip_plane()` creates and validates clip plane descriptors,
  with an explicit `space` argument ("world" or "eye"), plus examples for both.
- NEW: `render_options()` validates `clip_planes` (normal must be a finite,
  non-zero length-3 vector; `offset` a finite number; `space` valid) and the
  fog settings (finite, `fog_end > fog_start`).  Previously malformed clip
  planes were silently replaced by defaults in the C++ binding.
- NEW: `render_options()` exposes `near_plane` and `far_plane` (defaults 0.1 and
  10000, matching the C++ defaults), which define the depth range that NDC fog
  and mode "ndc" refer to.
- NEW: C++ `clip_plane_to_view_space()` converts a ClipPlane into the view space
  in which clipping happens (world planes are shifted by `dot(normal, eye)`).
  A zero-length normal now neutralizes its plane (clips nothing) instead of
  discarding the whole scene, and the normal is normalized without rescaling
  `offset`, so non-unit normals no longer move the plane.
- NEW: C++ `Rasterizer::fog_depth_from_ndc()` inverts the depth mapping of both
  perspective and orthographic projections, so world-space fog works for both.
- FIX: fog no longer divides by zero when `fog_start == fog_end`.
- DOC: document the coordinate space and unit of clip planes and fog in the C++
  headers, the R docs and the vignette, with worked examples for world and
  eye/NDC mode; document that multiple clip planes are ANDed and that point
  rendering ignores clip planes.
- DEV: add C++ tests (cpp_tests/test_clip_planes.cpp, cpp_tests/test_fog.cpp) and
  R tests (tests/testthat/test-clip-planes.R, tests/testthat/test-fog.R) for the
  coordinate spaces, camera invariance, plane combination and input validation.
  `RenderOptions::clip_planes` and the fog space had no test coverage at all
  before this release.
- FIX: the `full_CLI_renderer` example could not be linked (and neither could any
  other consumer that includes libfs.h in more than one translation unit): the
  vendored libfs header defined its 49 free functions without `inline`, so
  linking `libscimesh.a` (which contains them, via `obj_io.cpp`) together with a
  TU that also includes `libfs.h`/`fs_mesh_converter.h` failed with "multiple
  definition of `fs::...`" errors.  The fix belongs to libfs itself (libfs commit
  ab88cb4, "CHG: inline some functions") and is included in the vendored copy
  (see the DEP entry below).
- DEP: update the vendored libfs (src/third_party/libfs.h) from v0.6.0 to libfs
  commit ab88cb4 ("CHG: inline some functions"), i.e. v0.6.1 (upstream commit
  7bd0c26, "Volume header fixes": NIfTI sform/qform and vox2ras handling now
  match FreeSurfer's `mri_convert`) plus the fix that marks all 49 libfs free
  functions `inline`, so that the header can be included from more than one
  translation unit.  ab88cb4 is on the libfs `develop` branch and not part of a
  libfs release yet; the next libfs release should therefore be picked up when
  it is tagged.  scimesh only reads meshes through libfs (OBJ via
  `fs::Mesh::from_obj`, FreeSurfer surfaces via fs_mesh_converter.h), so the
  volume/geometry fixes do not change rendering output; the libfs test suite
  (150339 assertions) passes with the change.
- NEW: full_CLI_renderer example: new `[fog] space` config key and
  `--fog-space` flag ("world" or "ndc").
- FIX: `full_CLI_renderer` example: an unknown `--fog-space` value now warns and
  falls back to world units.
- FIX: `full_CLI_renderer` example: `--output` now honours a supported image
  extension (`.png`, `.ppm`, `.bmp`, any case) instead of appending it a second
  time (`--output brain.png` used to write `brain.png.png`).  Such an extension
  selects the output format and overrides `--format`; names without an extension
  still get the extension of `--format` appended, and the "Wrote <file>" message
  now reports the path that was actually written.
- DEV: C++ code coverage can now be measured: `cpp_tests` configured with
  `-DSCIMESH_COVERAGE=ON` (clang) instruments the tests and the library, and
  `dev_tools/coverage_cpp.sh` builds, runs the tests and writes a report (text
  summary, HTML, lcov trace file) to `coverage/`.  Test code, the Catch2
  amalgamation and `src/third_party/` are excluded from the report.  This is a
  local tool only: no upload, no badge, no coverage gates.  First baseline for
  src/core: 71.8 % lines, 66.1 % branches - with `ply_io.cpp` at 0 % and
  `obj_io.cpp`/`transforms.cpp` not even linked into the test binary (no C++
  test references them).  New C++ tests for mesh file I/O (`test_io.cpp`),
  mesh transforms (`test_transforms.cpp`), renderer features/SSAO
  (`test_render_features.cpp`), string formatting (`test_to_string.cpp`),
  colormaps (viridis and empty input, in `test_colormap.cpp`) and the
  FreeSurfer/PLY brain mesh path (in `test_brain_mesh.cpp`, whose test data path
  was wrong so the test silently did nothing) raise this to 91.6 % lines and
  79.0 % branches, with `ply_io.cpp`, `obj_io.cpp` and `transforms.cpp` now
  linked and covered (`transforms.cpp` at 100 %).  The new R test
  `tests/testthat/test-ssao.R` covers screen-space ambient occlusion through the
  R API.
- DEV: CI: new `examples.yml` workflow builds and runs all C++ examples
  (Linux and macOS) plus the R examples on every push/PR.  Previously no CI job
  compiled the examples at all, which is how the full_CLI_renderer link error
  could go unnoticed.  New `coverage.yml` workflow (manual dispatch or push to
  develop) runs the coverage script and stores the HTML report as a build
  artifact.
- FIX: the mesh transforms `scale_mesh()`, `rotate_mesh()` and
  `transform_mesh()` now update per-vertex normals (C++ and R).  They used to
  move the vertices and leave the normals untouched, which is only correct for
  translations: after `scale_mesh(mesh, c(1, 1, 4))` or any rotation the normals
  no longer matched the surface, so shading was visibly wrong (users had to pass
  `invert_normals = TRUE` or recompute the normals by hand).  Normals are now
  transformed with the inverse transpose of the matrix and renormalized, which is
  the correct rule for non-uniform scaling, shearing and mirroring; a singular
  matrix (e.g. a scale of 0) falls back to the plain 3x3 transform.  Meshes
  without normals are unaffected, `translate_mesh()` still leaves normals alone
  (as it must), and the C++ header and R docs now state all of this.
- FIX: `mesh_from_fs()` with `detect_transparency = TRUE` could never mark
  anything as transparent, so FreeSurfer brain surfaces were rendered with an
  opaque white medial wall.  Vertices whose per-vertex value is `NaN` (that is
  FreeSurfer's "no data" marker) were always turned into opaque white, so no
  vertex ever had an alpha < 1; on top of that, the loop looking for such
  vertices was guarded by `has_colors()`, so it could be skipped entirely.  Such
  vertices now get alpha 0, which makes the renderer blend them (and set
  `has_transparency`), so a medial wall can be seen through.
- FIX: screen-space ambient occlusion (`ssao_enabled = TRUE`) had no visible
  effect at all, for three independent reasons: (1) `Rasterizer::apply_ssao()`
  treated depth buffer values as if they were in [0, 1] when they are in
  [-1, 1], so the depth inversion was wrong for both projections.  The exact
  inversion for both projections now lives in one shared helper
  (`view_distance_from_ndc()`, which `Rasterizer::fog_depth_from_ndc()` also
  uses), so SSAO and world-space fog can no longer disagree about the depth
  range; (2) `ssao_radius` is documented (and now handled) in
  pixels, but was used as a world/screen-space factor, and is now rounded and
  clamped to at least one pixel; (3) the hemisphere test that decides whether a
  sample may occlude mixed normalized device depth with pixel units and had the
  wrong sign, so it rejected every valid occluder - the sample offset is now
  built from the world-unit depth difference.  Dead code (`depth_C/D/E`,
  `radius_scale`) was removed, and the new tests (C++ and R) assert that SSAO
  actually changes pixels for a perspective and for an orthographic render.
- FIX: translucent geometry lying *behind* opaque geometry was blended on top of
  it, which is what made a very transparent mesh look wrong: the blended pass
  ignored the depth buffer entirely
  (`if (blend_mode || depth < z_buffer[idx])`), so a translucent medial wall
  painted over the structures inside the brain.  Translucent fragments now pass
  the same depth test as opaque ones; they still do not *write* depth, so the
  buffer always holds the nearest opaque surface.
- FIX: the translucent pass sorted its triangles the wrong way round.  The
  centroid depth is a view-space z (the camera looks down -Z, so nearer is
  larger), and the sort was descending, i.e. front-to-back: the nearest
  translucent surface was drawn first and then covered by the ones behind it.
  Sorting is now back-to-front (`a.view_z < b.view_z`), which makes the result
  independent of the order in which the meshes were added to the scene.
- NEW: transparency no longer has to be declared.  `Mesh::is_transparent()`
  derives it from `default_color`, `colors` and `face_colors`, and that is what
  the renderer uses, so per-vertex, per-face and uniform-alpha meshes blend
  without setting `Mesh::has_transparency` (which is still honored as a manual
  override, never cleared by `Mesh::update_transparency()`).  Readers, the
  FreeSurfer converters and `Scene::add()` refresh the flag so it is accurate
  when inspected, and the R layer derives it from the colors it receives.
- NEW: PLY vertex colors now keep their alpha.  RGBA files (8-bit `alpha` as
  well as float `alpha`, including the common case of 8-bit RGB with a float
  alpha) are read into `Color::a` and mark the mesh as translucent; previously
  alpha was silently dropped (`a = 1.0`).  Alpha without any RGB is kept as a
  white vertex with that alpha.
- NEW: C++ `convert_fs_mesh(fs_mesh, morph_data, rgb_colors, nan_alpha)` gained
  the `nan_alpha` parameter (default 1.0 = unchanged behaviour): pass 0.0 to
  punch holes at vertices without data (the medial wall) or e.g. 0.5 to draw
  them half transparent.  `mesh_from_fs(detect_transparency = TRUE)` keeps
  producing holes.
- NEW: R `set_mesh_alpha(mesh, alpha)` returns a mesh with a uniform alpha
  (creating colors from `default_color`/gray if the mesh has none), the
  one-liner for a 10 % reference shell.
- DOC: document transparency end to end: the alpha column of `colors`, the
  per-vertex interpolation, the back-to-front ordering and depth test, the
  medial-wall recipe and the remaining limitations (per-triangle ordering,
  no occlusion by translucent surfaces, `render_points()` does not blend) in
  the vignette, `?render_mesh`, `?set_mesh_alpha`, `Mesh`, `Rasterizer` and
  `Scene::add()`.
- DOC: `camera_auto()`/`camera_fit_scene()`/`camera_fit_mesh()` documented
  `direction` as the direction the camera looks along, but the camera is placed
  at `center + direction * distance`, i.e. it is the side you view the mesh
  from (`c(0,0,1)` = front view of a +Z facing mesh).  The parameter docs now
  say so; the behaviour is unchanged.
- DEV: new `examples/cpp/transparency/` and `examples/R/transparency/` (both
  built and run by the examples CI job) render three scenes: translucent spheres
  with an opaque sphere inside the shell and one behind it, an alpha ramp from
  1.0 to 0.05, and a hemisphere whose medial wall is 50 % transparent with two
  opaque "voxels" inside it and an 80 % transparent sphere in front.
- DEV: new C++ tests (cpp_tests/test_transparency.cpp) and R tests
  (tests/testthat/test-transparency.R) cover detection, blending, the depth-test
  and ordering fixes, per-vertex alpha interpolation, alpha 0 holes, PLY alpha
  round-trips and the FreeSurfer `nan_alpha` option.
- FIX: rendering a double-sided mesh was platform dependent and produced visible
  artifacts, because two floating point ties were resolved by the rounding of
  the platform.  (1) `generate_plane()` (and other manually built meshes)
  contain a front and a back face at exactly the same depth, so *which* of the
  two wins the depth test is a tie: on some builds the back face won on about a
  quarter of the pixels and, being shaded with its own (away pointing) normal,
  came out ambient-lit - an opaque plane was speckled with dark pixels (940 of
  4096 at 64x64 on x86-64, with plain `-O2` *and* `-O3`).  `Rasterizer::
  rasterize_triangle()` now shades such fragments two-sided, i.e. a visible back
  side whose normal points away from the viewer is lit with the normal flipped
  towards the camera, so both faces of the pair look the same and the tie no
  longer matters.  A mesh whose normals disagree with its winding is left alone,
  so `RenderOptions::invert_normals` keeps its effect.  (2) The two triangles
  that make up a quad share a diagonal, and a pixel center lying exactly on it
  was covered by both of them (blended twice) or by neither (a crack), again
  depending on the rounding: a translucent plane had a seam of double-attenuated
  pixels (0.7^4 instead of 0.7^2 of the background).  Edge functions are now
  evaluated in a canonical endpoint order, which makes the value bit-identical
  in all triangles sharing an edge, and a new edge fill rule (analogous to the
  "top-left" rule of hardware rasterizers) gives a pixel sitting exactly on an
  edge to exactly one of them.  Coverage and blending no longer depend on the
  platform; regression tests in cpp_tests/test_rasterizer.cpp and
  cpp_tests/test_transparency.cpp pin both invariants.

Version 0.3.4 -- Minor fix for CRAN only
-----------------------------------------
- FIX: adapt link in docs to prevent winbuilder warning


Version 0.3.3 -- Scene transforms, scene object, glTF export
------------------------------------------------------------
- NEW: per-mesh placement transforms in the C++ Scene.  A scene now stores an
  optional model matrix (and name) per mesh; the renderer applies it as a model
  transform at render time, so meshes are no longer modified in place.  See
  Scene::add(), Scene::set_transform(), Scene::node()/nodes() in scene.h.
  Scene::compute_bounding_box() now accounts for placement transforms.
- NEW: first-class scene object in the R layer: `scene()` builds an S3 object
  of class 'scimesh_scene' bundling meshes (each with an optional 4x4 placement
  transform and name), a camera, and render options.  `render_scene()` accepts
  it (and still accepts plain mesh lists as before).
- NEW: glTF 2.0 export, one-way interoperability: new C++ header gltf_io.h
  writes .gltf (+ external .bin) and self-contained .glb files with geometry,
  per-node transforms and names, vertex colors (COLOR_0), and an optional
  camera.  New R function `write_gltf()` wraps it.  Renderer-specific settings
  (fog, SSAO, shading mode) are not part of glTF and are not exported;
  per-face colors are exported by splitting vertices.
- CHG: `transform_mesh()` and the Rcpp matrix conversion now interpret 4x4
  matrices in the standard row-major convention (M * p, translation in the
  last column).  Previously the matrix was effectively applied transposed,
  which only diagonal (scaling) matrices hid; non-diagonal inputs now behave
  as written.  `scene()` placement transforms follow the same convention.
- CHG: `render_scene()`'s `camera` and `options` arguments are now optional
  and fall back to the values stored in a 'scimesh_scene' when present
  (backwards compatible).
- DOC: new R docs for scene(), write_gltf(); updated render_scene() docs.


Version 0.3.2 -- CRAN review fixes
-----------------------------------
- FIX: do not strip debug symbols for R build, as per CRAN policy
- FIX: add more input checks and disable use of asserts in third-party code, as per CRAN policy that C++ code should not call assert
- NEW: full_CLI_renderer example: composite mode now supports --crop/--grow to pre-process each tile (e.g. trim colorbar whitespace) before arranging
- NEW: full_CLI_renderer example: add --info mode to print image dimensions ('<path> WxH') without external tools like ImageMagick's identify
- NEW: add composite_tight.sh example that produces compact, publication-style multi-view figures: crop each rendered view (--crop --grow), arrange via --composite --fit-mode pad (auto max-extent padding, no manual image sizing), then attach a trimmed colorbar


Version 0.3.1 -- TGA support, macOS fix
----------------------------------------
- FIX: make R package compile again unter MacOS by avoiding strip --strip-debug in Makevars, the MacOS version of strip does not support --strip-debug.
- NEW: add TGA image export (own uncompressed true-color writer, no dependencies) to the C++ layer and the R layer via new `write_tga()` function (32-bit RGBA default, optional 24-bit RGB)
- improve all c++ doc strings for better API docs
- DEP: update libfs to commit 2641453 which fixes version define typo
- CHG: default to white instead of transparent background
- DOC: add cpp example that is a full command line program that renders meshes
- DOC: add file in source tree that lists all third-party details with full licenses, see src/third_party/THIRD_PARTY_LICENSES.md (and a copy for R at LICENSE.note)


Version 0.3.0 -- Dependencies, repo structure, C++ versioning
------------------------------------------------------------
- BRK: move scimesh code from src/core/ to src/core/scimesh and all required downstream adjustments. avoids conflicts with other libraries in C++ layer
- NEW: get proper versioning in place for C++ layer
- SEC: Update libfs to v0.4.1
- DOC: dramatically improve all doc strings, integrate CPP_GETTING_STARTED into API docs
- NEW: add colorbar application functions (C++ and R)


Version 0.2.8 -- Changes for CRAN submission only
--------------------------------------------------
- NEW: Add dependency authors as package contributors
- CHG: Various documentation / example changes


Version 0.2.7 -- Small improvements
--------------------------------------
- NEW: check meshes for validity via is_valid() in rendering pipeline to avoid crashes later
- NEW: R interoperability improvements: add more convenient auto-conversion from rgl meshes to scimesh mesh format, including auto-convert in render function (accepts both formats now). add manual helper for the inverse way. Explained in vignette.
- DEP: suggest viridisLite for R in DESCRIPTION
- DOC: Restructure and dramatically improve the R vignette
- NEW: add camera_orbit helper function for video generation, add example in docs
- FIX: fix building vignette index for CRAN


Version 0.2.6 -- Add contrast render option
--------------------------------------------
- NEW: add contrast to render options and Image class, see vignette.


Version 0.2.5 -- Fix CRAN checks
---------------------------------
- NEW: Waste an evening and mess with third party vendored source to satisfy R CMD check --as-cran


Version 0.2.4 -- String representations and cam fix
----------------------------------------------------
- NEW: add convenience to string function for classes, R and c++
- FIX: fix camera auto mode when using ortho projection (less whitespace around scene now)
- DEV: move all third party code from src/core/ to src/third_party/


Version 0.2.3 -- Convenience Image Ops
---------------------------------------
- NEW: add some basic convenience image ops to Image class


Version 0.2.2 -- Better SSOA and primitives
-------------------------------------------
- NEW: improve SSOA quality dramatically by adding depth buffer
- FIX: fix various primitive generation algos, use duplicate vertices to get proper edge rendering where appropriate


Version 0.2.1 -- Primitive fixes, bridge more cpp functions to R
-----------------------------------------------------------------
- NEW: expose 4 more cpp functions to R layer, mostly mesh transforms
- NEW: export primitive generation functions to R
- FIX: fix face winding order of some primitives, add more primitive tests and example


Version 0.2.0 -- Remove neuro layer
-----------------------------------
* BRK: Braking change: remove neuro-specific functions from renderer package, will be republished as separate package neuromesh


Version 0.1.0 -- Initial release
--------------------------------
- NEW: First release on Github



