mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
Merge PR #393
This commit is contained in:
@@ -52,7 +52,7 @@ pub fn paint_mark(ui: &egui::Ui, rect: Rect, color: Color32) {
|
||||
let mut res = vello_cpu::Resources::new();
|
||||
rc.render(&mut pm, &mut res);
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([px as usize, px as usize], pm.data_as_u8_slice());
|
||||
let t = ctx.load_texture("artcraft_mark", ci, egui::TextureOptions::LINEAR);
|
||||
let t = crate::widgets::load_texture(ctx, "artcraft_mark", ci, egui::TextureOptions::LINEAR);
|
||||
ctx.data_mut(|d| d.insert_temp(key, t.clone()));
|
||||
t
|
||||
});
|
||||
|
||||
@@ -587,6 +587,11 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
|
||||
let Some(regions) = designcraft_render::damage::damage_with(&old, &new, Some(&app.session.cache)) else { return false };
|
||||
let t0 = crate::now_ms();
|
||||
let (tw, th) = ((sh.size.0 as f64 * ppp).round() as i64, (sh.size.1 as f64 * ppp).round() as i64);
|
||||
// A texture `upload` sampled down to the GPU limit has other pixels than the view: a region
|
||||
// computed in view pixels would be written outside it. Render it whole instead.
|
||||
if app.canvas.texture.as_ref().is_none_or(|t| t.size() != [tw.max(0) as usize, th.max(0) as usize]) {
|
||||
return false;
|
||||
}
|
||||
let k = sh.zoom * ppp;
|
||||
let mut px: Option<(i64, i64, i64, i64)> = None;
|
||||
for (r, rect) in ®ions {
|
||||
@@ -667,6 +672,7 @@ fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render:
|
||||
designcraft_render::separation_view(&mut img, None, app.ui.ink_limit);
|
||||
}
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let ci = crate::widgets::fit_texture(ci, ctx.input(|i| i.max_texture_side));
|
||||
match &mut app.canvas.texture {
|
||||
Some(tex) => tex.set(ci, egui::TextureOptions::LINEAR),
|
||||
None => app.canvas.texture = Some(ctx.load_texture("canvas", ci, egui::TextureOptions::LINEAR)),
|
||||
@@ -1336,7 +1342,7 @@ fn draw_inverse_highlight(
|
||||
let mut res = vello_cpu::Resources::new();
|
||||
rc.render(&mut pm, &mut res);
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([w as usize, h as usize], pm.data_as_u8_slice());
|
||||
let t = ctx.load_texture("inverse_selection", ci, egui::TextureOptions::NEAREST);
|
||||
let t = crate::widgets::load_texture(ctx, "inverse_selection", ci, egui::TextureOptions::NEAREST);
|
||||
ctx.data_mut(|d| d.insert_temp(cache_id, (key, t.clone())));
|
||||
t
|
||||
}
|
||||
@@ -2238,6 +2244,38 @@ mod tests {
|
||||
(s.horizontal_units, s.vertical_units)
|
||||
}
|
||||
|
||||
/// A viewport wider than the GPU's largest texture: the canvas texture is sampled down, and an
|
||||
/// edit repaints it whole. A region repainted in view pixels would land in the wrong place,
|
||||
/// or outside the texture (a wgpu validation error). A view that fits is still patched.
|
||||
#[test]
|
||||
fn an_edit_repaints_a_canvas_texture_sampled_down_to_the_gpu_limit_whole() {
|
||||
for (width, sampled_down) in [(1440.0, false), (2600.0, true)] {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let frame = app.run("frame.create", json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Text"})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(width, 900.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(6);
|
||||
let limit = h.ctx.input(|i| i.max_texture_side);
|
||||
let size = h.state().canvas.texture.as_ref().map(|t| t.size()).unwrap();
|
||||
assert!(size[0] <= limit && size[1] <= limit, "{size:?}");
|
||||
let shown = h.state().canvas.shown.unwrap().size;
|
||||
assert_eq!(shown.0 > limit as f32, sampled_down, "view {shown:?}, limit {limit}");
|
||||
let patches = h.state().canvas.patches;
|
||||
// A small edit: one more character in the frame.
|
||||
h.state_mut().run("text.insert", json!({"story": frame["story"], "at": 4, "text": "s"})).unwrap();
|
||||
h.run_steps(6);
|
||||
assert_eq!(h.state().canvas.patches == patches, sampled_down, "width {width}");
|
||||
assert_eq!(h.state().canvas.texture.as_ref().map(|t| t.size()), Some(size));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_merged_owners_paint_the_same_row_and_column_targets() {
|
||||
for column in [true, false] {
|
||||
|
||||
@@ -76,7 +76,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let bar = scroll.bar_inner_margin + scroll.bar_width + scroll.bar_outer_margin;
|
||||
let cols = (((ui.available_width() - bar) / CELL).floor() as u32).max(1);
|
||||
let ppp = ui.ctx().pixels_per_point();
|
||||
let cell_px = (CELL * ppp).round() as u32;
|
||||
let cell_px = (CELL * ppp).round().max(1.0) as u32;
|
||||
// The grid is one texture: no more rows than the GPU's largest texture holds (a narrow
|
||||
// panel of a large CJK font ran past it).
|
||||
let max_rows = (ui.ctx().input(|i| i.max_texture_side) as u32 / cell_px).max(1);
|
||||
chars.truncate((max_rows * cols) as usize);
|
||||
let ink = t.text_strong;
|
||||
let key = egui::Id::new(("glyph_grid", &st.family, &st.style, scope, &q, cols, cell_px, ink.to_array()));
|
||||
let tex: egui::TextureHandle = match ui.data(|d| d.get_temp::<egui::TextureHandle>(key)) {
|
||||
@@ -84,7 +88,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
None => {
|
||||
let img = designcraft_render::glyphs::glyph_grid(&db, &st.family, &st.style, &chars, cols, cell_px, ink.to_array());
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let h = ui.ctx().load_texture("glyph_grid", ci, egui::TextureOptions::LINEAR);
|
||||
let h = crate::widgets::load_texture(ui.ctx(), "glyph_grid", ci, egui::TextureOptions::LINEAR);
|
||||
ui.data_mut(|d| d.insert_temp(key, h.clone()));
|
||||
h
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ pub fn font_menu_body(app: &mut DesignApp, ui: &mut egui::Ui, menu: &[designcraf
|
||||
let tex = tex.unwrap_or_else(|| {
|
||||
let img = designcraft_render::glyphs::text_line(&fonts, f, "Regular", "Sample", (18.0 * ppp) as u32, t.text.to_array());
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let h = ui.ctx().load_texture(format!("font_preview_{f}"), ci, egui::TextureOptions::LINEAR);
|
||||
let h = crate::widgets::load_texture(ui.ctx(), format!("font_preview_{f}"), ci, egui::TextureOptions::LINEAR);
|
||||
ui.data_mut(|d| d.insert_temp(key, h.clone()));
|
||||
h
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ fn thumb(app: &mut DesignApp, ctx: &egui::Context, abs: usize, h: f32) -> Option
|
||||
&designcraft_render::RenderOptions { printing_only: true, ..Default::default() },
|
||||
)?;
|
||||
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
let tex = ctx.load_texture(format!("thumb{abs}"), ci, egui::TextureOptions::LINEAR);
|
||||
let tex = crate::widgets::load_texture(ctx, format!("thumb{abs}"), ci, egui::TextureOptions::LINEAR);
|
||||
// Drop thumbnails of older revisions.
|
||||
let (uid, rev) = (st.uid, st.revision);
|
||||
map.retain(|k, _| !(k.0 == uid && k.1 != rev));
|
||||
|
||||
@@ -717,3 +717,49 @@ mod tests {
|
||||
assert_eq!(m.step(), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// `img` made to fit the GPU's largest texture (`max_side` pixels a side), sampled down
|
||||
/// (nearest pixel) when it is larger. A texture past the limit panics in the renderer, so every
|
||||
/// UI texture goes through this: an oversized image loses resolution, not the document.
|
||||
pub fn fit_texture(img: egui::ColorImage, max_side: usize) -> egui::ColorImage {
|
||||
let [w, h] = img.size;
|
||||
let max_side = max_side.max(1);
|
||||
if w <= max_side && h <= max_side {
|
||||
return img;
|
||||
}
|
||||
let k = (max_side as f64 / w.max(h) as f64).min(1.0);
|
||||
let (nw, nh) = (((w as f64 * k).floor() as usize).clamp(1, max_side), ((h as f64 * k).floor() as usize).clamp(1, max_side));
|
||||
let mut pixels = Vec::with_capacity(nw * nh);
|
||||
for y in 0..nh {
|
||||
let sy = (y * h / nh).min(h.saturating_sub(1));
|
||||
for x in 0..nw {
|
||||
let sx = (x * w / nw).min(w.saturating_sub(1));
|
||||
pixels.push(img.pixels.get(sy * w + sx).copied().unwrap_or(Color32::TRANSPARENT));
|
||||
}
|
||||
}
|
||||
egui::ColorImage::new([nw, nh], pixels)
|
||||
}
|
||||
|
||||
/// [`egui::Context::load_texture`] for an image of any size (see [`fit_texture`]).
|
||||
pub fn load_texture(ctx: &egui::Context, name: impl Into<String>, img: egui::ColorImage, options: egui::TextureOptions) -> egui::TextureHandle {
|
||||
let max = ctx.input(|i| i.max_texture_side);
|
||||
ctx.load_texture(name, fit_texture(img, max), options)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod texture_tests {
|
||||
/// The glyph grid that crashed the renderer: 7 columns of 1200 characters at 60 px, 10320 px
|
||||
/// tall against an 8192 px limit.
|
||||
#[test]
|
||||
fn oversized_images_fit_the_texture_limit() {
|
||||
let img = egui::ColorImage::new([420, 10320], vec![egui::Color32::WHITE; 420 * 10320]);
|
||||
let out = super::fit_texture(img, 8192);
|
||||
assert!(out.size[0] <= 8192 && out.size[1] == 8192, "{:?}", out.size);
|
||||
assert_eq!(out.pixels.len(), out.size[0] * out.size[1]);
|
||||
let small = egui::ColorImage::new([10, 10], vec![egui::Color32::RED; 100]);
|
||||
assert_eq!(super::fit_texture(small, 8192).size, [10, 10]);
|
||||
let wide = egui::ColorImage::new([20000, 3], vec![egui::Color32::RED; 60000]);
|
||||
let out = super::fit_texture(wide, 8192);
|
||||
assert!(out.size[0] == 8192 && out.size[1] >= 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user