Skip to content

Releases: polynaut/dth-character-studio

DTH Character Studio v0.92.0

Choose a tag to compare

@polynaut polynaut released this 26 Aug 20:45
Immutable release. Only release title and notes can be modified.
117057b

What's changed

  • A missing baker texture the repath cannot rehome now names the product to reinstall. The Utils drawer's Baker-textures row searches your DIM manifests folders (the same set the product scan reads) for each unfindable file and, when an install manifest lists it, replaces the dead-end "reinstall the product" with the product's name and SKU — one line per product, ready for DAZ Install Manager. The match is on the file's folder as well as its name, so a texture name a dozen products share cannot point you at the wrong one; a file no manifest knows keeps today's generic wording.

DTH Character Studio v0.91.0

Choose a tag to compare

@polynaut polynaut released this 26 Aug 18:16
Immutable release. Only release title and notes can be modified.
239bb1c

What's changed

  • Dismissing a toast no longer closes the drawer or dialog under it. The toast layer mounts outside the overlay's own DOM, so its close ✕ read as an "outside click" — the drawer treated it as a backdrop dismiss. Clicks inside the toast layer are now exempt from outside-click dismissal for both the side panels and the dialogs.
  • Make paths portable now FIXES paths under a foreign Daz library root — the missing baker textures and "cannot be made portable" references a moved library (or a .hip from another machine) leaves behind. When a path's library-relative tail exists under your configured library, the repath repoints it there as $DAZ3D_LIB/… (portable straight away); the General tab counts these as fixable instead of stuck, the card badge says which missing textures the button repairs, and the report names each old → new pair. Only a path whose target file actually exists is ever rewritten — everything else stays reported, exactly as before.

DTH Character Studio v0.90.0

Choose a tag to compare

@polynaut polynaut released this 26 Aug 04:23
Immutable release. Only release title and notes can be modified.
64753ac

What's changed

  • The Daz product scan can now read MULTIPLE DAZ Install Manager manifests folders. Users who organize their installs across several DIM libraries (ManifestFiles, ManifestFiles2, …) add the extra folders in Settings — General (Home window) or the Project tab, under the primary field, which an activated Daz installation may still derive. The generated scan scripts bake all folders as one '|'-joined spec (runtime v104 splits it; '|' is illegal in Windows paths), a missing folder skips only its own manifests, and adding or removing a folder re-flags the characters' scripts for Tools → Refresh assets exactly like moving the single folder always has.
  • The character page's scripts pane now shows both pipeline directories: the "Export directory" row is renamed to "Daz export directory" (with a shorter hint), and a new "Export directory" row below it shows the character's FINAL export folder — where the Houdini DTH networks write the files that are then imported into Unreal Engine.

DTH Character Studio v0.89.0

Choose a tag to compare

@polynaut polynaut released this 25 Aug 17:35
Immutable release. Only release title and notes can be modified.
1c7eb77

What's changed

  • Running a character's visible ROM_… script by hand no longer saves the scene as rom-animations/<stem>_ROM.duf (runtime v103). That save is the DTH-Export flow's job — the hidden carriers still write it — but the manual script did it too, silently overwriting the flow-built ROM on disk and repointing the open scene's filename to the _ROM.duf. A manual run now builds the ROM on the timeline and stops. Run Tools → Refresh assets to regenerate installed scripts.
  • A crashed Unreal editor no longer leaves the DTH Export button Working forever. The import's state is derived from files in the project's Saved/DTHStudio/ folder, so an editor that died mid-import — leaving a claimed job and a result frozen at running — kept re-deriving a live import on every poll, across app restarts, behind a deliberately inert button. The poll now measures liveness for a CLAIMED import: when no editor process exists at all, or every running editor is identified and none holds that project, the import is reported as the failure it is and the run ends through the normal outcome path (failure toast, files cleaned). The verdict abstains wherever the studio cannot actually see: an editor whose project can't be read might be the one running the import; a platform that cannot enumerate editors at all (everything off Windows, where the probe is a stub) decides nothing; a failed probe read is a read hiccup, not a dead editor; and a job still queued unclaimed is just waiting — that is the normal queue-then-open flow. A dead verdict also re-reads the result file before believing itself, so an import that finished in the moment the probe took is reported as the success it was.
  • Every exporting DTH Export row now gets a FRESH Daz Studio session, and every row's export is judged by the exporter's own motion summary. Daz's re-evaluation of fitted followers silently degrades after a scene re-load inside one Daz session (measured 2026-08-24/25, DS4 4.24: every scripted export after a re-load froze eyes/grafts/clothing at 9–35% of the figure's moved frames, 5/5 reproductions — the exporter cannot fix it from inside). Two defenses ship together: - Fresh session per row (job-file contract v4, sessionPerRow; needs Runner v1.4.0): the Runner runs ONE row per Daz session and quits; the studio's new export supervisor starts the next session, kills a hung one (hard per-row timeout — a teardown was measured hanging indefinitely with the UI alive), requeues a crashed session's batch, and refuses to let a worn session (Daz already open with a scene) run row one. Older Runners keep working single-session — the gate below still catches the wear. - Motion-summary gate (studio-side, historical): after a batch, each scene's export log ("Alembic ROM motion summary", exporter ≥ 2.1.9) is parsed and the scene FAILS when multiple meshes moved on far fewer frames than the SAME meshes reached in earlier summaries of the same log — the measured degradation signature — or when nothing moved at all. Judging each mesh against its own history is what keeps scenes whose ROM legitimately leaves meshes still (a face the ROM never animates) from false-positiving; a first-ever export has no history and gates nothing. Thresholds are pinned by tests against verbatim blocks from both measured incident logs. A degraded scene drops out of the Houdini/Unreal continuation like any dead export set.

DTH Character Studio v0.88.4

Choose a tag to compare

@polynaut polynaut released this 25 Aug 12:29
Immutable release. Only release title and notes can be modified.
6431cff

What's changed

  • G9 characters keep their dialed pose-control shape through the ROM build (runtime v101). DTH's G9 base ROM presets carry explicit zero keys for ~700 value channels the ROM never walks — every stock breast pose control among them — so a character's dialed 100% "Breasts Up-Down" read 0% across the whole generated ROM, and a hand-fix at frame 0 died at the preset's next zero key. (The G8/G8.1 presets carry none of these channels, which is why the retired "Preserve morphs after ROM loading" option looked obsolete.) The runtime now restores such dials automatically, with no option or list: after all preset blocks, a root-figure dial that was non-zero before the ROM loaded and whose keys are ALL zero afterwards (zeroed flat — never walked) is flattened back to its pre-ROM value. Genuinely walked channels have a non-zero key somewhere and are never touched; ERC-driven halves are left to their master. A flat channel matches the base mesh on every frame, so the FBX and Alembic artifacts stay aligned. The same pass fixed a blind spot it would otherwise have exposed: the dialed-walked gate and the frame-0 morph anchor each resolved a morph name through their own truncated copy of the walk the ROM writer uses, so a node-owned G9 pose control could be walked while neither of them could see it. All three now share one resolver, and a walked pose control is reported and anchored like any other morph. Run Tools → Refresh assets to regenerate installed scripts onto the new runtime.
  • A DTH export that sampled no motion is no longer reported as success (runtime v102). The DTH Exporter (2.1.9+) can intermittently walk every ROM frame while the scene never re-evaluates — its own motion summary then reads "moved on 0 of N frames" — producing a statue alembic that used to land as a successful export and even purge the backups of the last good set. The export carrier now reads that summary: an all-zero verdict is a failed export (the previous set is restored, the report says why), and a run whose liveliest node barely moved lands with a run-log warning telling you not to trust the set. Only a summary written by the export that just ran can judge it, and every export now says in Daz's log what the audit concluded — so an un-audited export (older exporter, no summary) reads as un-audited rather than as clean. Exports from older exporter builds are otherwise unaffected.
  • Adopted oxlint 1.79's six React-compiler rules (deferred in #959, tracked in #960): all 52 flagged sites are now either genuinely fixed or carry a stated, per-site reason. The fixes are behaviour-preserving hardening, not features: - Latest-ref writes (ref.current = x during render — the drag editor, the unsaved-changes guard, the character draft, file drop, two kit hooks) moved into useInsertionEffect, so a render React discards can never leak into a ref. - Draft-follows-prop table cells (pose/bone/morph/JCM cells, the kit's NumberField) now reset through the new useDraftValue hook — adjusted during render instead of one frame late from an effect, so an outside commit never paints the stale draft first. - Async results that used a reset-effect (scene previews, the avatar size variant, the Houdini name-collision probe, the delete dialog's keep-Houdini probe) are now keyed by the inputs that produced them, so a stale result stops matching instead of needing to be cleared — a changed scene can no longer flash the previous scene's preview. - The attachment form seeds its name from the picked scene at initialization / in the pick itself, not via effects. Deliberate patterns (load-on-mount effects, trigger-only effect deps, floating-ui and TanStack Table API shapes, the drawer's two-phase mount, the live elapsed clock) keep their code and now state their reason at the site; the six config-off entries are deleted, so the rules gate new code at full strength.
  • A scene card's open menu now always offers Generate new ROM. It used to disappear whenever the saved ROM animation looked current — newer than both the scene file and the character's generated ROM script — with Ctrl as an undiscoverable way to force it back. That test assumes a file's mtime is an edit time, which stops being true in a Perforce or otherwise synced project: a sync that writes rom-animations/ after the scenes marks every animation "current", and the rebuild quietly vanishes from every scene with nothing on screen to explain it. The menu's two ROM rows now say what they mean — Open last ROM whenever the file is on disk, Generate new ROM always — and an animation older than the current definition keeps its "saved by an earlier run" tooltip, informing the choice instead of removing it.
  • The header's DTH Export button now stays in its Working state through the run's Unreal leg. Since the run learned to wait for the editor's answer, the task panel stayed up for the whole import — but the button had no face for that leg, so it dropped back to the idle "DTH Export" the moment the export legs were done, right under a panel saying the import was half way through. It now shows the Unreal mark with the leg's status and its own clock, and is deliberately inert: the import runs inside the Unreal editor, so there is nothing a click here could stop. The import's status line also lost its tail: "…is importing — the editor freezes while the DazToHue pipeline runs" overflowed the status bar and now reads just "…is importing".

DTH Character Studio v0.88.3

Choose a tag to compare

@polynaut polynaut released this 24 Aug 17:18
Immutable release. Only release title and notes can be modified.
c819032

What's changed

  • A DTH Export run that finishes into Unreal now ends when the editor answers, not when the job file is written. Writing that file takes a moment; the import that follows takes minutes — so the run reported "DTH Export finished in 8m 15s" over a progress bar that said the editor was half way through, and the two sat on screen together. Worse, the report tore the run panel down on its way out, so the next thing the import said rebuilt it with no task rows at all: a bare progress bar, ghosting a run that had already been declared over. The run's last leg is the import, so the report waits for it and carries its outcome as the report's last line — one report for the whole run, with the time the import itself took, instead of a finish toast and a separate outcome toast minutes apart. Nothing waits when there is nothing to wait for: a send that was refused, or a run with no Unreal project selected, reports the moment its export legs are done, exactly as before. The panel that now stays up for the wait keeps the run behind it: the Daz scenes and Houdini projects that finished stay ticked off and retire out of the list — and a leg that failed keeps its red row — instead of all of them dropping back to "waiting" and running the progress bar backwards. And an import that can never answer no longer takes the report down with it. If the job disappears before the editor picks it up, the run reports what its export legs really did and says the import's outcome is unknown, rather than waiting forever behind a job that is gone.

DTH Character Studio v0.88.2

Choose a tag to compare

@polynaut polynaut released this 23 Aug 19:34
Immutable release. Only release title and notes can be modified.
c3574b2

What's changed

  • A Houdini scan that lands after the DTH Export dialog opened now reaches it. The dialog read the stored scan ONCE when it mounted, so a project scanned later — by the background sweep, or by a manual Rescan — stayed invisible to it: picking a scene auto-selected no Houdini project, the Networks and Characters chips never appeared, and the run's task list showed a single row for a .hip that exports two networks. Reloading the app was the only way out. It surfaced right after the scan-version bump that shipped with the two-network scan fix, because that invalidates every stored scan at once and makes the gap between opening the dialog and the sweep landing as wide as it gets. The scans themselves were correct the whole time. Both readers now re-read: the dialog joins the sweep already in flight when it finds fewer scanned projects than linked ones, and the task list's copy refreshes every time the dialog opens — the moment that always precedes a run. A scan arriving under an open dialog cannot un-do the user's own picks: a Houdini project unticked by hand stays unticked when the scan lands on top of it, and only a fresh scene selection brings the default answer back.

DTH Character Studio v0.88.1

Choose a tag to compare

@polynaut polynaut released this 22 Aug 05:31
Immutable release. Only release title and notes can be modified.
5908d69

What's changed

  • Finished rows leave the DTH Export run list. A job that completes wears its tick for a beat — long enough to be seen — then fades, drifts aside and collapses, and the rows still to come close up over it. The list is a queue readout, and on a run into several Houdini projects the work still ahead used to scroll out of its five-row box behind work that was already over. A failed row never retires. It is the one row that has to catch the eye after the run has moved on, and disposing of failures beside successes would be exactly the lie its red edge exists to prevent. Row numbers count the whole run, retired rows included, so a row's ordinal never changes under the reader.
  • A Houdini project holding two DazToHue networks in ONE subnet now reports both of them, not one twice. The scan resolved a node's network by its parent and took the first daztohueimport child it found there — right for a subnet with one network, wrong for /obj/DazToHue holding two side by side, where both export nodes answered with the first import's name and the dedupe collapsed them. The DTH Export dialog showed such a project as writing a single export set; the run list showed both all along, because it names nodes by their own network box. Resolution is per NODE now, walking its inputs upstream to its own import node, with the parent's sole import as a fallback and an honest "cannot tell" when a node is unwired in an ambiguous parent. The same correction lands on the PoseAsset CSV pairing (both CSVs in such a project were checked against the first network's .dth) and on the run's own node targeting. Stored scans are re-earned automatically: SCAN_ANSWER_VERSION goes to 9, because the old answer is a legitimate value — "this project writes one set" — and would otherwise be served forever.
  • A Houdini project that failed no longer ticks itself off. The run's task list could only read a failure out of the per-network memo — so the commonest .hip there is, one holding a single DazToHue network, rendered its row positionally: "the queue has passed it" was the whole verdict, and a failed export came out struck through and green beside the projects that worked. The same held for a project that could not START, whose scan-named rows never had a run behind them at all. Both positional paths now take the leg's own verdict — the same run-report entry whose count already decides that the row is finished, so the two cannot disagree. Where the memo knows which network failed, it still wins: one red row among the ones that worked, not a project painted red.
  • A Daz or Houdini export leg that dies silently can no longer report as a success. - Export-landed guard: after the Daz batch finishes, each scene's export set is judged from the disk before the Houdini leg consumes it — a 0-byte .dth, a missing manifest, or the export sweep's .dthprev backups still standing (the signature of a script Daz's engine killed mid-export) now fails that scene loudly and drops it from the Houdini continuation, instead of cooking the corpse into a green checkmark. A scene that failed OUT LOUD — a failed Runner row, or a script that reported its own failure — now drops out of that continuation as well: its export folder looks landed only because the failure path put the previous export back, and handing it on imported last week's character under this run's checkmark. "Skip Daz — use last exports" gets the same verdict: its readiness test was "a .dth is there", which a 0-byte corpse passes — the mode whose whole input is what happens to be on disk now refuses to run Houdini over a dead export, and says which scene and why. - The export backup sweep finishes its job again (runtime v100): the .dthprev step that restores the previous export on failure and purges it on success was listing through a directory handle read before its own renames, so it did neither — a successful export kept every backup it made, and a failed one never got the previous set back. Refresh assets regenerates the installed scripts. Because a script from the previous runtime still leaves its backups behind, leftover backups are now reported as a warning rather than treated as proof the export failed; the .dth decides. - Honest Houdini death reporting: a headless export that exits without a word is reported as "Houdini exited during <its last step>" instead of quoting a stale load-time warning as the cause; the hython exit code — previously discarded by the fire-and-forget spawn — rides along (hex spelling included for Windows crash statuses).

DTH Character Studio v0.88.0

Choose a tag to compare

@polynaut polynaut released this 21 Aug 05:35
Immutable release. Only release title and notes can be modified.
136549c

What's changed

  • Refresh assets offers to carry a new DazToHue release into your Houdini projects A refresh only ever fixed half the pipeline. Your .hip files keep the DazToHue asset definitions they were built with, so a new DazToHue release left every project on the old ones until you opened each one in Houdini and pressed Refresh Assets on the shelf. The studio could already run that tool headlessly for one character (Utils → Refresh assets); it just never connected the two ends. Now, when Tools → Refresh assets notices the DazToHue release has changed since it last looked, it offers to sweep every linked Houdini project through DazToHue's own tool — all of them, in one hython run, without opening Houdini. The offer needs the Houdini installation folder and its matching documents folder in Settings; without them it stays away. It is still not a check, and does not pretend to be one. Nothing in a .hip records which DazToHue release its assets came from, so what the studio keeps is only its own record: which projects it has run this on, and under which release. A project it has never swept reads as "never refreshed by the studio" — the absence of a verdict, not a verdict. A project already refreshed under the active release is skipped and said to be skipped. The parts that decide what happens next are built so a bad run cannot strand work. Dismissing writes nothing, so the offer returns on the next refresh. A sweep where any project fails records the ones that worked but leaves the release outstanding, so the next refresh re-offers exactly the remainder — the usual cause of a failure here is DazToHue not being installed for the Houdini version the studio points at, which is something you fix and then expect to retry. Each project is copied into its backup/ folder before it is saved, and this report keeps Undo this run on the projects that succeeded, not only on the ones that failed: putting one project back on the previous DazToHue release is a want that arrives days later. Undoing also makes the studio stop counting that project as refreshed, so it comes back into the offer instead of quietly reading as done while sitting on the old assets. That copy is rolling — one per project — so saving a project replaces whatever is already beside it, which is precisely the copy somebody kept in order to go back a release. It would have gone silently, inside a run started by a button labelled "Refresh". Now it doesn't: if any project the sweep would touch already has a studio backup, the dialog says so in red, lists them with their dates, and refuses to start until you accept. Only the projects the run actually saves lose their old copy — one that reports no change, or that fails, keeps what it had — and a dry run is never held and never touches a backup at all.
  • Generate Houdini project asks which Daz scene on the first project too. A multi-scene character's first generated project was wired to the primary scene without asking — a choice made on the user's behalf that nothing on screen admitted to, and that only surfaced as five import paths aimed at the wrong scene inside the finished network. The Daz scene to import picker now appears whenever more than one scene is linked, first project included. The primary is still the default, so pressing Generate straight away wires it exactly as before; a first project for an outfit scene now costs one click instead of a throwaway project or five hand edits in Houdini.
  • Fix: the Houdini Utils drawer's "backups removed" confirmation now disappears on its own after a few seconds. Every other toast the drawer raises reports the result of a run that took hython tens of seconds, so it stays until dismissed — but a clean backup sweep on drawer close is housekeeping the user just asked for and watched happen, and leaving it pinned meant hand-dismissing a message with nothing to act on. The partial form ("2 of 3 backups removed — the rest are in use and stay") still sticks, because it names copies still sitting on disk and the drawer closing behind it makes that toast their only mention.
  • The Houdini refresh offer is a dialog again, not a form Everything that dialog said was true and worth saying — it runs DazToHue's own tool so it can't say in advance what will change, no check anywhere says a project needs it, the dry run is a weaker promise than elsewhere, the backup is one rolling copy. It said all of it on the surface, in six paragraphs, and six paragraphs of hedging is not honesty: it's a wall that gets skimmed, and skimming costs exactly the attention the one destructive line needs. The caveats moved behind the , where the rest of the app keeps this kind of thing. What's left on the surface is the decision: the release line, one list, the consent line when there's something to consent to, and the buttons. It's about half the height it was. The list absorbed what used to be prose. A Last refreshed by the studio header is what makes a bare never honest — it's the column's meaning, not a verdict about the project — and each project's last release moved to its own right-hand column instead of a sentence per row. Backups at risk are marked on the rows they belong to (backup at risk, with the file and its date on hover) rather than listed a second time underneath: they're the same projects, and a second list of the same names reads as twice the work. What's left of the warning is one red line and its switch, and that line is now a sentence rather than a specification: "Running may replace 2 existing backups.", with a Let it switch beside it. It used to read "2 existing backups at risk — one rolling copy per project, overwritten for whichever projects this run saves", which is accurate and unreadable, and a precise sentence nobody parses protects nobody. The distinction it was carrying — only the projects the run actually saves lose their old copy, and nothing can know in advance which those are — is real, and now lives behind the ⓘ where there is room to say it properly. An "i" popup now scrolls instead of running off the bottom of the window. Found by this change and fixed underneath it: a popup taller than the room under its "i" was simply rendered off-screen, unreachable — the page cannot be scrolled to it. This one was, so the paragraph telling you to close Houdini first was not in fact "one click away". Every info popup in the app gets the fix. Nothing about the gate changed: a real run is still held until you accept, and a dry run is still never held.
  • Follow-ups to the rename cleanup. Three fixes to what renaming a character reports and remembers. The Houdini Utils drawer keeps a copy of every project it saves — that copy is what Undo this run restores, and what the drawer offers to clear when you close it. The new retarget operation's copies were missing from the list the drawer builds, so they would have been offered for neither. Nothing routes a retarget through the drawer today, so this was not reachable; it is fixed before it is. Renaming a character that has no exports yet but does have a linked Houdini project now says what it is actually doing. It used to announce "Clearing the old exports…" when there were none, and then report back with a lower-case "repointed 1 Houdini project." asking you to rebuild a set that had never been built. It now says "Repointing the Houdini projects…" and "Repointed 1 Houdini project. Run DTH Export to fill them." And when the rename cannot follow itself into one of your projects, the warning now tells you where the studio's copy of that project from just before the attempt is, instead of only suggesting you repoint the paths by hand.
  • The item-scope row under a frame-0 morph now reads at the same size as the rest of the form. The node badge, the this scene badge and the unscoped "All items" text were set at 10px — small enough that the scope a row applies at, which is the thing that decides whether a fit value lands on one bag or on the whole outfit, was the least legible part of the row. They match the "Daz UI name" line beside them now.
  • DTH Export names the jobs each project will contribute, before the run starts The panel's Houdini rows now carry one chip per DazToHue network the stored scan says that project writes, and the Unreal rows one chip per character this run would land in that project. So the size of a run — two networks in one .hip, one character re-imported and another dropped — is readable before pressing Start, instead of only once the task list has filled in. The two lists answer different questions, and the rows are worded for it. The Houdini chips describe the project: they stand whether the row is ticked or not, and under "Skip Houdini", which runs no Houdini leg at all. The Unreal chips describe this run, already narrowed to the sets the studio located in that project — the send is re-import only, so those are exactly the import jobs the run will queue for it. Both stay silent where the studio cannot say: an unscanned Houdini project names nothing rather than claiming it writes none, a run that produces no export names no characters rather than promising the stale folder on disk, and a set the Unreal project has never held is left off, because the run would drop it too.
  • Renaming a character now takes its exports with it. The exporter names every file it writes after the character — and, measured on a real export, writes the name inside them too: a .dth carries "Character Name" and absolute paths to its own .fbx/.abc siblings. So a rename used to leave a full export set on disk that nothing would ever write to again, while the Houdini projects went on importing it by the old name — silently, because those files still exist and still load. Renaming a character that has no exports yet is unchanged: it just renames. Renaming one that does now opens a dialog first, itemizing...
Read more

DTH Character Studio v0.87.0

Choose a tag to compare

@polynaut polynaut released this 20 Aug 10:27
Immutable release. Only release title and notes can be modified.
2fedffc

What's changed

  • The Unreal project cards in the project footer now carry the same Utils wrench the Daz-scene and Houdini cards do, in place of their old install button. It opens a Utils drawer whose Install tab holds the list that used to be a modal — DTH content, the DTH Character Studio Runner, and every configured plugin build matching the project's engine version — as a full-height drawer with Install pinned to its bottom edge, so a long plugin list no longer scrolls the button out of reach. What is ticked for you has changed: it is now what the project is missing. Anything already installed and current starts unticked — tick it to install it again (a checked row still overwrites). The one exception is an installed-but-outdated DTH Character Studio Runner: the card's amber ⚠ says "re-install it", so the drawer offers that row like an absent one and marks it out of date. The card's amber ⚠ now covers both reasons a linked project still needs setting up — no DTH content yet, or an out-of-date Runner — and clicking it opens the drawer that fixes it. The wrench itself stays neutral, like every other card's.
  • Closing a dialog or drawer no longer pops the tooltip back up. Tooltips are swept away when an overlay opens (they render above it), but closing one hands focus back to the control that opened it — and a tooltip on focus shows with no delay, so it reappeared over the app under a mouse that had never moved. Focus now only shows a tooltip when the focus is the keyboard's: tab to an icon-only control and its description still appears immediately, while focus the app moved for you — an overlay closing, a click landing on a button — stays quiet. Hovering is unchanged.