fix(iced/feedback): 不定态两条 p1 —— 线性先排序合并(P1-A)+ 环形补全局旋转(P1-B)
复审 r1 的两条 p1 都是「报告写了 ✓、图上没有」,逐条修:
P1-A(视觉)线性不定态把两段画成一段:
- 根因:`phase_segments` 的两段**不保证有序**(第一线前缘抢跑 250ms、第二线从 0 起),
`linear()` 却按返回顺序单调铺 `FillPortion`,且份额之和在 34/100 相位上 >1(最大 1.99),
iced 按总和归一 ⇒ phase 0.55 的两段被压成一段 100px 台阶。
- 修:新增 `layout_spans`(排序 + 合并重叠 + 显式铺间隙)与 `portion_shares`(累计舍入,
份额之和**恒为 1000**),`linear()` 只消费这两者。
- 断言:`raw_segments_are_unordered_and_layout_sorts_them`(原始输出不有序、排序后位置对得上
A1 keyframes)、`portion_shares_always_sum_to_the_full_track`(200 个相位全 = 1000)、
以及渲染层 `12.4:phase 0.55 必须数出两段`(旧实现会数到 1 段)。
P1-B(代码)环形不定态少一条全局旋转 + keyframes 首点语义:
- 官方是两条旋转相加:`CircularGlobalRotationDegreesTarget = 1080f`(`tween(6000, LinearEasing)`,
javap 偏移 5125)与 8 关键帧旋转(0→360°),绘制时 `fadd` 后 rotate ⇒ 每周期 1440°(4 圈)。
我们只做了 360°,转速是官方的 1/4。
- `ROTATION` 首点在 300ms/90°:Compose 会自己补 t=0(值 = 动画初值 0),我们没补 ⇒ `[0,0.05)`
恒 90°,且周期末尾 360° → 下周期 90° 瞬时倒跳 270°。现在显式补 `KeyPoint{ms:0, value:0}`
并在 `Keyframes::sample` 加 `debug_assert!(首点 ms == 0)`,同一类坑不再靠自觉。
- `CircularGeometry::indeterminate` 改返回 `(弧长, 圈数)`,圈数 = 3.0×phase + 关键帧(可为 >1),
`progress.rs` 直接乘 360、不做 rem_euclid。
- 断言:`circular_indeterminate_rotation_is_four_turns_and_continuous`(每周期 4 圈、
跨周期跳变 ≤ 一帧 = 4/60 圈,修前是 0.75 圈 = 270°、旋转单调)。
probe 拆成 `probe.rs` + `probe/circular.rs`(新断言会把单文件顶过 400 行)。
This commit is contained in:
1 parent
36f58d7904
commit
e9af27c8f1
6 files changed
+494
-202
No files matched your search
@@ -93,6 +93,71 @@ impl Progress {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 0..1 的区间**排序 + 合并重叠**,摊成「间隙 / 指示条」交替的列表(总长恒为 1.0)。
|
||||
///
|
||||
/// ## 为什么必须排序(复审 P1-A,视觉 r1 的 p1)
|
||||
///
|
||||
/// [`phase_segments`] 返回的两段**不保证有序**:第一线的前缘抢跑 250ms,所以它的区间在右,
|
||||
/// 第二线从 0 起跑的区间在左。照顺序铺 `FillPortion` 会把左段接到右段**之后**,并且
|
||||
/// `FillPortion` 之和一旦 ≠ 1000,iced 就按总和归一 —— 位置和宽度一起被压扁。
|
||||
/// 实测 phase 0.55:本应两段 `[0.003,0.079]` + `[0.355,0.860]`(x 689..713 / 802..963),
|
||||
/// 实际被画成一段 100px 的台阶(x 749..848),且 **34/100 个相位**上总和 >1(最大 1.99)。
|
||||
///
|
||||
/// 返回的每项是 `(是不是指示条, 占比)`;间隙项也显式给出来,才能断言「总和 = 1.0」。
|
||||
pub fn layout_spans(spans: &[(f32, f32)]) -> Vec<(bool, f32)> {
|
||||
let mut clipped: Vec<(f32, f32)> = spans
|
||||
.iter()
|
||||
.map(|(start, end)| (start.clamp(0.0, 1.0), end.clamp(0.0, 1.0)))
|
||||
.filter(|(start, end)| end > start)
|
||||
.collect();
|
||||
clipped.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
|
||||
let mut merged: Vec<(f32, f32)> = Vec::new();
|
||||
for (start, end) in clipped {
|
||||
match merged.last_mut() {
|
||||
Some(last) if start <= last.1 => last.1 = last.1.max(end),
|
||||
_ => merged.push((start, end)),
|
||||
}
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut cursor = 0.0_f32;
|
||||
for (start, end) in merged {
|
||||
if start > cursor {
|
||||
items.push((false, start - cursor));
|
||||
}
|
||||
items.push((true, end - start));
|
||||
cursor = end;
|
||||
}
|
||||
if cursor < 1.0 {
|
||||
items.push((false, 1.0 - cursor));
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
/// 占比 → `FillPortion` 整数份额,**总和恒为 `PORTIONS`**(累计舍入,最后一项吃掉残差)。
|
||||
///
|
||||
/// 为什么不用「每项各自 round」:各项独立取整后总和可能是 999 或 1001,iced 按总和归一,
|
||||
/// 就又回到 P1-A 那个「整条被压扁」的坑。这里让每个**累计边界**取整,最后一项补足,
|
||||
/// 于是任意相位下份额之和都精确等于 1000。
|
||||
pub fn portion_shares(items: &[(bool, f32)]) -> Vec<(bool, u16)> {
|
||||
let mut shares = Vec::with_capacity(items.len());
|
||||
let mut accumulated = 0.0_f32;
|
||||
let mut previous = 0_u16;
|
||||
for (index, (is_bar, fraction)) in items.iter().enumerate() {
|
||||
accumulated += fraction;
|
||||
let target = if index + 1 == items.len() {
|
||||
PORTIONS as u16
|
||||
} else {
|
||||
(accumulated * PORTIONS).round() as u16
|
||||
};
|
||||
let share = target.saturating_sub(previous);
|
||||
previous = target;
|
||||
shares.push((*is_bar, share));
|
||||
}
|
||||
shares
|
||||
}
|
||||
|
||||
/// 线性进度条(轨道 + 指示条,厚 4、圆角 full)。
|
||||
///
|
||||
/// 轨道宽 `Fill`(由宿主决定),指示条按占比分到 `FillPortion` 上——**不需要知道轨道像素宽**,
|
||||
@@ -105,18 +170,13 @@ pub fn linear<'a, M: 'a>(ui: &'a Ui, progress: Progress) -> Element<'a, M> {
|
||||
let mut bar = row![]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(geometry.thickness));
|
||||
let mut cursor = 0.0_f32;
|
||||
for (start, end) in progress.spans() {
|
||||
let (start, end) = (start.clamp(0.0, 1.0), end.clamp(0.0, 1.0));
|
||||
if end <= start {
|
||||
for (is_bar, share) in portion_shares(&layout_spans(&progress.spans())) {
|
||||
if share == 0 {
|
||||
continue;
|
||||
}
|
||||
if start > cursor {
|
||||
bar = bar.push(spacer(start - cursor));
|
||||
}
|
||||
bar = bar.push(
|
||||
bar = bar.push(if is_bar {
|
||||
container(Space::new().width(Length::Fill).height(Length::Fill))
|
||||
.width(Length::FillPortion(portion(end - start)))
|
||||
.width(Length::FillPortion(share))
|
||||
.height(Length::Fill)
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(active_color)),
|
||||
@@ -125,12 +185,11 @@ pub fn linear<'a, M: 'a>(ui: &'a Ui, progress: Progress) -> Element<'a, M> {
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
}),
|
||||
);
|
||||
cursor = end;
|
||||
}
|
||||
if cursor < 1.0 {
|
||||
bar = bar.push(spacer(1.0 - cursor));
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
spacer(share)
|
||||
});
|
||||
}
|
||||
|
||||
container(bar)
|
||||
@@ -153,8 +212,9 @@ pub fn circular<'a, M: 'a>(ui: &'a Ui, progress: Progress) -> Element<'a, M> {
|
||||
let (sweep, rotation) = match progress.amount {
|
||||
Amount::Determinate(value) => (value.clamp(0.0, 1.0) * 360.0, 0.0),
|
||||
Amount::Indeterminate(phase) => {
|
||||
let (sweep_fraction, rotation_fraction) = geometry.indeterminate(phase);
|
||||
(sweep_fraction * 360.0, rotation_fraction * 360.0)
|
||||
// 圈数直接乘 360(不做 rem_euclid):每周期 4 圈是官方口径,截断就丢转速。
|
||||
let (sweep_fraction, turns) = geometry.indeterminate(phase);
|
||||
(sweep_fraction * 360.0, turns * 360.0)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,18 +253,13 @@ pub fn circular<'a, M: 'a>(ui: &'a Ui, progress: Progress) -> Element<'a, M> {
|
||||
}
|
||||
|
||||
/// 两段之间/两端的透明占位。
|
||||
fn spacer<'a, M: 'a>(fraction: f32) -> Element<'a, M> {
|
||||
fn spacer<'a, M: 'a>(share: u16) -> Element<'a, M> {
|
||||
Space::new()
|
||||
.width(Length::FillPortion(portion(fraction)))
|
||||
.width(Length::FillPortion(share))
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 0..1 的占比 → `FillPortion`(≥1,避免 0 段;调用方已过滤空段)。
|
||||
fn portion(fraction: f32) -> u16 {
|
||||
(fraction.clamp(0.0, 1.0) * PORTIONS).round().max(1.0) as u16
|
||||
}
|
||||
|
||||
/// 一层几何图元(固定尺寸 + `currentColor` 上色)。
|
||||
fn glyph<'a, M: 'a>(
|
||||
geometry: &CircularGeometry,
|
||||
|
||||
Reference in new issue
Block a user