Keep one invalidation table, and derive the inverse a UI wants
There were two answers in the tree to "which stored bytes stop being valid when
this knob moves", and only one of them was checked.
`flow/address/block-knobs` is per block and asserted by biconditional —
`address-test` re-freezes the take once per knob and requires that the bytes
changed if and only if the key did. `domain/params`'s `:affects` was per area,
had no caller but a test asserting it returned what it was written as, and was
already wrong in both directions on the one entry where the two granularities
disagree: `:aperture-cut` claimed `#{:mouth}`, where it reaches no block, and
omitted the teeth, whose contour bytes it genuinely moves by gating
`condition/interior`'s smoothing. `:blink-cut` claimed `#{:eye}` and reaches no
block either, because a blink is `[:vis]` keys in tier 1.
So `:affects` and `affected-areas` are gone, and `address/knob-roles` is the
derived inverse of the table that is asserted — which is what a parameter panel
actually wants to ask. A knob absent from it invalidates no block, and that is
an answer rather than a gap.
Two new assertions keep the derivation from rotting at either edge: every role
in the table is reachable from some knob, and every knob a block declares is one
the registry defines. The second closes a real hole — `block-descriptor` checks
only that a knob was PASSED, and the freeze's `merge take/knobs` makes that true
of anything spelled like a keyword, so a typo in `block-knobs` would have named a
setting no slider can move.
228 CLJS tests, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9cd5243983
commit
27bfe18bee
5 changed files with 85 additions and 13 deletions
|
|
@ -204,6 +204,20 @@ bug waiting for a bigger project:
|
|||
This table is the argument for the split, and it should be derivable from the
|
||||
sub graph rather than maintained by hand.
|
||||
|
||||
As built, half of it is, by a different route. `flow/address/block-knobs` is this
|
||||
table for tier 2 — per block, the settings its bytes depend on — and
|
||||
`address-test` asserts it by biconditional rather than deriving it, which is
|
||||
stronger than a derivation would have been: a derived table is only as right as
|
||||
the graph it reads. What is not written down anywhere is the rest of the column.
|
||||
Stages 3 to 5 are one call chain in `flow/take/measure` rather than a chain of
|
||||
subs, so there is no graph above `::base-scene` to read a dependency off, and the
|
||||
tier-1 half of a re-freeze is therefore whole-clip.
|
||||
|
||||
An earlier arrangement had a second table in `domain/params`, per feature area,
|
||||
saying which areas a knob affected. It disagreed with the tested one on the first
|
||||
entry where the two granularities part company and it had no caller; see that
|
||||
namespace for why one checked table beats two.
|
||||
|
||||
| Knob | Invalidates from |
|
||||
| --- | --- |
|
||||
| model / footage | 2 detect |
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
(ns arthur.domain.params
|
||||
"Definitions for generated settings. Defaults, scope and value constraints live
|
||||
here once; UI metadata and regeneration rules can be added to each entry.")
|
||||
"Definitions for generated settings: defaults, scope and value constraints, once.
|
||||
|
||||
WHAT IS NOT HERE. A knob's INVALIDATION — which stored bytes stop being valid
|
||||
when it moves — is `arthur.flow.address/block-knobs`, and it is there rather
|
||||
than here for two reasons that are not about layering.
|
||||
|
||||
It is per BLOCK and this registry is per AREA, and the difference is not
|
||||
granularity, it is disagreement. `:aperture-cut` is a mouth setting that reaches
|
||||
the TEETH block's bytes and does not reach the mouth's, because it gates the
|
||||
contour smoothing and the mouth's own geometry is smoothed either way.
|
||||
`:blink-cut` is an eye setting that reaches no block at all, because a blink is
|
||||
`[:vis]` keys in tier 1. An `:affects #{:eye}` on that entry read as documentation
|
||||
and was wrong in both directions.
|
||||
|
||||
And `block-knobs` is TESTED — `address-test` re-freezes the take once per knob
|
||||
and asserts the biconditional — while a field here could only be believed. An
|
||||
earlier version of this namespace carried `:affects` and an `affected-areas`
|
||||
reading it, and the only thing that ever called it was a test asserting it
|
||||
returned what it was written as. Two tables where one is checked and one is not
|
||||
is worse than one table, because the unchecked one is the one a parameter UI
|
||||
would reach for first. `address/invalidates` is the derived inverse, and it
|
||||
cannot drift from the thing that is asserted.")
|
||||
|
||||
(def definitions
|
||||
{:anchor-avg {:area :subject :default 2 :type :integer :min 0
|
||||
:affects #{:head :mouth :eye :brow :teeth}}
|
||||
:contour-avg {:area :subject :default 1 :type :integer :min 0
|
||||
:affects #{:mouth :eye :brow}}
|
||||
{:anchor-avg {:area :subject :default 2 :type :integer :min 0}
|
||||
:contour-avg {:area :subject :default 1 :type :integer :min 0}
|
||||
:verts {:area :mouth :default 8 :type :integer :min 4 :even? true}
|
||||
:aperture-cut {:area :mouth :default 0.12 :type :number :min 0 :max 1}
|
||||
:eye-verts {:area :eye :default 8 :type :integer :min 4 :even? true}
|
||||
|
|
@ -34,10 +52,6 @@
|
|||
|
||||
(def areas #{:subject :mouth :eye :brow :teeth})
|
||||
|
||||
(defn affected-areas [id]
|
||||
(when-let [spec (get definitions id)]
|
||||
(or (:affects spec) #{(:area spec)})))
|
||||
|
||||
(defn for-area [wanted-area]
|
||||
(into {} (keep (fn [[id {:keys [area default]}]]
|
||||
(when (= wanted-area area) [id default])))
|
||||
|
|
|
|||
|
|
@ -152,6 +152,28 @@
|
|||
"teeth" [:anchor-avg :aperture-cut :blob-grow :cavity-erode :min-area
|
||||
:teeth-on :teeth-smooth :teeth-verts :tongue-reject :top-bias]})
|
||||
|
||||
(def knob-roles
|
||||
"knob -> the roles whose bytes it moves. The inverse of `block-knobs`, DERIVED.
|
||||
|
||||
This is what a parameter UI wants — \"what stops being valid if I drag this\" —
|
||||
and deriving it is the whole point: `block-knobs` is the table `address-test`
|
||||
asserts by biconditional, so an answer computed from it cannot drift from an
|
||||
answer that is checked, and an answer written down beside it could.
|
||||
|
||||
A knob absent from this map invalidates NO BLOCK, and that is a real answer
|
||||
rather than a gap. `:blink-cut`, `:iris-size` and `:pupil-size` are all absent,
|
||||
because a blink is `[:vis]` keys and the two sizes are framed channels: tier 1,
|
||||
editable, and rewritten by a re-freeze without a byte of tier 2 moving."
|
||||
(reduce (fn [m [role knobs]]
|
||||
(reduce (fn [m knob] (update m knob (fnil conj #{}) role)) m knobs))
|
||||
{}
|
||||
block-knobs))
|
||||
|
||||
(defn invalidates
|
||||
"The roles one knob's bytes depend on, or an empty set."
|
||||
[knob]
|
||||
(get knob-roles knob #{}))
|
||||
|
||||
(defn block-descriptor
|
||||
"The canonical text naming one dense block.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@
|
|||
(is (= :person-4 (get-in scene [:features :eye-8 :subject])))))
|
||||
|
||||
(deftest parameter-definitions-drive-the-current-take-defaults
|
||||
(is (= #{:eye} (params/affected-areas :blink-cut)))
|
||||
(is (= #{:head :mouth :eye :brow :teeth}
|
||||
(params/affected-areas :anchor-avg)))
|
||||
(is (params/valid-settings? :eye {:blink-cut 0.2}))
|
||||
(is (not (params/valid-settings? :eye {:verts 8}))))
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,31 @@
|
|||
(str "add " id " to block-knobs for " (pr-str role))
|
||||
(str "remove " id " from block-knobs for " (pr-str role)))))))))))
|
||||
|
||||
(deftest the-knob-inverse-is-derived-and-total
|
||||
;; `invalidates` is the parameter UI's question — "what stops being valid if I
|
||||
;; drag this" — and the reason it is derived rather than written down is that
|
||||
;; the thing it is derived FROM is the thing the biconditional above asserts.
|
||||
;; These two assertions are what keep the derivation honest in the only two ways
|
||||
;; it could rot.
|
||||
(testing "every role in the table is reachable from some knob"
|
||||
(is (= (set (keys address/block-knobs))
|
||||
(into #{} (mapcat val) address/knob-roles))))
|
||||
(testing "every knob a block declares is a knob the registry defines"
|
||||
;; The other direction of the same edge: a typo in `block-knobs` would name a
|
||||
;; setting no slider can move, and `block-descriptor` only checks that the
|
||||
;; knob was PASSED, which the freeze's `merge take/knobs` makes true of
|
||||
;; anything spelled like a keyword.
|
||||
(is (empty? (remove #(contains? params/definitions %)
|
||||
(into #{} (mapcat val) address/block-knobs)))))
|
||||
(testing "a knob that moves no bytes says so, rather than having no answer"
|
||||
;; Tier 1 knobs. Each one rewrites keys or a framed value on a re-freeze and
|
||||
;; no block's address changes, which is why the empty set is the right answer
|
||||
;; and not a missing entry.
|
||||
(doseq [knob [:blink-cut :iris-size :pupil-size]]
|
||||
(is (= #{} (address/invalidates knob)) (str knob)))
|
||||
(is (= #{"teeth"} (address/invalidates :aperture-cut))
|
||||
"the mouth's threshold reaches the teeth block and no other")))
|
||||
|
||||
(deftest the-teeth-table-is-asserted-at-the-descriptor
|
||||
;; Every knob the teeth block declares must reach its key. The other half — that
|
||||
;; each one also moves its bytes — needs real pixels, because the crop, the otsu
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue