the layout graph is maintained, and the shine draws for real

- the interface's layout graph is maintained as anchors change instead of rediscovered from scratch: 1.48 ms per mutated frame becomes 34 µs, and end to end a running cast now costs +0.14 ms where it cost +4.33
- the autocast shine draws its authored population — 1200 particles through the star's own projection, spinning as the model says
- the shine is clipped to its own widget: the model frame installs its rect as the viewport, so nothing it draws leaves the button

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-17 17:01:46 +07:00
co-authored by Claude Fable 5
parent e4b6776b81
commit 2381df5a58
13 changed files with 1307 additions and 461 deletions
@@ -137,7 +137,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
let changed = input.width.to_bits() != w.to_bits();
input.width = w;
if changed {
model.touch_layout();
// A size write moves no edge and no roster membership (decision 1388).
model.touch_layout_frame(h);
}
Ok(())
})?,
@@ -151,7 +152,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
let changed = input.height.to_bits() != ht.to_bits();
input.height = ht;
if changed {
model.touch_layout();
model.touch_layout_frame(h);
}
Ok(())
})?,
@@ -167,7 +168,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
input.width = w;
input.height = ht;
if changed {
model.touch_layout();
model.touch_layout_frame(h);
}
Ok(())
})?,
@@ -423,13 +424,40 @@ fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Re
.iter()
.any(|a| a.point == point);
if !same_at_tail {
// Value-only unless the target moved (decision 1388) — the per-frame `SetPoint` idiom
// (a dragged window, a moving spark) re-points the SAME anchor at the SAME target with new
// offsets, so it names its node and the cached graph survives the frame.
let structural = anchor_retarget_is_structural(&input.anchors, &new);
input.anchors.retain(|a| a.point != point);
input.anchors.push(new);
model.touch_layout();
if structural {
model.touch_layout();
} else {
model.touch_layout_frame(h);
}
}
Ok(())
}
/// Would this `SetPoint` change the node's set of anchor TARGETS — i.e. is it structural?
///
/// The layout scope's reverse edges are built from `Anchor::relative_to` (decision 1350): "this
/// node reads that node's rect". An anchor whose OFFSETS moved is a value change — the node
/// re-solves and nothing else does, which is exactly what a precise touch claims. An anchor whose
/// TARGET moved is not: an edge has to disappear and another to appear, and no per-node hash can
/// say so, so the cached graph must be thrown away instead (decision 1388).
///
/// Mirrors the `retain(point) + push` the setters do. It is value-only in exactly one shape — one
/// existing anchor carries this point and keeps the same target. Zero (an edge appears) and two or
/// more (edges disappear) are both structural, and so is any change of target.
pub(crate) fn anchor_retarget_is_structural(anchors: &[Anchor], new: &Anchor) -> bool {
let mut same_point = anchors.iter().filter(|a| a.point == new.point);
match (same_point.next(), same_point.next()) {
(Some(old), None) => old.relative_to != new.relative_to,
_ => true,
}
}
/// Bit-exact anchor equality — the same lens the layout gate's fingerprint reads anchors through
/// (`InputFingerprint::anchors` feeds `f32::to_bits`), so the setters' no-op detection and the
/// gate can never disagree about whether a write "changed" something.
@@ -451,7 +451,11 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
// anchored regions to conclude nothing had moved — the castbar's bug class in miniature, for
// every frame of such a drag (decision 1385).
if moved {
model.touch_layout();
// Offsets only — a resize grip never repoints an anchor, so the frame names itself and the
// cached graph survives the drag (decision 1388). A drag is per-frame by nature: this is
// the difference between a smooth window resize and one that re-derives 13,656 nodes on
// every mouse-move.
model.touch_layout_frame(sz.frame);
}
model.sizing = Some(FrameSizing { sample: pos, ..sz });
}
@@ -481,8 +485,9 @@ pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
}
// The one mutation path every layout setter shares — the tier-1 epoch, never the
// solver's arrays (see [`super::layout_methods`]'s mutate-only-on-change law; a zero
// delta returned above, so this write always moved something).
model.touch_layout();
// delta returned above, so this write always moved something). Translating every
// anchor moves no TARGET, so the frame names itself (decision 1388).
model.touch_layout_frame(mv.frame);
}
}
model.moving = Some(FrameMove { sample: pos, ..mv });