Projection Mapper — Cinema 4D plugin
Change log

Newest version first. Each entry summarises what was added or fixed. This file is
shipped at the root of every build ZIP and provides the release notes for the
GitLab Release (only the current tag's section is shown there).

================================================================================
v0.98.1 - The test card learns to read
================================================================================
(v0.98.0 carried these same changes but its tag build failed on CI -- the engine
text source was not wired into the R25 project-tool build; v0.98.1 is the fix.)


-- Test Pattern (mire) text -------------------------------------------------
- The mire's labels are now rasterised by the ENGINE (common-motor-cpp, via
  stb_truetype) instead of C4D's GeClipMap. GeClipMap silently capped glyph
  rendering above ~120 px -- it drew only the bottom of large letters, so a
  label cut in half as soon as its size was raised. The engine renders crisply
  at any size, with exact ink metrics, and the very same call will feed the
  Blender port and a future web version of the tool (deterministic, WASM-ready).
  The plugin ships DejaVuSans.ttf in its resources for this.
- Text size is now a percentage of the image WIDTH (100 % = full width); it
  never overflows -- a string too wide for the card shrinks just enough to fit.
- Text position X/Y are percentages that move the label across its on-card
  travel: 0 % flush to the left/top edge, 100 % flush to the right/bottom. The
  label always stays fully on the card.
- The font, colour, opacity (now shown as a %), outline width and outline colour
  are SHARED by all four fields (Projector Name / Lens / Resolution / Format);
  each field keeps only its own show/text/position/size. The old per-field
  background rectangle is gone -- the text reads directly over the pattern,
  legible thanks to its outline.
- Sensible default layout: name top-left (large), lens top-right, resolution
  bottom-left, format bottom-right.
- The mire now shows the projector's REAL database name -- the model alone,
  without the brand -- and the real lens name, rather than the (renameable)
  object name; it falls back to the object name for custom projectors.
- New "Circle Skip" control (default 1): draw a concentric circle every N grid
  lines instead of every one.
- The "Show ..." element toggles are laid out in three columns.

-- Projector object ---------------------------------------------------------
- The projector casing texture is also fed into the material's Luminance
  channel, so the projector body is always visible (self-lit) even where no
  scene light reaches it.
- Removed the forced "Flat" viewport Display tag from the body and the lens.

-- Build --------------------------------------------------------------------
- ccache is used automatically when installed (no-op otherwise), so rebuilding
  after a one-file change is near-instant; the cache is shared across worktrees.

================================================================================
v0.97.1 - One rectangle, and nothing else
================================================================================

-- Projector casing (OpenBox) -----------------------------------------------
- On a short-throw lens with a large optical-axis offset the projection came out
  as a bright band with the image floating detached above it, and the rest of the
  room lit. The enclosure introduced in 0.97.0 was doing two wrong things at
  once: its snout kept the APERTURE's cross-section all the way back to a third
  of the way to the aperture, where the beam is only a third as wide and (with
  lens shift) a third as high off the axis, so the snout floor cut straight
  through the middle of the beam; and what it did let past was a band the beam
  never owned. Measured on a Panasonic PT-RQ45K + ET-D75LE95 (throw ratio 0.36,
  optical axis 0.46 image-widths below the image).
- The enclosure is now a closed box straddling the light whose front face
  carries a hole exactly the size of the output footprint. Every ray through the
  hole is a ray of the projection frustum, by construction, and every other ray
  meets a wall -- there is nothing left to tune. A long lens, whose aperture
  sits metres in front of the light, gets a snout for the remaining distance,
  and that snout is a frustum of the beam, so it can never clip it either.
- The slide (diapo) is still the last polygon of the mesh, so its material,
  selection and UVW mapping are unchanged.

-- Projection light ---------------------------------------------------------
- The cone of the projection spot is now measured from the four corners of the
  image instead of from the throw ratio alone. The old formula is only correct
  for a lens on the optical axis: tilt the cone onto a shifted image and the
  corners move in the tilted frame. On the ET-D75LE95 it came out 2 deg too
  NARROW horizontally -- the bottom corners of the projection were cut off --
  and 11 deg too WIDE vertically, throwing light 93 deg off axis for an image
  that ends at 65. A lens with no shift gets exactly the same cone as before.

================================================================================
v0.97.0 - The projection light stays in its box
================================================================================

-- Projector casing (OpenBox) -----------------------------------------------
- The projection light is an omni sitting at the box origin, and the OpenBox
  that masks it used to have its back wall flush with the lamp, so everything
  the lamp threw backward or sideways leaked out. The OpenBox is now a
  light-tight enclosure: a large closed box straddles the lamp (back + four
  sides), a chamfer narrows it down to the projection footprint, and a snout
  carries the beam to the front. Only the front window -- sized to the cone, so
  the projected image is never clipped -- stays open.
- The lens shift is carried by the snout and absorbed by the chamfer; the slide
  (diapo) is still the last polygon of the mesh, so its material, selection and
  UVW mapping are unchanged.

================================================================================
v0.96.0 - The licence window watches the database
================================================================================

-- Licence window -----------------------------------------------------------
- The licence window (Extensions > Licence) now shows the projector database
  version on this machine next to the one the server has, so a waiting update is
  visible at a glance instead of guessed at. The check reads only the small
  server manifest -- nothing is downloaded -- and runs off the window's thread,
  the same way the sign-in does, so the panel never freezes.
- A Force Database Update button sits right there too, running the exact same
  command as Extensions > Force Database Update and then re-reading the two
  versions so the panel reflects what just landed.
- When the server refuses the check with a 403, the panel says what it means and
  what to do: the licence session has gone stale, sign out and back in to
  refresh it. That refusal is what the tightened download security now returns
  once a token expires -- previously it surfaced only as an opaque download
  failure with no hint that re-signing fixes it.

================================================================================
v0.95.0 - Every projector on one list
================================================================================

-- Projector Manager ---------------------------------------------------------
- A new dockable panel (Extensions > Projector Manager) lists every projector in
  the scene, one row each, with what that projector actually sees: the row's
  first cell is a render through its own lens, so you can tell where a projector
  is aimed without flying the viewport there.
- Three switches per row, and they are independent. ACTIVE cuts the projector
  entirely -- no light, no slide, and no cone left in the viewport. LAMP is the
  one you use while you work: no light and nothing projected onto the geometry,
  but the cone stays drawn, because a projector with its lamp off is still aimed
  and still framed. SLIDE off keeps the projector lit and puts a flat colour of
  your choosing through it instead of the test card.
- LOCK makes a row read-only. SOLO isolates one projector: every other one is
  muted for as long as it is on, and the mute is worked out rather than typed,
  so a projector pasted in while a solo is running arrives muted too.
- Groups. Put a selection into a named group, open and close it, reorder it,
  rename it. The group row summarises its members in the same three columns: the
  mark when they all agree, a dash when they do not. Membership is a name the
  projector carries, so a projector pasted into another scene lands in the same
  group there; the order and what is open live in the document.
- The camera column looks through a projector and back out again -- which is
  also what puts the render settings on that projector's native resolution. The
  Move view button does the other thing: it brings your own viewport camera to
  the projector's viewpoint and leaves the render settings alone.
- Three more columns, and they are handles rather than switches -- the same
  three things the icons in the viewport do, done from the row. Press the target
  cell and drag: the projector arms its target (the move gizmo goes onto it) and
  the target then follows your cursor in the view, live -- a row aims the
  projector without flying the viewport there. A plain click just arms it, as
  before. Drag the image width sideways and the image resizes, through the same
  clamp AND the same code path the viewport handle uses, so it resizes whatever
  the calc block's active input is and the lens never promises a size it cannot
  reach. Drag the banking and the projector rolls; double click it and
  the roll goes back to zero. The three cells carry the viewport's OWN handle
  icons, the same PNG files, so the mark for a thing is one mark in this plugin
  and not two. They are loaded once for the whole plugin now instead of once per
  projector, which a scene of seventy-eight of them was doing.
- Every cell that shows nothing when it is off shows a grey ghost of its mark
  under the cursor: an empty cell that does something has to say so. The camera
  cell follows the same rule and only carries its mark on the projector the
  viewport is actually looking through. Both figures are the projector's own parameters,
  shown as they stand: the row and the viewport are never two versions of the
  same number. Banking is greyed on a projector in free mode, where it is
  manual, exactly as the Attribute Manager greys it.
- The rendered views are made one at a time, only for rows on show, and only
  when the projector they belong to has moved or changed. A scene where one of
  them costs more than a moment switches the column off and says so rather than
  making the panel unusable.

-- Fixes ---------------------------------------------------------------------
- The Configure... button no longer crashes Cinema 4D. Two unrelated helpers
  were both named SpecCell, which collapsed onto one memory layout and corrupted
  the heap the moment the panel filled its specification card.
- The manager's image-size handle now actually resizes. It went through the
  screen-width path that only applies when the calc block's active input is the
  screen width, so in any other input mode the drag did nothing; it now takes
  the same buteed route the viewport handle does and works in every mode.
- Cut all now cuts whenever anything is still throwing light, and only brings
  the projectors back once the scene is already dark. It was deciding on the raw
  Active flag, which let a click switch everything back on instead of cutting.

================================================================================
v0.94.0 - Which projector, and how they differ
================================================================================

-- The projector search is a search again ------------------------------------
- The window used to ask for a shot: image width and height, throw distance,
  target nits, a weight limit and a power limit -- and it ranked the library
  against them. That question belongs to the scene. There the projector already
  stands, the screen is where it is, and the Lux Meter answers against the real
  geometry instead of against six figures typed a second time. Answered here it
  was a second opinion nobody could check, and the two could disagree without
  either saying why. The mode is gone.
- What is left is what a database can actually answer: which bodies exist, and
  in what way they differ. Two tabs, one query, one set of pins.

-- Results: a tree cut the way you read it -----------------------------------
- The matches are grouped into levels you name -- brand, then resolution, out
  of the box -- and each level is a chip you can drop. Any attribute a projector
  has can be a level, including two bands, brightness and power, for the figures
  that read better as ranges than as values.
- The selected model's figures run down a column on the right, with every lens
  it takes beside them. Each lens is drawn against the span the whole body
  covers, so a pair of numbers stops being a pair of numbers: you can see at a
  glance which lens sits where in the projector's reach, and how much of that
  reach it uses.
- Brand and brightness are picked rather than typed, from a combo and two
  sliders. They are not a second store: each writes its own token into the query
  and reads itself back from it, so the chip under the field takes a brand off
  and the combo follows.

-- Compare: the spec is the row, the projector is the column -----------------
- Pin as many models as you like and read a spec along its row. The winning
  column is marked on every row that has a winner -- more lumens, less weight,
  more stacking -- and rows every model answers the same way can be hidden
  altogether, which is what "Differences only" does.
- The last row is the one no figure can give on its own: how much of the throw
  range each body covers, all of them drawn to one scale.

-- One table of attributes, four menus ---------------------------------------
- The columns of the result table, the levels the tree is cut into, the key it
  is sorted on and the rows of the comparison all name a field from the same
  table of twenty-seven. A field can no longer be offered in one place and be
  missing from another -- which is exactly what the old five-value sort enum and
  eight-value group enum did to each other, and to the twenty-odd fields neither
  of them named.
- Right click the result table for the columns: every spec, the ones on show
  marked, with "add all" and "remove all" at the top. Columns are dragged to
  width, the model column sizes itself and never scrolls, and what does not fit
  beside it scrolls sideways.

-- The window looks like the panel it lives in -------------------------------
- Rounded corners on the tabs and on the frame around the results, and the open
  tab is joined to the panel it opens rather than sitting above it.
- The four buttons are drawn rather than borrowed from the system, and they
  light under the cursor.
- A colour bug that had been there since the window was written: every recessed
  surface -- the caption strips, the group rows, the scrollbar gutters -- was
  mixed towards "the opposite of the text colour", which on the default theme
  lands within a thousandth of the background itself. They had all been coming
  out the colour of the background, at any ratio. Headings now read as headings.

-- The licence token no longer sits in the clear -----------------------------
- The licence token carries the AES key of the projector database. Written to
  licence.json in the clear, any licensee could read it and decrypt the database
  offline. It now goes through the OS vault -- DPAPI on Windows. An existing
  licence.json migrates silently and one way; a machine with no vault falls back
  to the plain file, which is never worse than before.

================================================================================
v0.93.0 - What you asked for, and what the optics will give
================================================================================

-- The calculation block answers in three rows -------------------------------
- One cell was serving as both the question and the answer, and every argument
  the block has caused came from that. Type a 50 m screen width in front of a
  target 12.07 m away through a 0.3579 lens and the field came back reading
  33.72 -- correct, and written straight over the request. The 50 was gone, the
  reason for the 33.72 was never given, and backing the projector off gave
  nothing back because nothing remembered 50 m was what was wanted.
- The block now carries three rows. INPUT is what was asked for, and only the
  active mode's column has one. FROM SCENE is what the scene measures, which is
  the distance to the target. COMPUTED is what actually comes out, in all four
  columns. Two cells per column, never more.
- When the request is reachable the rows say the same thing and the widget reads
  the way it always did. When they diverge the COMPUTED row lights up and gives
  the two numbers that were missing: 33.72 m from here, or 17.90 m of clearance
  for the 50 m. Which of those to do is the user's call, and now it can be made.
- The row names moved to a strip on the left. With three rows, a caption above
  every value cost more height than the values it labelled, and the same word
  repeated four times across a row said nothing the first one did not.

-- An input is never corrected ------------------------------------------------
- The requested width has its own place now, separate from the width in force
  that all the geometry reads. Nothing overwrites it: not the butee, not a lens
  change, not a target drag.
- Because the butee is applied to the REQUEST on every evaluation rather than
  once on entry, the image now grows as the projector backs away -- and stops
  at the width that was asked for. Before, the width stayed at whatever the
  first butee had left it at, and the request was already lost.
- Same rule for the pixel size and the lux: in their own input mode their field
  is no longer recomputed from the width, so it keeps exactly what was typed.
  If the optics cannot deliver it at this distance, the COMPUTED row says so
  instead of quietly editing the entry.
- The image-size handle writes the request as it drags. It is direct
  manipulation and it cannot be dragged past the stop, so what is on screen IS
  the request -- without this the stored request would take the handle back on
  the very next recalculation.

-- L mode: the scene is the input, and it is read on time ----------------------
- L mode had no input anywhere. The distance cannot be typed -- it is wherever
  the target stands -- so the INPUT row sat empty from end to end and the block
  looked like nothing was driving it. The SCENE is the input in L mode, and it
  is the FROM SCENE row that now carries the mark. Which cell drives and which
  cell can be written to are two different questions, and only L mode ever had
  a different answer to them.
- The measured distance reached the optics one evaluation late. It was published
  to the container only AFTER the recalculation had read it, so every pass was
  served the previous pass's distance. A live drag re-evaluates frame by frame
  and hides that; one discrete change -- swapping a lens, typing a ratio --
  simply left the readout wrong until something else triggered a pass. Measured
  on a PT-RQ45K walked across its whole lens range, the distance now matches at
  every step instead of trailing by one.
- The distance still moves when the lens changes and the target has not: it is
  measured from the lens's optical exit, and in Projector Pivot mode a swap
  holds the chassis while the optics glide. Across the PT-RQ45K catalogue that
  is 14.7 cm, exactly the travel of the object itself. The readout is right; it
  is the exit that moved.

-- Between two lenses, only the projector is left ------------------------------
- When no lens covers the current throw ratio, the projector now shows nothing
  but its own body: no cone, no lens, no slide, no light. An image cannot form
  there, and drawing one said it could.
- All of that was already written and none of it could fire, because the throw
  ratio stored on the object was no longer the projector's own. It was written
  once, right after the butee, and never again -- so once the measured distance
  was folded in, the stored value kept whatever the butee had left while the
  screen width moved out from under it. Measured on a PT-RQ45K: the width swept
  0.9 m to 8.7 m while the stored ratio sat at 1.67 the whole way. Everything
  reading it -- the automatic lens choice, the coverage count, the red wire, the
  "Out of range" status -- was reading a ratio the projector did not have.
- The ratio is a CONSEQUENCE of the distance and the width, so the object now
  carries the consequence, and the coverage count is recomputed against it in
  the same pass: the assembly runs immediately after and reads that count to
  decide what to build.
- The rule "does this lens cover this ratio" had three copies that had drifted
  apart; one of them still ignored the fixed-focal tolerance. There is one now.

-- Fixed focal lengths are reachable in automatic mode ------------------------
- A fixed lens has a throw-ratio window of zero width. A ratio dragged with the
  mouse never lands on it to the last float, so it never entered, and the
  hysteresis then held the current lens for good: the ET-D3LEU101 and the
  ET-D75LE95 could not be reached in automatic mode at all, and with them the
  widest image the projector can make.
- Half-open windows already had a 2% relative tolerance. Degenerate ones now get
  the same, in both places that decide coverage -- the automatic selection and
  the cone's own test -- so the two can never disagree about what is covered.

================================================================================
v0.92.1 - The keyboard is enough, and the optics stop disagreeing
================================================================================

-- The catalogue: the arrows choose, Enter adds --------------------------------
- The dialog opened the cursor in the search field and stopped there: choosing
  anything other than what the mouse was pointing at meant letting go of the
  keyboard. It now does what Blender's own search popup does, which is the
  gesture everyone already knows -- three letters, Enter.
- The up and down arrows move the highlight and scroll it back into view without
  taking the focus off the field, so left and right still walk through the text
  and the next letter typed still lands in the search.
- The first row is highlighted after every filtering, so typing then validating
  is enough, and the dialog never waits on a selection nobody can see.

-- Ten image circles to a row --------------------------------------------------
- The viewport grid counted how many 76-pixel circles fitted across the frame
  and wrapped as soon as that was exceeded: six or seven to a row in a viewport
  flanked by Cinema 4D's usual panels. The count leads now and the size follows
  -- a row fills up to ten circles whatever the frame's width, and the circle
  shrinks so the ten fit.
- The one guard is the smallest circle still readable: if the frame is too narrow
  for ten at that size, the row holds what it can and the grid climbs. The old
  calculation could let a row spill past the right edge of the frame.
- The horizontal gap goes from 18 to 6 pixels: a cell already reserves the room
  for the widest image circle in the catalogue, so the gap has only to keep two
  neighbours from touching. Every pixel spent there is a pixel of circle when ten
  share a row.

-- The Sonde readout sits below its bottom-left corner -------------------------
- It was drawn to the right of the probe's origin, where the move gizmo passes as
  soon as the probe is selected: the arrows ran through the text. It now sits 30
  pixels under the bottom-left corner of the probe's screen footprint, never more
  than 140 pixels from that origin, on a translucent rounded plate with a lead
  line pointing back at the measured point -- the same placement as the Blender
  overlay, to the pixel.

-- The automatic name follows the selection again ------------------------------
- A projector's name stopped tracking the selection as soon as it differed from
  the composed name by one character. The ".52" suffix Cinema 4D gives a copy was
  enough, and so is a prefix of the user's own: "Panasonic PT-RQ50K -
  ET-D3QS400.52" kept its lens after a lens change.
- The remembered name is cut back into its two halves -- "<brand> <model>" and
  "<lens>" -- and each is looked for in the current name and replaced at every
  occurrence. What surrounds a half does not count; one character of difference
  does: ET-D3QS401 is not ET-D3QS400 and stays put. A name that says the lens
  twice means it twice. A name that carries neither half is the user's own and is
  never rewritten.
- Opening a scene renames nothing: what is remembered is the canonical name of
  the selection, so both halves already match as long as the selection does not
  move.
- A lens the database cannot name at that instant no longer erases the one
  already in the name, and a remembered name with no recognisable separator no
  longer has its model half substituted while a lens is selected.

-- Off-centre lenses point where they actually point ---------------------------
- The runtime baked the BoxOrigin/Dir nulls with each null's own rotation. Three
  models carry it the wrong way round (ET-C1U200, ET-D3LEU100/101) and their
  optical exit landed behind the body -- 41 cm behind the front face on the
  C1U200, with the beam starting inside the chassis. The quarter turn between
  the nulls' frame and the mesh's is the same for the whole database, so it is a
  constant now. 62 of the 66 models are unchanged.
- The relations already carried CenterLensX/Y, the lens's own optical axis:
  loaded by the engine, read by nobody. On the ET-C1U200 that is 0.78 image
  heights -- the offset the manufacturer builds into the lens. It now reaches
  the geometry: OpenBox frustum, look-through camera film offset, viewport cone,
  screen-width handle, Sonde cone.
- The image-circle overlay keeps the raw shift: it is the diagram of the shift
  mechanism, not a physical view. Its limits stay the manufacturer's (+/-18% in
  H, 0 to +20% in V) and the user starts at 0 without ever seeing the 78%.

-- The cone, the gadget and the target agree on one distance -------------------
- The viewport cone ran to sw * trMax when that was further than the screen, and
  painted there a rectangle no lens can fill: 58 m wide at 65 m for an
  ET-D3QW300 that stops at 38 m. It stops at the projection surface now.
- Its half-width was computed from the distance to the TARGET rather than the
  optical distance. The beam angle is optical -- the image is sw wide at
  dist = sw * tr -- so whenever the two diverged the cone lied, twice too wide
  or down to a thread, while the slide itself stayed right.
- Typing a value in the calculation block marked nothing dirty (only a mode
  change did), so the geometry waited for a rebuild that never came.
- The "from scene" distance was derived (l = tr * sw) AFTER the ratio was
  clamped, so the field published a distance that does not exist: 22.49 m shown
  for a target at 28 m, as soon as the ratio was parked in a gap between two
  lenses. It is measured now. When the clamp bites, what gives way depends on
  the mode: with the lens locked the image width yields; in automatic the ratio
  is free to leave the window -- that is what triggers a lens change -- so the
  requested width holds.

-- Also in this release --------------------------------------------------------
- The R25 build fix written up under 0.91.1, which never got a tag of its own.
  v0.88.0 and v0.91.0 both stopped at the same R25 failure, and release-plugin
  publishes nothing without all four binaries -- so this is the first tag to
  reach customers since v0.87.0.
- One more R25-only build error, reported by the pipeline of v0.92.0 and fixed
  here. R25 declares BaseObject::GetMp() and GetRad() non-const; 2024 made them
  const. The Sonde's readout reads the probe's bounding box through a const
  pointer, so it compiled on three SDKs out of four -- and v0.92.0, having built
  2024, 2025 and 2026 and nothing else, published nothing, exactly like the two
  tags before it.

================================================================================
v0.91.1 - Build fix: the Attribute Manager gadgets are 2024 and up
================================================================================

- v0.88.0 gave the Attribute Manager two data types of its own so that one
  widget could own a whole grid. They are declared with CustomDataTypeT, written
  and read through GeData's typed SetCustomDataType / GetCustomDataType<>, and
  registered with CUSTOMDATATYPE_INFO_NO_ALIASTRANS. All three arrived with the
  2024 SDK. On R25 the base class is the bare CustomDataType and the GeData
  accessors take an explicit type ID, so that build failed with forty errors --
  "undefined base class", "does not take 1 arguments" -- while 2024, 2025 and
  2026 went green. Both tags posted since -- v0.88.0 and v0.91.0 -- stopped
  there: release-plugin needs all four binaries before it will publish, so
  neither produced a release at all.
- The shim sits in am_gadgets.h behind the same MAXON_API_ABI_VERSION < 100000
  gate the rest of the plugin already uses for R25: the missing base class, the
  flag as a no-op (it is a copy-on-write hint, and the bit 2024 uses is
  unassigned on R25 -- setting it would be a guess), and one pair of helpers for
  the accessors. Every read and write of the two types goes through that pair,
  so the alternative -- the same #if repeated at eight call sites -- cannot
  come apart one site at a time.
- All four SDKs were built locally before this commit. R25, 2024 and 2025 with
  the project tool, 2026 with cmake: four binaries, no errors.

================================================================================
v0.91.0 - One panel to configure, tabs that hide on their own
================================================================================

-- Fewer tabs: everything you edit lives in Projector Parameters ----------------
- The object's parameters were spread across the forced "Object" tab, a separate
  Projector Parameters tab, and a Calculation Method tab. They are now one panel:
  the Configure button, the projector/lens pickers, the formula toggle and the
  Calculation Method block, in that order. Cinema forces its first tab to the name
  "Object" and will not let a plugin rename it, so that tab is simply emptied --
  it carries no visible parameter and disappears on its own.
- The Lens and Projector Pivot dropdowns share one grid now, so their labels and
  their menus line up in the same two columns instead of drifting apart.

-- The Calculation Method reads and edits in place ------------------------------
- The method gadget is a slider you can drag, with hover chevrons to step the
  value and a double-click to type one in -- no more separate field underneath to
  change what the gadget already shows. The value is centred in each column with
  the chevrons in the side margins, and the block spans the full panel width.

-- Icons on the generator, the Sonde, and the Lux Meter ------------------------
- The Projector generator, the Sonde and the Lux Meter tool were the last plugins
  showing as bare text among icons. They now draw their own, like the commands did
  since 0.89.0.

-- Configure: only the toggles that still mean something -----------------------
- Optics and Calculations lost their visibility toggles when their sections moved
  into the always-visible merged tab, so the dialog no longer offers them. What
  remains are the two tabs that genuinely come and go -- Display and Spec (the
  former "Info", renamed here and everywhere from a single place). Display is now
  the last tab of the list, and the Throw Ratio slider sits directly under its
  "Use Formulas" toggle.

-- Hiding one of Display / Spec no longer hides both ---------------------------
- Turning off either tab from Configure hid both, irreversibly. The Apply path
  re-read the eye toggles through GetBool on their state ids; those are custom
  drawn widgets, not checkboxes, and the read landed on an unrelated gadget that
  answered "off", forcing whichever tab you had not just touched back to hidden.
  The snapshot no longer reconstructs the eye states from widgets -- they are kept
  as you set them -- so each tab hides and shows on its own.

================================================================================
v0.90.0 - The lens list reads like the optics
================================================================================

-- The lenses are ordered by throw ratio, not by name ---------------------------
- The Attribute Manager sorted a projector's lenses alphabetically, which on the
  Panasonic references means Standard, Tele, Ultra, Wide -- S before T before U
  before W. On a PT-MZ882B the ultra-short ET-ELU20 (0.33:1) sat fourth, below a
  1.61:1 standard. The list now runs by increasing throw ratio, so it reads
  Ultra, Wide, Standard, Tele: the order the Search window and the Blender
  version have always shown.
- The commercial ratios are the sort key rather than the ratios the formulas give
  at the current screen width. Those move as you drag, and a menu that reorders
  itself under the cursor would be worse than one sorted a hair off.
- A lens whose ratios the manufacturer never published has nothing to sort on and
  goes to the end of the list, by name.

-- The object name keeps up with the lens auto mode picks -----------------------
- Changing the projector or the lens from the Attribute Manager or from Quick
  Config has renamed the object since 0.56.0. Auto mode also changes the lens on
  its own -- while the target or the image-size handle is being dragged and the
  throw ratio crosses a lens boundary -- and that path writes the new lens
  straight onto the object without sending a parameter message, so the name kept
  a lens the projector no longer had.
- The name is reconciled with the database selection once per evaluation pass
  now, in the scene hook. That catches every path at once instead of the ones we
  remembered to instrument.
- It costs nothing when nothing moved: the selection a name was composed from is
  remembered next to it, and the check ends there. Only a changed selection
  reaches the database.
- A projector renamed by hand is still left alone. So is one saved before this
  feature existed: opening an old scene renames nothing.

-- The image circle is smaller, and in the corner --------------------------------
- The overlay disc was drawn from a 100-pixel span and floated 80 pixels above
  the bottom edge and 24 in from the left. The span is 76 now, 12 in from the
  left and 30 up -- flush into the bottom-left corner, keeping only what Cinema
  4D's own viewport HUD line needs. The icon bar and the projector name still sit
  above the circle.
- Several projectors showing their circle at once still tile bottom-left, as many
  per row as the frame width takes.
- Two overlays could run into each other, and so could the names above them. The
  grid sized its cells at 1.2 times the span while the image circle the database
  gives runs from 1.0 to 1.7 matrix widths depending on the lens -- so on a
  generous circle the drawing was wider than the cell holding it. Cells are sized
  on 1.7 now, the widest the catalogue has.

-- A probe reads lux and distance, and stops there -------------------------------
- Every row of the probe readout ended with the projector's name, and with it the
  lens, since a projector object is named "<brand> <projector> - <lens>". It was
  the widest thing in the panel, for something already on screen. A row is the
  illuminance and the throw distance now.
- The Lux Meter tool draws the same panel and reads the same way.

================================================================================
v0.89.0 - Icons on the commands, and the application's own blue
================================================================================

-- The commands have icons -----------------------------------------------------
- Projector Catalog, Projector Search, Force Database Update, Remap Licence,
  Projector Configuration and View From Projector all registered with no icon at
  all, so they sat in the menus as bare text between entries that had one. Each
  now carries its own.
- They are drawn in code rather than shipped as files, for the same reason the
  panels are: a file can go missing in packaging, and the icon folder is
  resolved at runtime from a path that has already found nothing once. An icon
  compiled into the plugin cannot be lost on the way to a machine.
- Rasterised at four times the size and filtered down, so the edges are smooth
  at 32 pixels rather than the staircase a direct draw gives.

-- The accent is Cinema 4D's blue, not the website's violet ---------------------
- Every drawn surface carried Nocturne's #9184d9, which is the web front end's
  accent. It was never this window's: a violet edge on a Cinema 4D panel reads
  as something pasted into the application. The accent is now #545a99, the
  interface highlight Cinema 4D itself paints active tabs and pressed buttons
  with, so the panels read as part of the host.
- Thin work -- hairlines, the monogram, accented text -- takes the same colour
  one step up, because at one pixel on a dark ground the base accent is a smudge.
- The projector search window had carried a blue of its own, close to the brand
  and never equal to it. It is the same colour as everything else now.
- The web front end keeps the violet. The divergence is the point: one accent
  belongs to the product, the other to the application hosting it.

-- Custom mode counted fields that were not on screen ---------------------------
- A straight lens has no L2, a non-180 lens has no L3, and neither has a
  rotation -- those rows are never built. Closing the panel still reported them
  as edited, because reading a Cinema 4D gadget that does not exist SUCCEEDS and
  hands back zero rather than failing, so each one looked like a value changed
  to 0 against the figure still stored on the object.
- Which fields the current lens type actually builds was written out inside the
  snapshot and nowhere else. It is one function now, asked by everything that
  needs it -- the third defect this month traced to the same shape of omission.

-- Configure: the Info group was missing ---------------------------------------
- The Attribute Manager's Info tab could be hidden by nothing: the Configure
  panel listed Optics, Calculations and Display, and stopped there. It has an
  eye of its own now, remembered with the rest.

-- Configure: Apply, and what counts as a change -------------------------------
- Apply then Close asked "Close without applying 8 changes?" with nothing left
  to apply. The eyes became drawn areas rather than checkboxes in 0.88.0, and
  reading a gadget that no longer exists returns false rather than failing --
  so all of them counted as changed, for ever. The count is taken from the same
  state the eyes are drawn from now.
- A shift limit typed by hand is normalised before it is stored and was compared
  before being normalised, so certain values never stopped counting either.

-- Configure: "Show all" stays on its side of the panel ------------------------
- "Show all" reached across both modes: used in Custom it also opened and closed
  the four database groups, and the other way round. Worse, it was changing eyes
  the panel was not showing, which then counted as pending changes nobody could
  see -- the likeliest source of the phantom counts above.
- The test that says which eye belongs to which mode was written out by hand in
  one place and implied in three others, which is how the Info eye came to be
  missed by it on the day it was added. There is one of them now.

================================================================================
v0.88.0 - The Attribute Manager draws itself
================================================================================

-- The Info tab is a datasheet, not a list ------------------------------------
- Twenty-four label/value rows became one drawn sheet: a header with the model,
  brand, series and reference, then imaging, luminosity, physical and lens in a
  two-column grid. It says the same things in a fifth of the height.
- A figure the manufacturer never published reads "Unknown" at the dimmest step
  and loses its weight, so it is legible without ever being mistaken for a
  measurement. The header counts them once, rather than leaving you to discover
  the gaps cell by cell.
- Selecting several projectors that are not the same model says so, instead of
  showing the first one's datasheet as if it spoke for all of them.
- Two figures were wrong on the old rows and are fixed here: the chip size
  printed an unpublished value as -1.00", and dimensions were metres formatted
  as centimetres.

-- Calculation Method: one widget, one grid -----------------------------------
- The tab row and the four values were separate widgets. A tab strip sizes its
  tabs to their labels while a layout group sizes its columns to their content,
  so the two rows could never line up -- and the value cells carried no label at
  all, which left their position as the only way to tell them apart. They are
  now a single drawn widget on one grid, aligned by construction.
- Each column states its role: the one you type into, the distance measured from
  the scene, and the ones derived from them. In distance mode nothing is
  editable, which was true before and is now visible.
- The value being typed stays a native field, so units, arrows, undo and
  animation keep working exactly as they did.
- The numbers stopped losing their unit off the end of a narrow column:
  "85.364 m..." and "7755.5 l..." are "85.36 mm" and "7756 lux".

-- The Configure panel ---------------------------------------------------------
- An eye marks what the Attribute Manager will show. The square checkbox used to
  mean two different things -- hiding a row and setting a value -- and now keeps
  only the second.
- Database mode shows the specifications of the machine you picked. It used to
  be two dropdowns and nothing else: you chose a model by name and had to close
  the window to find out what it was.
- A drawn header band carries the bound projector, its mode and the size of the
  selection.
- Apply counts what is waiting: "Apply (3)". Close no longer discards pending
  edits without a word.
- Sections are named rather than numbered; the numbering ran 1, 2, 3, 4, 5, 8, 9
  and the gaps were visible. The stack section stopped shipping the placeholder
  text "(to be defined)".
- Every numeric field in custom mode has the arrows the resolution rows had.

-- Build -----------------------------------------------------------------------
- Restored the ad-hoc macOS signing step, which a Cinema 4D SDK refresh had
  silently reverted along with the rest of the vendored cmake file. macOS builds
  had been shipping unsigned. The block now carries a marker so the next refresh
  shows it in the diff.

================================================================================
v0.87.1 - Signing in connects, and does so at once
================================================================================

-- The licence window reaches the server -------------------------------------
- Signing in no longer fails with "SSL server verification failed". The client
  now checks the server certificate against the operating system's trust store
  (the system bundle on macOS, the system store on Windows) instead of a path
  that, inside Cinema 4D, resolved to no certificates at all -- the server and
  its certificate were valid the whole time.

-- ...and without the long wait ----------------------------------------------
- Signing in used to freeze for five to twenty seconds after the click. The
  work itself takes under a second; the wait was the window relying on its own
  timer to notice the answer, a timer Cinema 4D only services when the event
  loop happens to wake. The background thread now wakes the window the instant
  it is done, so the result appears at once.
- Each request also asks the server to close the connection when it answers.
  That removes a separate stall where the encrypted connection's shutdown
  waited on a keep-alive server that never volunteered to hang up.

================================================================================
v0.87.0 - A column for the tech, and specifications that admit what they do not know
================================================================================

-- A specification nobody published says so ----------------------------------
- The database now spells "the manufacturer does not publish this" as NULL,
  and the dialog reads it back as "Unknown" instead of inventing a figure.
  The stat tiles were the worst of it: Grouped() prints the absolute value,
  so an unpublished brightness came out as "1 lm ISO" and an unpublished
  weight as "-1.0 kg", both of which read as measurements.
- Max inclination words each axis on its own. One may be documented and the
  other not, and the line used to print "V -1" beside a real figure.
- A numeric filter is no longer satisfied by a figure that does not exist:
  "weight<20" used to list every projector whose weight is simply missing
  from the database, right at the top of the results.

-- The tech reads from the left --------------------------------------------
- The chip and lamp were set against the right edge of the model column, so
  every row started them wherever its own name happened to end. They have a
  column of their own now, left-aligned under the TECH heading like every
  other column in the table, and the model name takes the width left over.

-- The Specs tab stops answering the lens question ---------------------------
- The "lens for your throw" card is gone from it. The Lenses tab asks the shot
  and judges every compatible lens against it, so the card was the same
  question answered twice and a shorter answer at that -- two answers that
  would have drifted apart the next time one of them changed.

-- Compare holds whatever is pinned -----------------------------------------
- The four-model ceiling is gone. Columns keep a readable minimum width and
  the table scrolls sideways when they no longer fit, with the row labels
  painted over the columns that pass under them so the figures stay readable
  against what they are figures of.
- The pinned list scrolls too, rather than running off its panel.
- "best" no longer eats the value it is marking: the number is fitted first
  and the marker takes what is left of the column. A column too narrow for
  both still says which one won -- in the accent, rather than in a word.

================================================================================
v0.86.0 - The Lenses tab answers the shot
================================================================================

-- Which lens do I need? -----------------------------------------------------
- The Lenses tab of the detail panel now carries the two numbers the question
  actually needs -- image width and throw distance -- with the ratio they come
  to underneath. It is the same shot the Fit-the-shot page works from, so a
  distance typed here re-ranks that page too.
- Every lens is judged against that ratio and says so in its own column: too
  wide, at min, fits, at max, too long. The list is ordered by throw ratio
  rather than default-first, so top to bottom it reads too wide -> fits ->
  too long; the default lens keeps its own mark.
- The Specs tab says "too wide" as well, where it used to say "too short" --
  one word for one situation, whichever panel it is read in.
- The panel's heading and its three tabs moved into an area of their own, so
  the fields can sit between the tabs and the list where the design puts them:
  a text field cannot be drawn inside a user area.

-- Everything the record holds -----------------------------------------------
- With the shot fields gone from it, the specification list has the room for
  the rest of the database: ANSI brightness, both eco figures, eco and strike
  power, the chip's second dimension, the stacking frame's height and the
  commercial series.
- A line the model has no figure for is dropped instead of printed as a dash:
  a panel of dashes reads as a broken database rather than as a gap.

-- The shot diagram was losing its top edge ----------------------------------
- Its box was sized before the type grew, so the screen it draws ran off the
  top and the "screen 5.00 m" label sat on top of it. Each label has a band of
  its own now and the cone takes what is left, both measured from the font
  rather than fixed -- the next change of size cannot bring the fault back.

-- Near misses ---------------------------------------------------------------
- Listed again by default. The band that opens them says plainly where the
  answers stop, so they no longer read as part of the list above them.

================================================================================
v0.85.0 - The type scale, and columns that line up
================================================================================

-- One size up, everywhere ---------------------------------------------------
- FONT_STANDARD is the interface's base size, and the design sets its body
  three points above it, so the whole window was a size down from the mockup at
  once. The body is FONT_BIG now, which puts the captions back at the base size
  and the headings at FONT_HUGE: roughly the design's 10 / 13 / 18 instead of
  10 / 11 / 14.
- The number columns were re-measured against the bigger face, and no wider
  than their widest real value: every pixel taken there comes off the column
  that has to hold a sentence.

-- The chip and lamp have a column ------------------------------------------
- They were drawn straight after the model name, so each row started them at a
  different x and the eye read a staircase instead of a table. The name is set
  against the left edge of the column and the chip and lamp against the right,
  so both sides are flush and the ragged gap falls between them -- where it
  says nothing. A TECH heading marks the column.
- Nothing is cut to make that work: only a name long enough to reach the far
  side takes room away, and only then does the tech give way.

-- Label right, value left --------------------------------------------------
- The specifications ran label hard left and value hard right, so a short pair
  left a hand's width of nothing in between and the eye had to travel back to
  pair them up. Both halves are set against a common centre now, and the column
  of labels reads as one thing. Same for the row headings in Compare.

================================================================================
v0.84.2 - Build fix: the two new type sizes are 2025 and up
================================================================================

- v0.84.0 reached for FONT_HUGE and FONT_SMALL_BOLD. They arrived with the 2025
  SDK; on R25 and 2024 the enum stops at FONT_BIG_BOLD, and those two builds
  failed on "identifier not declared" while 2025 and 2026 went green. Guarded
  behind MAXON_API_ABI_VERSION, the way the rest of the plugin already handles
  SDK differences: R25 and 2024 lose a type size, not the plugin.
- Both sides of the guard are compiled locally now before a tag goes out. A
  green build here is a green build on 2026 and says nothing about R25.

================================================================================
v0.84.1 - Brighter text
================================================================================

- The window took its text colour straight from the theme, which is a
  comfortable grey for a manager full of small labels and too quiet for a
  reading surface. It is now lifted halfway towards the extreme, and the four
  secondary tones with it.
- Lifted AWAY FROM THE BACKGROUND rather than towards white: on a light theme
  that means darker text, not a washed out one. And stopped halfway, because
  the extreme glares.

================================================================================
v0.84.0 - Readable, and only what fits
================================================================================

-- The window reads at a glance ----------------------------------------------
- Cinema 4D has more than the three faces the drawn window was using. FONT_HUGE
  is +7 points and FONT_SMALL_BOLD is a point smaller and bold, which is what
  the design sets its 19px headings and its 10px all-caps captions at. The
  selected projector's name is now the size it is in the design, and every
  caption -- PIN / MODEL / LUMENS, BRIGHTNESS, THROW RATIO, PINNED FROM THE
  LIST -- is small, bold and upper case rather than body text in capitals.
- The model name is bold on every row, not only on the selected one: it is what
  the row is about. So is the brightness, in the list and on the fit cards.
- The secondary tones were too far down -- 0.86 / 0.70 / 0.54 of the way from
  the background to the text reads as grey on grey. They are 0.93 / 0.82 / 0.68
  now. A caption has to be readable to be worth drawing.

-- Fit the shot answers the question it was asked ----------------------------
- Only the projectors that fit are listed. The near misses were mixed in with
  them, dimmed, and dimming is not a separation: the list had to be re-read to
  find where the answers stopped.
- "Show the near misses" at the top right brings them back, under a band across
  the full width that says how many follow. They keep their tag and their
  reason.

================================================================================
v0.83.0 - Pin from where you are judging
================================================================================

- Fit the shot has a Pin box on every card, in the same place and the same
  shape as the one in the List tab. Deciding which projectors are worth lining
  up side by side happens while reading the cards that judge them, so having to
  go back to the List tab to tick them was a trip for nothing.
- A pinned card carries the same tint and outline a pinned row does, so what
  Compare holds is legible from either tab.
- Toggling a pin no longer sends the fit list back to the top.

================================================================================
v0.82.0 - Projector Search, drawn
================================================================================

The window is no longer a stack of Cinema 4D gadgets. Everything below the
query field is a GeUserArea that draws itself and reads its own clicks: a table
whose rows carry two type sizes and a state, filter chips you take off by
clicking them, the shot drawn as a shot, cards with the lens range under them.
Only what has to be typed stayed a native gadget -- the query, the six shot
numbers, the three buttons.

-- One window, three modes ---------------------------------------------------
- List, Fit the shot and Compare, sharing one search, one selection, one set of
  pins and one shot. The mode bar carries live counts, and the shot it all
  hangs on is written along the right of it with an Edit that jumps there.

-- The search box takes a query, not a form ----------------------------------
- The two rows of combo boxes and spin fields are gone. What replaces them is
  the field itself: "brand=Panasonic lumens>25000 weight<80" reads the way it
  is meant. "=", ":", ">", "<", ">=" and "<=" all work, "a-b" is a range, a
  quoted value keeps its spaces, and anything without a key still matches the
  brand, label, reference and model name.
- Queries written before this grammar still mean what they meant: ":" keeps its
  historical reading, so a bare "lumens:5000" is still "at least 5000".
- Lens conditions (lens, lenstype, throw, fvalue) must be satisfied by a SINGLE
  compatible lens, as before -- asking for a short-throw zoom under f/2.8 finds
  projectors that have one lens doing both, not one lens each.
- Every condition becomes a chip under the field, in words: "lumens > 25 000
  lm", "weight < 80 kg". Clicking one takes that term back out of the query. A
  half-typed term filters nothing and shows no chip, so the list does not empty
  itself while you type.

-- The results table ---------------------------------------------------------
- Model, brand, chip and lamp on one line, the numbers right-aligned in their
  own columns, group headers on a recessed band, the selected row on an accent
  wash with a bar down its left edge.
- A Pin column at the far left. Pinned rows keep a tint of their own, and they
  are what the Compare mode compares.
- The column headings sort; the "Sort / Group" line opens a menu with both.
- The list scrolls itself rather than living in a Cinema 4D scroll group: a
  scroll group adopts its content's minimum size, and 78 rows asked for a
  window 2 200 pixels tall.

-- The projector, on the right -----------------------------------------------
- Four tiles for the numbers one opens the panel for -- brightness, weight,
  resolution, power -- then the rest as label/value rows.
- Specs / Lenses / Accessories as a segmented control carrying its own counts.
- The Lenses tab draws every compatible lens with its throw window as a bar on
  a scale shared by the whole projector, and the shot's own ratio as a tick
  across them. Which lens reaches this shot stops being arithmetic.
- Under the specifications, the one line that answers "and which lens, then":
  the lens that covers the shot's throw ratio, its range, and whether it fits.

-- Fit the shot --------------------------------------------------------------
- A new mode. The shot is described once -- image width and height, throw
  distance, target nits, optional weight and power ceilings -- and drawn: the
  screen, the beam, the distance.
- The lumens the shot needs come from the engine's own photometry read
  backwards. Illuminance.h computes lux = lumens / beam area, so this asks for
  the flux that puts the wanted nits on a matte white screen at gain 1.0. A
  projector that fits here is one the Lux Meter agrees with in the scene.
- The matches come back as cards, ranked by headroom, each with the lens that
  reaches the ratio, where in that lens's zoom the shot falls, and how much
  light is left over. The best one is outlined and says so.
- A near miss keeps its place, dimmed, and says why: "1 500 lm short of 600
  nits on 5.00 m", "no lens reaches 1.70 - closest 1.35", "over the weight
  limit by 12.0 kg". It can be hidden from the toggle at the top right.

-- Compare -------------------------------------------------------------------
- Up to four pinned models, spec against spec, the best value in each row
  marked -- and marked on none of them when they all carry the same number.
- The last two rows are the ones that matter for the job in hand: the lens each
  would wear for this shot, drawn to its own scale, and whether it fits.

-- Double click adds it ------------------------------------------------------
- Double clicking a projector adds it to the scene with its default lens.
  Double clicking a row of the Lenses tab adds it with THAT lens. Double
  clicking a card in Fit the shot adds it with the lens that reaches the ratio.
  The Add to Scene button still does what it did.

-- Colours come from the theme -----------------------------------------------
- Only the accent is fixed. Everything else is mixed from the theme's own
  background and text, in two directions -- towards the text for what comes
  forward, away from it for what recedes -- so the window sits on a light theme
  as well as on a dark one. The old fixed green / yellow / amber / red on the
  Lumens and Weight columns read as a warning in one and as noise in the other.

-- Fixed ---------------------------------------------------------------------
- Dimensions were printed in centimetres from a value stored in metres, so a
  110 cm projector read "1 cm". (projector_generator.cpp had it right; this
  window did not.) The width / height / depth search keywords are documented as
  metres for the same reason.
- Brightness and power figures are grouped ("40 000 lm"), like every other
  number in the window.

-- Also carried by this tag --------------------------------------------------
- Projector Catalog lost its menu flyout. Brand > Serie > Model > Lens was four
  levels deep to reach a model, and a menu cannot carry a search field. It is
  one window now: a search field, every projector in the database as a list,
  and a click that adds the projector with its default lens -- the same shape
  as the Blender add-on's Projector Catalog. Catalog is the "I already know the
  model" path; Projector Search is everything else.
- The v0.81.0 section below never got a tag of its own, so the licence window's
  rework ships here too.

================================================================================
v0.81.0 - The licence window says who you are
================================================================================

-- The account is the subject of the window, and now looks like it ------------
- The account and the plan were two static texts in the same body as every
  label around them, when they are the two things one opens this window to
  read. They are now drawn in a card of their own: the address in the big bold
  face, the plan as an accent chip on the right. AddStaticText cannot change
  type size; a user area can.
- A long address drops a type size before it is cut short, and is cut at the
  end rather than being allowed to push the plan chip off the card.
- Signed out, the card keeps the remembered address, muted: it says which
  account the prefilled field is about to sign in.

-- One deadline, in one place -------------------------------------------------
- "Sign in again within 12 days to extend the offline window" was written twice
  over: once in days by the status message, once as a date by a line of its own
  underneath. The status message is now just "Licence active.", and the
  deadline is the detail line inside the state box, under the headline it
  belongs to -- one block, one place to look.
- The box holds two texts from now on: the headline is the last thing that
  happened (a state, a result, an error), the detail underneath is the deadline,
  which stays put while results come and go above it.
- Both areas tint their ground from the Cinema 4D theme's own background colour
  instead of painting the bare one, so the state reads as a block on a light
  theme as well as a dark one.

-- Two things the form got wrong ----------------------------------------------
- "Email address" was cut short: its label was fixed at 96 pixels, which is
  narrower than the words on any theme whose interface font is a little wider
  than the default.
- The password note was split across two static texts, because a static text
  does not wrap and the first wording ran past the window. It is one shortened
  sentence on one line now.

- Nothing changed in what the window does: the same three gestures, the same
  calls, the same enforcement in the compiled library.

================================================================================
v0.80.1 - v0.80.0 delivered nothing
================================================================================

- The four SDK builds of v0.80.0 all failed before compiling a single file, on
  the source parity guard: DatabasePack.cpp arrived in common-motor-cpp with the
  compressed-database work, and the project-tool build lists every source by
  hand. Nothing was published, so the fix goes out as a new tag rather than by
  moving the old one.
- projectdefinition.txt now carries the four pieces the cmake builds get from
  target_compile_definitions: DatabasePack.cpp and miniz.c in Include.Win and
  Include.OSX, the miniz include directory, and MINIZ_NO_ZLIB_COMPATIBLE_NAMES
  -- without which miniz defines uncompress and crc32 as globals, in a plugin
  loaded into a Cinema 4D that carries its own zlib.
- No change to what the plugin does. v0.80.0's release notes still describe it.

================================================================================
v0.80.0 - The database arrives compressed
================================================================================

-- Compressed under the encryption, because that is the only place it works ---
- The database travels as `projectors.db.enc`, and that file is AES ciphertext:
  nothing in front of it can shrink it. Transport compression gains zero bytes
  on ciphertext. The only place compression pays is INSIDE the encryption, and
  that only works if the plugin knows how to inflate what it receives.
- The engine now reads a container: "RMPZ", a format version, the uncompressed
  size, then a deflate stream. An explicit header rather than a guess at the
  bytes -- a SQLite file and a compressed one can be told apart by sniffing, but
  getting that wrong hands the catalogue a database made of noise, and that is
  not a question to settle on a hunch.
- On the current-schema database this is roughly a quarter of the bytes,
  downloaded and on disk both. The weight is the 3D geometry, stored as JSON
  text, which compresses by about 84%; the textures barely move, being PNG
  already.

-- Read at the door, not at the download -------------------------------------
- The inflate happens where the database is opened, not where it is installed.
  The file outlives its download by months and is reopened every session,
  including by the lazy paths that fetch geometry and textures. A decision taken
  once at install time would have to be remembered somewhere -- and a plugin
  rolled back to a build that cannot inflate would read that remembered answer
  and hand back geometry made of deflate.

-- The plugin says what it can read, on every request ------------------------
- The updater announces `X-Remap-Accept-Payload-Encoding: deflate`, on the
  manifest request as well as the download. The service compresses only for
  callers that ask; the manifest announces the hash of the copy THAT caller
  would receive, so asking on one request and not the other would fail the
  comparison every time and be reported as a corrupt download.
- A plugin that does not ask keeps receiving exactly what it received before.
  That is what protects every build already installed.

-- Engine ---------------------------------------------------------------------
- common-motor-cpp moves to the commit carrying `DatabasePack` and the vendored
  miniz codec. miniz is compiled with MINIZ_NO_ZLIB_COMPATIBLE_NAMES: without
  it, it defines `uncompress` and `crc32` as plain globals, and this plugin is
  loaded into Cinema 4D, which carries its own zlib.

================================================================================
v0.79.0 - Signing in downloads the database, and says so
================================================================================

-- The licence window fetches the database, in front of you ------------------
- Cinema 4D starts signed out, so the startup database check bows out: no
  licence, nothing to open, nothing downloaded. Signing in was the moment that
  could be fixed, and it already triggered a download -- on a detached thread
  that told nobody. 18 MB went by behind the words "Signed in", and a failure
  came back later as a message box with no visible link to the click.
- The download now runs from the licence window and reports in the same box the
  sign-in answered in: "Downloading the projector database...", then the version
  that landed, or why it did not. A first install that fails is shown in red -
  the catalogue is empty; a refresh that fails is shown in amber - the previous
  database still works.
- One entry point for "get the database into shape" (SyncProjectorDatabase),
  shared by the startup check and the window, with a guard so the two cannot
  download over each other when someone signs in while the check is running.

-- The window says which licence, and until when -----------------------------
- Two new lines under the account: the plan (Trial / Active subscription /
  Payment overdue) and the deadline - "Sign in again within 12 days (works
  offline until 2026-09-08)". The number of days answers the question people
  actually ask; the date is what they can write in a calendar.
- Shown only while the licence verifies. In every other state those figures
  come from a token that was just refused, and the message already says what
  to do about it.

-- The window is in English --------------------------------------------------
- Labels, buttons and messages of the licence window and of the licence store,
  in line with the rest of the product. The Blender extension got the same
  wording on the same day, deliberately: two plugins that drifted on "what does
  an expired licence say" would give the same customer two answers.

================================================================================
v0.78.0 — La base se telecharge, elle ne se livre plus
================================================================================

-- Le plugin ne transporte plus la base de projecteurs -----------------------
- Les paquets R25, 2024 et 2025 emportaient 18 Mo de base chiffree que le
  plugin est cense aller chercher lui-meme. Ils passent de ~21 Mo a ~4 Mo, au
  niveau du paquet 2026 qui, lui, avait toujours ete correct.
- Ce n'etait pas qu'une question de poids : une base embarquee devient
  illisible des la rotation suivante de la cle de chiffrement. Le moteur ne
  sait plus l'ouvrir, le catalogue est vide, et rien ne l'explique.
- Deux chemins d'empaquetage cohabitent (project-tool pour R25/2024/2025, cmake
  pour 2026) et ils divergeaient depuis des semaines sans que personne le voie.
  La CI verifie desormais l'archive elle-meme : aucun .db ni .enc ne peut plus
  y entrer, par l'un ou l'autre chemin.

-- Les zips ne disent plus << cpp >> -----------------------------------------
- c4d-plugin_v0.78.0_cpp_2025_0_1-win.zip devient
  c4d-plugin_v0.78.0_2025-win.zip. L'ancien nom venait du dossier du SDK Maxon
  tronque : exact et illisible, puisque << cpp >> designe le SDK C++ et non le
  langage, et que << 2025_0_1 >> est une version de SDK, pas de Cinema 4D.
- Les quatre etiquettes sont r25, 2024, 2025, 2026. La correspondance est une
  table explicite : un SDK inconnu fait echouer le build la ou on l'ajoute,
  au lieu de produire en silence un nom que la CI attendra en vain plus loin.

-- La base vient du service de licences, plus du CDN --------------------------
- Le plugin va chercher la base sur https://remapengine.duckdns.org, derriere
  son jeton de licence, et non plus directement sur le CDN. Les BINAIRES de
  plugin, eux, continuent d'arriver du CDN : ils ne valent rien seuls, et faire
  transiter des megaoctets pour proteger un zip serait du cout sans benefice.
- Ce que ca change : le service remet a chaque compte une copie de la base
  chiffree avec SA cle, de sorte qu'une cle qui refait surface designe un compte
  au lieu de ne designer personne. Actif cote serveur depuis le 2026-08-27,
  publie en meme temps que cette version -- il n'y avait pas encore
  d'utilisateurs, donc pas de flotte a migrer.
- Consequence de deploiement : cette version exige un front end qui expose
  /api/downloads/db. Deployer le service AVANT de publier ce plugin.
- Les deux constantes XOR-voilees qui portaient l'adresse du CDN ont disparu
  avec leur dernier usage. La CI continue de verifier que << remapcdn >>
  n'apparait pas dans le binaire : ce n'est plus un voile a maintenir, c'est
  une garde contre la reintroduction d'un acces direct.

-- Le premier demarrage telecharge la base -----------------------------------
- Le controle de mise a jour au demarrage abandonnait quand la base etait
  absente. C'etait coherent tant qu'elle partait dans le zip ; depuis qu'elle
  n'y est plus, ce meme abandon livrait un plugin sans base et sans moyen d'en
  obtenir une, sauf a trouver << Force Database Update >> dans un menu.
  Il la telecharge maintenant, une fois, et la met en cache.
- Le telechargement est aussi declenche juste apres une connexion reussie. Au
  tout premier lancement il n'existe aucun jeton, donc le controle du demarrage
  s'etait deja retire faute de licence : sans ce second point d'entree, il
  aurait fallu redemarrer Cinema 4D pour obtenir une base apres s'etre connecte.
- Un echec le dit dans une fenetre, avec la raison et la marche a suivre,
  plutot que dans la console.

================================================================================
v0.77.1 — Le plugin exige une licence
================================================================================

-- La base ne s'ouvre plus sans licence --------------------------------------
- Le plugin refuse d'ouvrir la base de projecteurs tant qu'une licence valide
  n'a pas ete presentee. La base EST le produit (geometrie, optique, modele
  d'eclairement), donc la verrouiller verrouille tout.
- Le refus vit dans la bibliotheque compilee, pas dans une couche au-dessus :
  contourner le controle revient a se passer du moteur.
- Nouveau menu << Licence Remap... >> : se connecter, voir l'etat, se
  deconnecter. Rien d'autre. La saisie du mot de passe est masquee
  (EDITTEXT_PASSWORD) et le mot de passe n'est jamais conserve : il sert a
  obtenir le jeton de licence, puis il est oublie.
- La fenetre hors ligne n'est pas un compteur : c'est la duree de vie du jeton,
  14 jours, reemise a chaque demarrage connecte. Etre hors ligne ne bloque
  jamais en soi ; l'etre au-dela de la fenetre bloque. Le demarrage ne touche
  pas au reseau ; le rafraichissement se fait en arriere-plan.
- Une licence tenue par une autre machine peut etre deplacee, apres
  confirmation : l'autre poste la perd.

-- La fenetre de licence ------------------------------------------------------
- Les commandes du plugin s'eteignent quand la licence manque, au lieu de
  s'ouvrir puis d'echouer. Un menu grise dit ce qui se passe avant qu'on
  clique ; un menu qui repond par une erreur le dit apres.
- La fenetre de connexion reste vivante pendant l'appel reseau : le travail se
  fait en arriere-plan et la fenetre se rafraichit sur son Timer, plutot que
  de figer Cinema 4D le temps d'un aller-retour au serveur.
- Dialogue redessine aux couleurs du produit, bandeau trace au trait plutot
  qu'en image -- il reste net a toutes les echelles d'interface.

-- La base est chiffree sur le disque, et le reste ---------------------------
- La base de projecteurs est livree chiffree (AES-256) et n'existe JAMAIS en
  clair sur le disque : le moteur la dechiffre en memoire, cle en main, a
  l'ouverture. La cle n'est compilee nulle part -- elle arrive dans le jeton
  de licence signe, par utilisateur et par machine (chiffrement a enveloppe).
  Copier la base et le jeton sur un autre poste ne donne rien : le jeton ne
  verifie pas ailleurs que sur la machine pour laquelle il a ete emis.
- Les mises a jour en ligne suivent : le fichier chiffre du CDN s'installe
  TEL QUEL, verifie par son empreinte (payload_hash du manifest). L'updater
  ne dechiffre plus jamais rien -- il n'a pas de cle, c'est le principe.
- L'ancienne cle AES compilee dans les versions precedentes a ete RETIREE et
  la cle de la base a tourne : les fichiers publies ne sont plus lisibles avec
  ce qu'un binaire v0.76 contient. La CI verifie sur chaque artefact que ces
  octets n'y reviennent jamais, et que l'adresse du CDN n'apparait pas en
  clair (voilee dans le binaire).
- L'ancien sous-module de donnees common-motor (module Python historique) est
  supprime : la base chiffree est versionnee directement (LFS), et le moteur
  est common-motor-cpp seul.
- La verification de mise a jour ne se lance que si une licence est active :
  sans licence le produit est ferme, telecharger une base qu'on ne peut pas
  ouvrir ne serait que de la bande passante depensee.

-- Limites assumees ----------------------------------------------------------
- Le voile sur l'adresse du CDN est un voile, pas un secret : le trafic reseau
  la revele. Ce qui protege la base est sa cle, pas son adresse.
- La charge du jeton est signee mais pas chiffree : un licencie peut en
  extraire la cle de SA base. Inherent -- le client legitime doit pouvoir
  dechiffrer. Ce qui est garanti : on ne peut ni fabriquer un jeton, ni s'en
  servir sur une autre machine.

================================================================================
v0.76.2 — Nouvelle adresse du CDN (mise a jour REQUISE)
================================================================================

-- Le CDN a son propre nom d'hote -------------------------------------------
- La base de projecteurs est desormais servie depuis remapcdn.duckdns.org, un
  nom dedie qui ne depend d'aucun autre service de la machine. Le jour ou le
  CDN part chez un vrai diffuseur, l'adresse se repointe en DNS sans qu'une
  nouvelle version du plugin soit necessaire — ce qui est precisement le
  probleme que cette version-ci corrige.
- L'ancienne adresse, boiteakk.duckdns.org/projectors-db, a ete retiree le
  2026-08-25 et repond maintenant 410 Gone.

-- Consequence : les versions anterieures ne se mettent plus a jour -----------
- Tout plugin en v0.76.1 ou anterieure interroge l'ancienne adresse et ne
  recoit donc plus aucune mise a jour de la base. La base deja telechargee
  continue de fonctionner normalement, hors ligne : rien n'est perdu, mais
  plus rien n'arrive.
- Installer cette version retablit les mises a jour. La base locale est
  conservee, aucune autre action n'est necessaire.

-- Verifie avant publication -------------------------------------------------
- Manifeste, base complete et deltas repondent sur la nouvelle adresse, en
  canal prod comme en canal test, et un delta reel se telecharge.

================================================================================
v0.76.1 — Projector Pivot: SW-mode fixes (target drag, mode switch, lens swap)
================================================================================

-- SW mode + Projector Pivot: the projector no longer dances ------------------
- Dragging the target: the projector BASE is now the world-fixed end for the
  whole drag (frozen when the target handle is grabbed). The body pivots on
  its base to track the target and auto lens switches glide the optics
  sideways instead of kicking the body -- no more dance/recoil.
- Switching Projector Pivot <-> Optical Pivot no longer moves the stored
  target (the old handler re-captured it at the nominal distance along the
  gizmo axis, which teleported it -- sometimes to the OPPOSITE side of the
  projector when the target gizmo was still selected). A mode switch now
  also settles the nominal/world drift so the projected screen re-seats on
  the target immediately, without having to nudge it.
- Swapping the lens in the SW-family input modes now holds the DISTANCE to
  the target (the L-mode invariant): if the picked lens cannot reach
  TR = distance/SW, the image size (SW) is traded through the shared throw
  butee instead of letting the projected screen teleport to the nominal
  distance far off the target -- what a real lens swap does to the image on
  a wall. Lenses that cover the distance leave SW untouched.
- Pose solver for bent-lens swaps: more fixed-point iterations for the large
  (~90 degrees) direction changes of elbowed lenses.
- Known minor issue (to revisit): a small glitch remains right after a lens
  swap while the target handle is still armed.

================================================================================
v0.76.0 — Projector Pivot + mire text plates
================================================================================

-- Projector Pivot: the ID_PROJ_MODE option finally works ---------------------
- The option (renamed "Projector Pivot", 2 entries) now really selects what
  stays world-fixed when the lens or projector is swapped:
  * Optical Pivot: the optics (BoxOrigin) and the image hold their position,
    the body recoils through the lens geometry.
  * Projector Pivot: the projector stays ON ITS BASE (bottom-centre of the
    body bounding box, world-anchored) and PIVOTS AROUND IT so the beam keeps
    hitting the stored target -- like re-aiming a real installed projector
    without moving it. The image stays on target, the optics glide.
  The gizmo/axis always stays at the optics in both modes, and every optical
  formula keeps measuring from the BoxOrigin.
- Auto-lens selection gained hysteresis: when the throw ratio sits in the gap
  between two lens windows, the current lens is kept (its clamp pins TR at
  the window edge) instead of re-picking the nearest lens on every tick --
  this removed an endless bent<->straight flip-flop in L mode.
- The old unconditional compensation (body kept fixed in EVERY mode, which
  made the option inert) is gone; the Projector Pivot pose is solved
  world-anchored and history-free, so lens swap round trips return exactly
  (no more creeping/"dancing" projector).

-- Test card texts: translucent white plate + black outline ------------------
- Every text field of the mire now sits on a backing plate: white at 85 %
  opacity by default, padded around the glyph block, with its own
  show/colour/opacity settings per field. The text outline now defaults to
  2 px black. Keeps the labels readable over the busy card; the Blender
  readouts share the same style.

================================================================================
v0.75.1 — Parametric test card / mire (issue #45)
================================================================================

-- Test Pattern shader: full redesign to the issue #45 design spec ------------
- The mire is now defined in the projector's NATIVE PIXEL space and follows
  the agreed chart: 25% grey ground, magenta 1 px frame (always topmost)
  and centre axes, white 100 px grid justified on the axes (no centred
  cell), blue corner-to-corner diagonals, and concentric circles stepped on
  the grid pitch cycling red/green/blue out to the corners.
- Arrows at every line end, pointing outward and white-outlined: blue
  60 px arrows at the four axis ends and at the four corners (along the
  diagonals, drawn in front), magenta half-size arrows at every grid-line
  end. Height is adjustable (proportions and the 2:1 large/small ratio are
  fixed); the 1 px outline width is its own setting.
- Line width, grid step and every colour are editable; straight strokes
  snap to the native pixel grid so a 1 px line renders as exactly one
  crisp pixel at native resolution.
- Four text fields — projector name, lens, resolution ("1920 x 1200") and
  format ("16:10") — each with its own show/position/size/font/colour/
  opacity/outline settings. Name, lens and the format values are pushed
  automatically from the projector's DB selection (the lens label is new);
  custom projectors without a DB lens simply hide the lens text.
- The first-generation presets, SMPTE bars, greyscale ramp, checker,
  sub-grid, ruler and corner squares are retired.
- The card's geometry is computed by the shared engine (common-motor-cpp),
  so the Blender addon projects the exact same mire.

================================================================================
v0.74.0 — Projector series (DB schema 1.3), shift as % of the half matrix
================================================================================

-- Matrix shift readout: percent of the HALF matrix on each axis (issue #46) --
- The Attribute Manager vertical shift was shown as a percentage of the FULL
  matrix height (raw value / aspect): 45% V for the RQ50K + S400 at max
  shift. Projector spec sheets express each axis as a percentage of its own
  HALF matrix instead -- a floor-standing projector that puts the bottom
  edge of the image on the horizon reads 100% V. The vertical display scale
  is now 2 x resX/resY, so the RQ50K + S400 reads 32% H / 90% V at max
  shift, matching the spec sheet values. Display only: the stored raw
  values, the image-circle clamp and the cone/camera/OpenBox geometry are
  untouched, so existing scenes are unaffected.
- The FINAL clamped value snaps to the display precision (2 decimals of a
  percent), truncated TOWARD ZERO: an image-circle wall stored rounded in
  the DB (0.501 instead of 0.50097656) puts the geometric butee at
  90.0089% -- the stored value now snaps to exactly 90.00, never 90.01, so
  the reachable shift can never read above the spec-sheet maximum. The AM
  display truncates the same way, covering legacy scenes whose stored
  value predates the snap.

-- Projector series (DB schema 1.3) -------------------------------------------
- The database now groups projectors by commercial series (per-brand lookup
  table, e.g. Panasonic "PT-RQ"; schema 1.3, additive — older databases keep
  loading, the writer migrates them on open).
- Catalog flyout: the Extensions menu now reads Brand > Serie > Model > Lens;
  models without a series stay directly under their brand, after the serie
  submenus.
- Projector Search: new "Series" entry in the results "Group by" combo
  (headers read "Brand - Serie") and a new serie: search keyword; plain-word
  searches match the series name too.
- Series are assigned via the backend import (template 3.2, field IdSerie =
  series name, created on the fly within the brand).

================================================================================
v0.73.3 — Auto-lens gap snap, bare casing when no lens fits, viewport polish
================================================================================

-- L mode: per-lens TR windows now use the distance formulas ------------------
- Every consumer of the per-lens throw-ratio windows (availability gauge,
  lens auto-select, gap snap, TR slider bounds, locked-lens range, lens
  dropdown filter) evaluated the LW_SW / LT_SW formulas at the current SW in
  every input mode. In L mode SW = L/TR moves with the slider, so all the
  windows moved too: the gauge columns danced, the auto-select churned
  between lenses on a plain TR drag, and each spurious switch shifted the
  cone apex and target sideways (lens-origin compensation) -- the target
  visibly drifted in X/Z while only TR was edited.
- In L mode those windows are now evaluated with the LW_L / LT_L formulas at
  the held distance (ID_L), which is exactly what the L-mode TR clamp always
  used: stable rails, stable auto-select boundaries, and the target stays
  put. Other modes keep the SW formulas at the current SW.

-- Auto mode: TR/distance snap to the nearest lens window ---------------------
- In auto-lens mode the committed throw ratio can no longer rest in a gap
  BETWEEN two lens windows: it now sticks to the edge of the active lens's
  window and jumps to the nearest lens once the input crosses closer to it,
  so the derived distance (L = TR x SW) snaps between reachable values --
  the same blocked-then-jump feel L mode always had from its per-lens
  formula clamp. The lens coverage count is recomputed against the final
  snapped value so cone colour, gauge and status agree it is reachable.

-- Auto mode: no matching lens = bare casing ----------------------------------
- When no lens covers the current throw ratio (e.g. the target is dragged
  into a gap or beyond the set's range), the projector is now assembled
  WITHOUT the lens, the OpenBox slide plane and the projection light:
  nothing pretends an image can form. Everything remounts as soon as a lens
  window covers the ratio again. Locked mode keeps its lens mounted (the red
  cone signals out-of-range there).

-- Locked lens: red cone honoured the wrong TR range --------------------------
- With "Use Formulas for TR Range" enabled, the in-range test behind the wire
  colour still compared TR against the raw commercial ratios, so the cone
  turned red on values the formula range (and the clamp) legitimately allow.
  The test now runs after the clamp, against the same resolved range, and
  also refreshes on SW / formula-toggle / lens changes.

-- Viewport ---------------------------------------------------------------------
- The projected screen marker is a hollow white rectangle at the screen
  distance again (was four corner ticks).
- The image-circle overlay now also draws the matrix home position (zero
  shift) as a dark grey rectangle under the pink shifted one.
- Fixed two per-frame small memory leaks in the viewport draw path.

================================================================================
v0.73.2 — L mode: cone width exact at the zoom extremes (clamp-order fix)
================================================================================
- At the TR slider's butees the cone drew slightly narrower (~1%) than the
  projected diapo and the look-through camera, and the derived distance L
  drifted by the same amount on every slider edit held at the extreme. Cause:
  SW was derived from the TR clamped by the LW_L/LT_L formula window, then the
  global range clamp (commercial ratios, which differ from the formula window
  by the lens-offset term: 2.00 vs 1.99 wide, 3.41 vs 3.44 tele on the
  ET-D3QT500) tightened TR afterwards, leaving SW on the stale ratio.
- SW is now re-derived from the FINAL clamped TR, so the optical invariant
  L = TR x SW holds exactly at every slider position and the cone, diapo and
  camera agree at the extremes too. L no longer drifts.

================================================================================
v0.73.1 — "Info" datasheet tab, lens-shift overhaul, L-mode throw-ratio fix
================================================================================

-- L mode: the throw ratio drives the image again (formula regression) --------
- Fixes a regression introduced by v0.72.2: once the LW_L / LT_L formulas
  actually evaluated, L mode fed their result straight into the image width.
  Those formulas actually return the THROW RATIO at the lens's wide / tele
  zoom extremes for a given distance -- the code misread them as an SW in
  meters (a ~100x bogus width), so SW snapped to a nonsense value, editing TR
  no longer resized the projection cone, and the cone stopped meeting the
  edges of the projected diapo.
- L mode now keeps the optical invariant SW = L / TR: editing TR resizes the
  image at the held distance, and cone and diapo agree again at every ratio.
  The LW_L / LT_L formulas are used as what they are -- the reachable
  throw-ratio window at the current distance -- clamping the requested TR at
  the lens's zoom limits (their large-L limit is the commercial ratio; the
  affine term models the lens offset).

-- New "Info" tab: the full projector datasheet -------------------------------
- New read-only "Info" tab on the projector object, populated straight from the
  database record for the selected projector (and lens). It gathers everything
  the catalogue knows in one place, grouped into:
  * Identification — brand, model, reference.
  * Imaging / Technology — chip technology (DLP / LCoS type), lamp type
    (LASER / UHP), native resolution (with the XPR-doubled resolution when the
    panel has Quad Pixel Drive), image ratio, chip size, refresh rate, contrast.
  * Luminosity — Normal and Eco lumens, each in ISO and ANSI.
  * Physical / Electrical — dimensions, weight, chassis colour, lens mounting,
    power draw at 220 V (Normal / Eco / Strike), max V/H inclination, stack max.
  * Lens (Selected) — reference, model, type and F-value range of the mounted
    optic; it updates when you switch lenses.
- The tab appears only in Database mode (hidden for Custom projectors and while
  no projector is selected). Every field falls back to "-" when the underlying
  column is empty, so nothing ever reads as a blank or stale value.
- Chip-technology and lamp-type labels are resolved from their lookup tables, not
  shown as raw ids.

-- Lens shift: correct direction, spec-convention %, exact IC limits ----------
- Vertical shift direction fixed: dragging the matrix UP in the image-circle
  overlay now shifts the projected image UP (previously it went down). The sign
  is flipped in lockstep on all three consumers — the viewport cone, the
  look-through camera's film offset and the OpenBox projected slide (DB and
  custom assemblers) — so the cone, the render and the projected geometry stay
  consistent. Horizontal is unchanged.
- Shift percentages now follow the projector-spec convention. Horizontal is shown
  as a percentage of the HALF image width (x2), vertical as a percentage of the
  FULL image height (raw / aspect). On the PT-RQ50K / ET-D3QT500 the max reads
  32.0% horizontal and 45.0% vertical (was 16% / 23.7%). This is display-only:
  the stored value, the clamp and all optics keep using the raw IC-normalised
  shift, so nothing about the geometry changes.
- The maximum shift now equals the true geometric edge of the image circle. The
  clamp applied a 0.002 inset to the matrix RECTANGLE (halfW/halfH - margin)
  instead of to the image-circle polygon, which let the matrix centre overshoot
  the real boundary by 0.2% of the matrix width (16.2% instead of the exact
  16.0% = 0.66 - 0.5 horizontally). The inset is removed (margin = 0); a genuine
  safety margin, if ever needed, must inset the polygon, not shrink the rectangle.

================================================================================
v0.72.2 — "Use Formulas for TR Range" actually applies the formulas
================================================================================
- The projector/lens throw-ratio formulas are now evaluated. The formula parser
  was substituting the obsolete RMH/RMV variable names, but the database formulas
  use MH/MV (the raw MatrixH/MatrixV resolution) — so every formula silently
  failed and the TR range always fell back to the commercial ratios, in every
  mode.
- With the toggle ON (locked lens), the TR slider bounds now follow the LW_SW /
  LT_SW formulas at the current image size; OFF keeps the commercial ratios. The
  auto-mode global envelope, the projection-cone markers and the screen-width
  butée go through the same formulas too.
- Formulas are fed the raw MatrixH/MatrixV (not the reduced aspect ratio), and
  toggling the checkbox refreshes the slider bounds immediately.

================================================================================
v0.72.0 — Projector body texture loaded straight from the database
================================================================================
- New "Projector DB Texture" channel shader (ID 1069828): the projector body
  albedo is now sampled directly from the database texture BLOB, decoded in
  memory at render time. The texture is never written to disk, so it cannot leak
  as a loose file, and the .c4d stores only a hidden integer key — not a file
  path. This removes the stale-path "black projector" failure mode for good
  (superseding the v0.69.0 temp-dir workaround).
- Old documents self-heal: on rebuild, a projector whose body still carries the
  legacy on-disk bitmap is upgraded to the new shader automatically.
- Graceful fallback: if the database (or that texture) is unavailable at render
  time — e.g. a net-render node without the DB — the body falls back to the
  material's constant albedo colour instead of rendering black.

================================================================================
v0.71.0 — Render-matched look-through camera + "View From Projector"
================================================================================
- Looking through a projector now renders EXACTLY its view:
    * Camera focal length is driven by the throw ratio (horizontal FOV = the
      projection cone), aperture fixed at 36 mm. Works in DB and custom mode, no
      chip data needed.
    * Lens shift is mapped to the camera film offset: X maps 1:1, Y is scaled by
      matrixH/matrixV (width->height conversion) — both keep the shift's sign.
    * Render settings auto-sync to the active projector's native resolution
      (XRES/YRES + film aspect) while looking through it, and are restored to the
      user's originals when switching to another camera. Originals are stored in
      the render data, so they survive document save/reload.
- New command "View From Projector" (ID 1069815): moves the active viewport camera
  to the selected projector's viewpoint. If the view is already a projector (a
  locked camera), the default editor camera is used instead.

================================================================================
v0.70.0 — Image-circle shift overlay fixes
================================================================================
- Fix DB-mode IC overlay drawn upside-down (Y-flip centralised in IcScreenTransform).
- Double-click the matrix rectangle to reset H/V shift to 0 (recentre).
- CI matrix shift no longer clobbers the active 3D handle.
- AM H/V shift sliders clamp to the image circle and follow its arc when blocked.

================================================================================
v0.69.0 — Fix the "black projector"
================================================================================
- DB PBR textures are extracted to Cinema 4D's own temp directory (Application
  TEMP_DIR, "remap" subfolder) instead of the OS temp dir the renderer could not
  resolve. The projector body loads its albedo again.

================================================================================
v0.68.0 — Look-through camera + same-tick rebuild
================================================================================
- Projector look-through camera handles: centered layout, screen-axis drag, locked
  camera.
- Same-tick projector rebuild: removes the one-frame lag when dragging the projector
  or target with the Move tool.
- Look-through camera and projection light hidden in the editor from
  Execute@GENERATOR+1 (out of Draw), applied to the finalised cache every pass.

================================================================================
v0.67.0 — Luma Map post effect (ID 1069770)
================================================================================
- Illuminance heat map of the projectors, driven from Render Settings > Effects and
  toggled per viewport via Options > Post Effects: per pixel in the rendered image,
  adaptive OpenGL overlay in the editor, gradient gadget, min/max or auto scale,
  opacity, optional shadow rays.
- Also restores the R25 build (three 2024+-only API uses fixed).

================================================================================
v0.66.0 — Exclude body/lens from the projection light
================================================================================
- Projector body and lens excluded from the projection light (EXCLUDE mode,
  LIGHT_EXCLUSION_LIST) so the casing no longer blocks the beam or casts a shadow
  into the projected image.

================================================================================
v0.65.0 — Persist projector world-state
================================================================================
- Target, lens anchor, active handle and pivot persisted via Read/Write/CopyTo:
  position and aim kept on reload and duplicate; slide mire shows the advertised
  aspect ratio with XPR-doubled resolution.

================================================================================
v0.64.0 — One slide material per projector
================================================================================
- Unique Diapositive_<name> material carrying the projector's name on each slide,
  cloned on duplicate, renamed with the projector; legacy shared materials adopted.

================================================================================
v0.63.0 — True photometric lux (lm/m²)
================================================================================
- True lux everywhere: Attribute Manager ID_LUX, Sonde probe and Lux Meter tool;
  common-motor illuminance standardised on metres.

================================================================================
v0.62.0 — Sonde lux probe
================================================================================
- Scene object + click-to-place via the Lux Meter, shared common-motor illuminance
  (Blender-ready), dual-resolution mesh, aperture-bounded near-field lux.

================================================================================
v0.61.0 — 4K slide preview, light hide, material at creation
================================================================================
- Slide material: 4K preview + refraction index 1.0; Test Pattern defaults show
  name/resolution/ratio.
- Slide material created up-front at projector creation (menu + Catalog/Search).
- Projection light hidden in the editor (still renders + casts shadows).

================================================================================
v0.60.x — CI + auto cone fixes + per-lens gauge
================================================================================
- v0.60.3: CI, tagged releases built from main/dev (cpp branch retired).
- v0.60.1: cone hidden outside the lens range; lens auto-switch on move.
- v0.60.0: per-lens availability gauge (TR columns on the right edge, white line at
  current TR); cone/markers coloured by the active lens.

================================================================================
v0.59.0 — Image-size butée + continuous auto-switch
================================================================================
- Throw-ratio range as a single source of truth (mode + "Use Formulas" aware),
  shared by SW butée, TR clamp, slider bounds and cone markers; auto mode:
  continuous resize + lens auto-switch; keyboard entry validated.

================================================================================
v0.58.0 — Shared in-memory ProjectorDatabase
================================================================================
- Shared in-memory projector database + background lens-mesh preload for throw-ratio
  auto mode.

================================================================================
v0.55.0 – v0.57.1 — Handle/overlay polish, multi-selection matrix drag
================================================================================
- v0.57.1: darker, firmer viewport icon backdrop.
- v0.57.0: projector name above the IC circle in multi-selection.
- v0.56.0: IC matrix viewport drag for multi-selected projectors.
- v0.55.0: size handle stays on the target plane, +Z aim every pass, banking as a
  horizontal drag, lens<->target line, name HUD.

================================================================================
v0.54.0 — Projector banking
================================================================================
- Banking as a persistent parameter rolled inside the shared look-at (AimZAxis);
  RADIAL circle/knob handle, 45° snap, double-click reset; jump-free free-target
  mode; hidden Protection tag (rotation locked, position free).

================================================================================
v0.47.25 – v0.53.1 — C++ foundations (aiming, search, updater, drag)
================================================================================
- v0.53.0: database updater present in all four Cinema 4D builds
  (R25/2024/2025/2026), static OpenSSL 3, hard build failure if absent.
- v0.52.0: Projector Search revamp (lens/accessory trees, specs grid, field:value
  search).
- v0.51.0: aim delegated to common-motor-cpp + Projector Search command (ID 1069548).
- v0.50.0: interaction overhaul (body grab zone, free 3D target drag, live cone);
  textures moved to res/images.
- v0.49.0: C++ target-aiming system (world target, +Z convention).
- v0.48.0: schema version warning, per-parameter visibility toggles, Force Database
  Update.
- v0.47.25: CDN URL uses the /prod prefix, update pipeline validated end-to-end.

(Older history: see the annotated Git tags in the repository.)
