🐛 Take the first equally near variant on a switch

A switch picks the variant whose properties are nearest the wanted
ones; between equally near ones it took the last child of the variant
container. Since combine as variants keeps the order of the components
it combines, that is the last of them: switching an empty variant to
"primary" gave primary/icon+test where the QA case (PENPOT-2446)
expects primary/hover, the first primary variant. It now takes the
first child among the equally near ones.

variants-switch-target-test: two variants one value away, in both
orders of the container's children.

Benchmark against the 1e baseline (lower quantile, ms):

  world                               baseline     now  ratio
  deepest-edit-through-three-levels     35.215  16.810   0.48
  edit-through-a-chain-of-dependents    68.931  32.765   0.48
  library-update-into-copies            83.083  39.232   0.47
  main-edit-among-many-shapes            4.837   2.277   0.47
  main-edit-into-10-copies               1.790   0.807   0.45
  main-edit-into-100-copies             15.415   8.054   0.52
  main-edit-into-1000-copies           153.086  81.186   0.53
  variant-switch-into-copies            32.052  17.933   0.56

AI-assisted-by: claude-opus-5-5
This commit is contained in:
alonso.torres 2026-10-16 10:46:42 +02:00
parent 86e622ed44
commit 37e233f6e1
2 changed files with 28 additions and 4 deletions

View File

@ -435,8 +435,9 @@
"The variant component that the variant copy head `shape` switches to when
its property at position `pos` takes the value `val`: among the other
variants with that value, the one whose properties are nearest to the
current ones. Nil when the property already has that value or no variant
has it."
current ones; between equally near ones, the first among the children of
the variant container. Nil when the property already has that value or no
variant has it."
[libraries shape pos val]
(let [component-id (:component-id shape)
component (ctf/get-component libraries (:component-file shape) component-id
@ -446,12 +447,16 @@
objects (-> (ctf/get-component-page ldata component) :objects)
target-props (-> (:variant-properties component)
(update pos assoc :value val))
distance #(ctv/distance target-props (:variant-properties %))
;; in the order of the container's children
candidates (->> (cfv/find-variant-components ldata objects (:variant-id component))
(remove #(= (:id %) component-id))
(filter #(= (get-in % [:variant-properties pos :value]) val))
(reverse))]
(when (seq candidates)
(apply min-key #(ctv/distance target-props (:variant-properties %)) candidates))))))
(reduce (fn [nearest candidate]
(if (< (distance candidate) (distance nearest)) candidate nearest))
(first candidates)
(rest candidates))))))
(defn valid-switch?
"Whether the variant copy head `shape` can switch its property at position

View File

@ -66,6 +66,25 @@
target (clv/find-switch-target (libraries file) (ths/get-shape file :copy01) 0 "p1v2")]
(t/is (= (thi/id :c03) (:id target)))))
(t/deftest switch-target-between-equally-near-variants-is-the-first-of-the-container
;; From p1v1, p2v1, setting Property 1 to p1v2 can give c02 (p1v2, p2v2) or
;; c03 (p1v2, p2v3), both one value away; the one placed first among the
;; container's children wins.
(let [file (-> (thf/sample-file :file1)
(thv/add-variant-two-properties :v01 :c01 :m01 :c02 :m02)
(ths/add-sample-shape :m03 :type :frame :parent-label :v01
:variant-id (thi/id :v01) :variant-name "p1v2, p2v3")
(thc/make-component :c03 :m03)
(thc/update-component :c03 {:variant-id (thi/id :v01)
:variant-properties [{:name "Property 1" :value "p1v2"}
{:name "Property 2" :value "p2v3"}]})
(thc/instantiate-component :c01 :copy01))
target (fn [order]
(let [file (ths/update-shape file :v01 :shapes (mapv thi/id order))]
(:id (clv/find-switch-target (libraries file) (ths/get-shape file :copy01) 0 "p1v2"))))]
(t/is (= (thi/id :c02) (target [:m01 :m02 :m03])))
(t/is (= (thi/id :c03) (target [:m01 :m03 :m02])))))
(t/deftest switch-into-own-main-is-a-nesting-loop
(let [file (-> (thf/sample-file :file1)
(thv/add-variant :v01 :c01 :m01 :c02 :m02)