fix(text): show text in command previews (move/copy/etc) (#316)
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY, PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/ scaled/stretched/mirrored, but those helpers moved only `points`, never `text_verts`, and the ghosts' glyphs were never uploaded — so text vanished (or froze at its source) during the preview, same root cause as the grip-drag case. Transform text_verts alongside points in all five helpers (shared map_text_verts, reconstructing + re-splitting the double-single world position), and gather preview_wires' glyphs into the per-frame preview-text buffer in the render path. This covers every command preview automatically. The grip-drag else branch no longer sets preview_text explicitly (the render gather handles it); the grip-slide fast path keeps its own scene.preview_text channel (empty preview_wires, so no double-draw). MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off -> readable relocation via translated(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
67fe363775
commit
f5870b011d
4 changed files with 95 additions and 14 deletions
|
|
@ -812,12 +812,11 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||||
self.tabs[i].scene.set_preview_text(slid);
|
self.tabs[i].scene.set_preview_text(slid);
|
||||||
self.tabs[i].scene.set_preview_wires(Vec::new());
|
self.tabs[i].scene.set_preview_wires(Vec::new());
|
||||||
} else {
|
} else {
|
||||||
|
// Wire geometry, or a reshape grip: re-tessellate. The
|
||||||
|
// preview WireModels carry the entity's glyphs (gathered
|
||||||
|
// for the preview-text buffer in the render path), so a
|
||||||
|
// dimension / MTEXT-width drag keeps its text visible too.
|
||||||
let preview = self.tabs[i].scene.wire_models_for(&[grip.handle]);
|
let preview = self.tabs[i].scene.wire_models_for(&[grip.handle]);
|
||||||
let ptext = preview
|
|
||||||
.iter()
|
|
||||||
.flat_map(|w| w.text_verts.iter().copied())
|
|
||||||
.collect();
|
|
||||||
self.tabs[i].scene.set_preview_text(ptext);
|
|
||||||
self.tabs[i].scene.set_preview_wires(preview);
|
self.tabs[i].scene.set_preview_wires(preview);
|
||||||
}
|
}
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,11 @@ impl WireModel {
|
||||||
p[1] += delta.y;
|
p[1] += delta.y;
|
||||||
p[2] += delta.z;
|
p[2] += delta.z;
|
||||||
}
|
}
|
||||||
|
if !out.text_verts.is_empty() {
|
||||||
|
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
|
||||||
|
out.text_verts =
|
||||||
|
map_text_verts(&self.text_verts, |x, y, z| (x + dx, y + dy, z + dz));
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +189,14 @@ impl WireModel {
|
||||||
p[0] = center.x + dx * c - dy * s;
|
p[0] = center.x + dx * c - dy * s;
|
||||||
p[1] = center.y + dx * s + dy * c;
|
p[1] = center.y + dx * s + dy * c;
|
||||||
}
|
}
|
||||||
|
if !out.text_verts.is_empty() {
|
||||||
|
let (cx, cy) = (center.x as f64, center.y as f64);
|
||||||
|
let (s, c) = (s as f64, c as f64);
|
||||||
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||||
|
let (dx, dy) = (x - cx, y - cy);
|
||||||
|
(cx + dx * c - dy * s, cy + dx * s + dy * c, z)
|
||||||
|
});
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +211,13 @@ impl WireModel {
|
||||||
p[1] = center.y + (p[1] - center.y) * factor;
|
p[1] = center.y + (p[1] - center.y) * factor;
|
||||||
p[2] = center.z + (p[2] - center.z) * factor;
|
p[2] = center.z + (p[2] - center.z) * factor;
|
||||||
}
|
}
|
||||||
|
if !out.text_verts.is_empty() {
|
||||||
|
let (cx, cy, cz) = (center.x as f64, center.y as f64, center.z as f64);
|
||||||
|
let f = factor as f64;
|
||||||
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||||
|
(cx + (x - cx) * f, cy + (y - cy) * f, cz + (z - cz) * f)
|
||||||
|
});
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +238,18 @@ impl WireModel {
|
||||||
p[2] += delta.z;
|
p[2] += delta.z;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !out.text_verts.is_empty() {
|
||||||
|
let (mnx, mny) = (win_min.x as f64, win_min.y as f64);
|
||||||
|
let (mxx, mxy) = (win_max.x as f64, win_max.y as f64);
|
||||||
|
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
|
||||||
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||||
|
if x >= mnx && x <= mxx && y >= mny && y <= mxy {
|
||||||
|
(x + dx, y + dy, z + dz)
|
||||||
|
} else {
|
||||||
|
(x, y, z)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +280,18 @@ impl WireModel {
|
||||||
p[0] = 2.0 * t * ax - p[0];
|
p[0] = 2.0 * t * ax - p[0];
|
||||||
p[1] = 2.0 * t * ay - p[1];
|
p[1] = 2.0 * t * ay - p[1];
|
||||||
}
|
}
|
||||||
|
// Glyph quads reflect wholesale (true mirror) — the caller only routes
|
||||||
|
// text through here for MIRRTEXT-on; MIRRTEXT-off relocates via
|
||||||
|
// `translated` so glyphs stay readable.
|
||||||
|
if !out.text_verts.is_empty() {
|
||||||
|
let (ax, ay, len2) = (ax as f64, ay as f64, len2 as f64);
|
||||||
|
let (p1x, p1y) = (p1.x as f64, p1.y as f64);
|
||||||
|
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||||
|
let (dx, dy) = (x - p1x, y - p1y);
|
||||||
|
let t = (dx * ax + dy * ay) / len2;
|
||||||
|
(p1x + 2.0 * t * ax - dx, p1y + 2.0 * t * ay - dy, z)
|
||||||
|
});
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,6 +310,36 @@ impl WireModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map every glyph vertex's double-single world position through `f`, re-
|
||||||
|
/// splitting the result. The preview transforms above move `points`, but SDF
|
||||||
|
/// glyph quads live in `text_verts` (absolute-world double-single) — so a text
|
||||||
|
/// ghost (MOVE / COPY / ROTATE / SCALE / STRETCH / MIRROR preview) must carry
|
||||||
|
/// these along or the dragged text renders frozen at its source (issue #316).
|
||||||
|
fn map_text_verts(
|
||||||
|
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
||||||
|
f: impl Fn(f64, f64, f64) -> (f64, f64, f64),
|
||||||
|
) -> Vec<crate::scene::pipeline::text_gpu::TextVertex> {
|
||||||
|
use crate::scene::pipeline::text_gpu::split_ds;
|
||||||
|
verts
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
let (nx, ny, nz) = f(
|
||||||
|
v.pos[0] as f64 + v.pos_low[0] as f64,
|
||||||
|
v.pos[1] as f64 + v.pos_low[1] as f64,
|
||||||
|
v.pos[2] as f64 + v.pos_low[2] as f64,
|
||||||
|
);
|
||||||
|
let (xh, xl) = split_ds(nx);
|
||||||
|
let (yh, yl) = split_ds(ny);
|
||||||
|
let (zh, zl) = split_ds(nz);
|
||||||
|
crate::scene::pipeline::text_gpu::TextVertex {
|
||||||
|
pos: [xh, yh, zh],
|
||||||
|
pos_low: [xl, yl, zl],
|
||||||
|
..*v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for WireModel {
|
impl Default for WireModel {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ impl TextVertex {
|
||||||
|
|
||||||
/// Split an f64 into the double-single (high f32, low residual f32) pair the
|
/// Split an f64 into the double-single (high f32, low residual f32) pair the
|
||||||
/// shaders reconstruct relative-to-eye.
|
/// shaders reconstruct relative-to-eye.
|
||||||
fn split_ds(v: f64) -> (f32, f32) {
|
pub(crate) fn split_ds(v: f64) -> (f32, f32) {
|
||||||
let high = v as f32;
|
let high = v as f32;
|
||||||
(high, (v - high as f64) as f32)
|
(high, (v - high as f64) as f32)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1126,14 +1126,22 @@ impl Scene {
|
||||||
// draws only the text that belongs to it. Cached on the wire content
|
// draws only the text that belongs to it. Cached on the wire content
|
||||||
// id so an unchanged wire set is not re-walked every frame.
|
// id so an unchanged wire set is not re-walked every frame.
|
||||||
let text_verts = self.gather_text_verts(&all_wires, wire_content_id);
|
let text_verts = self.gather_text_verts(&all_wires, wire_content_id);
|
||||||
// Grip-drag / command-preview glyphs live on `self.preview_text` (set by
|
// Grip-drag / command-preview glyphs, excluded from the epoch-cached base
|
||||||
// the edit handlers), excluded from the epoch-cached base gather above.
|
// gather above. Two sources, both tiny (one operation's worth) and walked
|
||||||
// Clone the (tiny — one dragged entity) set per frame so dragged text
|
// per frame: the overlay wires' own glyphs (MOVE / COPY / ROTATE / SCALE /
|
||||||
// stays on screen without re-uploading the resident base buffer (#316).
|
// STRETCH / MIRROR ghosts carry text_verts) and `self.preview_text` (the
|
||||||
let preview_text_verts = if self.preview_text.is_empty() {
|
// grip-slide fast path, which emits bare glyphs with empty preview_wires).
|
||||||
Arc::new(Vec::new())
|
// The two never overlap — a slide leaves preview_wires empty — so a plain
|
||||||
} else {
|
// concat is correct, no double-draw (issue #316).
|
||||||
Arc::new(self.preview_text.clone())
|
let preview_text_verts = {
|
||||||
|
let mut pv: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||||
|
for w in preview_wires.iter() {
|
||||||
|
if !w.text_verts.is_empty() {
|
||||||
|
pv.extend_from_slice(&w.text_verts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pv.extend_from_slice(&self.preview_text);
|
||||||
|
Arc::new(pv)
|
||||||
};
|
};
|
||||||
Some(ViewportData {
|
Some(ViewportData {
|
||||||
wires: all_wires,
|
wires: all_wires,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue