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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-10 10:49:13 +03:00
1 parent 67fe363775
commit f5870b011d
4 files changed
+95 -14

No files matched your search

+4 -5
View File
@@ -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_wires(Vec::new());
} 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 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.refresh_selected_grips();
+74
View File
@@ -168,6 +168,11 @@ impl WireModel {
p[1] += delta.y;
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
}
@@ -184,6 +189,14 @@ impl WireModel {
p[0] = center.x + dx * c - dy * s;
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
}
@@ -198,6 +211,13 @@ impl WireModel {
p[1] = center.y + (p[1] - center.y) * 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
}
@@ -218,6 +238,18 @@ impl WireModel {
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
}
@@ -248,6 +280,18 @@ impl WireModel {
p[0] = 2.0 * t * ax - p[0];
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
}
@@ -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 {
fn default() -> Self {
Self {
+1 -1
View File
@@ -70,7 +70,7 @@ impl TextVertex {
/// Split an f64 into the double-single (high f32, low residual f32) pair the
/// 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;
(high, (v - high as f64) as f32)
}
+16 -8
View File
@@ -1126,14 +1126,22 @@ impl Scene {
// 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.
let text_verts = self.gather_text_verts(&all_wires, wire_content_id);
// Grip-drag / command-preview glyphs live on `self.preview_text` (set by
// the edit handlers), excluded from the epoch-cached base gather above.
// Clone the (tiny — one dragged entity) set per frame so dragged text
// stays on screen without re-uploading the resident base buffer (#316).
let preview_text_verts = if self.preview_text.is_empty() {
Arc::new(Vec::new())
} else {
Arc::new(self.preview_text.clone())
// Grip-drag / command-preview glyphs, excluded from the epoch-cached base
// gather above. Two sources, both tiny (one operation's worth) and walked
// per frame: the overlay wires' own glyphs (MOVE / COPY / ROTATE / SCALE /
// STRETCH / MIRROR ghosts carry text_verts) and `self.preview_text` (the
// grip-slide fast path, which emits bare glyphs with empty preview_wires).
// The two never overlap — a slide leaves preview_wires empty — so a plain
// concat is correct, no double-draw (issue #316).
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 {
wires: all_wires,