Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
021a9474f8 | ||
|
|
de564bc24f | ||
|
|
495694e1ac | ||
|
|
584f08ae70 | ||
|
|
db8be89052 | ||
|
|
1ba987ee7a |
No files matched your search
@@ -31,7 +31,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use eframe::egui_wgpu::NativeAdapterSelectorMethod;
|
||||
use eframe::wgpu::{self, Backend, DeviceType, PowerPreference};
|
||||
use eframe::wgpu::{self, Backend, Backends, DeviceType, PowerPreference};
|
||||
|
||||
/// The adapters a restart leaves out, those the window failed on, as comma-separated [`key`]s
|
||||
/// (`Vulkan:1002:164e`, `Metal:Intel Iris Pro Graphics`). Set by the app itself when it starts
|
||||
@@ -81,6 +81,15 @@ pub fn preferred_displays(pref: Option<&str>, env: Option<PowerPreference>) -> V
|
||||
if automatic(pref, env) { display_gpus() } else { Vec::new() }
|
||||
}
|
||||
|
||||
/// The graphics backends the window starts with: on Windows Direct3D 12, falling back to OpenGL,
|
||||
/// unless `WGPU_BACKEND` names others (`env_set`; `default` then holds them). Creating a Vulkan
|
||||
/// instance loads every installed Vulkan driver into the process, and a faulty one (Intel's
|
||||
/// `igvk64.dll`, #806) crashed the app before its window appeared. DX12 is Windows' own backend,
|
||||
/// as in PdfCraft. Elsewhere, `default`.
|
||||
pub fn backends(default: Backends, env_set: bool) -> Backends {
|
||||
if cfg!(windows) && !env_set { Backends::DX12 | Backends::GL } else { default }
|
||||
}
|
||||
|
||||
/// One adapter, as [`adapter_order`] sees it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Candidate {
|
||||
@@ -809,6 +818,18 @@ mod tests {
|
||||
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_never_loads_vulkan_drivers_unless_asked() {
|
||||
let all = Backends::all();
|
||||
let start = backends(all, false);
|
||||
if cfg!(windows) {
|
||||
assert_eq!(start, Backends::DX12 | Backends::GL, "#806");
|
||||
} else {
|
||||
assert_eq!(start, all);
|
||||
}
|
||||
assert_eq!(backends(Backends::VULKAN, true), Backends::VULKAN, "WGPU_BACKEND=vulkan still picks it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_preference_follows_the_environment_then_the_preference() {
|
||||
assert_eq!(power_preference(None, None), AUTOMATIC);
|
||||
|
||||
@@ -467,6 +467,7 @@ fn main() -> std::process::ExitCode {
|
||||
// Metal on an Iris Pro, #651). `WGPU_VALIDATION_INDIRECT_CALL=1` turns it back on.
|
||||
create.instance_descriptor.flags =
|
||||
(eframe::wgpu::InstanceFlags::from_build_config() - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL).with_env();
|
||||
create.instance_descriptor.backends = gpu::backends(create.instance_descriptor.backends, std::env::var_os("WGPU_BACKEND").is_some());
|
||||
}
|
||||
options
|
||||
};
|
||||
|
||||
@@ -936,6 +936,14 @@ fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document) -> u
|
||||
return 0;
|
||||
}
|
||||
let mut with = view.difference(doc, rect).any;
|
||||
// Taking a text object out changes only what is drawn in its box. When the art would still
|
||||
// differ from the page by more than `compare` allows with every candidate gone, the layers
|
||||
// aren't used whatever is pruned: don't draw them again for each one (#758).
|
||||
let margin = OUTLINE_REACH + 1.0;
|
||||
let boxes: f64 = found.iter().filter_map(|(_, b)| *b).map(|b| b.inflate(margin, margin).intersect(rect).area().max(0.0)).sum();
|
||||
if with - boxes / rect.area().max(1e-9) > MAX_DIFFERENCE {
|
||||
return 0;
|
||||
}
|
||||
let mut removed = 0;
|
||||
for (id, bounds) in found {
|
||||
if bounds.is_none_or(|b| b.intersect(rect).is_zero_area()) {
|
||||
|
||||
+5
-41
@@ -187,10 +187,6 @@ fn shadow_filter(dx: f64, dy: f64, blur: f64, color: peniko::Color) -> Filter {
|
||||
})
|
||||
}
|
||||
|
||||
fn blur_filter(sigma: f64) -> Filter {
|
||||
Filter::from_primitive(FilterPrimitive::GaussianBlur { std_deviation: sigma.max(0.0) as f32, edge_mode: EdgeMode::None })
|
||||
}
|
||||
|
||||
fn pcolor(ink: Ink, c: &vectorcraft_doc::color::Color) -> peniko::Color {
|
||||
let [r, g, b] = ink.rgb(c);
|
||||
peniko::Color::new([r, g, b, 1.0])
|
||||
@@ -495,44 +491,12 @@ impl Renderer {
|
||||
c.pop_layer();
|
||||
});
|
||||
}
|
||||
// The content itself (blurred / feathered / filtered).
|
||||
let blur: f64 = rfx
|
||||
.iter()
|
||||
.map(|fx| match fx {
|
||||
RasterFx::Feather { radius } | RasterFx::GaussianBlur { radius } => radius.max(0.0),
|
||||
_ => 0.0,
|
||||
})
|
||||
.sum();
|
||||
if rfx.iter().any(|fx| matches!(fx, RasterFx::Pixel(_))) {
|
||||
// The content itself (blurred / feathered / filtered), on its pixels: a filter layer's blur
|
||||
// works on a coarser grid tied to the canvas, so the blurred art shifted by up to a pixel
|
||||
// and looked blocky as the view moved by parts of a pixel (#755).
|
||||
let blurs = rfx.iter().any(|fx| matches!(fx, RasterFx::Feather { radius } | RasterFx::GaussianBlur { radius } if *radius > 0.0));
|
||||
if blurs || rfx.iter().any(|fx| matches!(fx, RasterFx::Pixel(_))) {
|
||||
self.pixel_content(ctx, f, content, rfx, paint);
|
||||
} else if blur > 0.0 {
|
||||
self.with_filters(ctx, f, reach, blur, None, |r, c, fr, _| {
|
||||
let mut layers = 0;
|
||||
for fx in rfx {
|
||||
match fx {
|
||||
RasterFx::Feather { radius } if *radius > 0.0 => {
|
||||
c.set_transform(fr.view);
|
||||
if let Some((g, rule)) = content.outline {
|
||||
c.set_fill_rule(fill_rule(rule));
|
||||
c.push_clip_layer(g);
|
||||
layers += 1;
|
||||
}
|
||||
c.push_layer(None, None, None, None, Some(blur_filter(radius / 2.0)));
|
||||
layers += 1;
|
||||
}
|
||||
RasterFx::GaussianBlur { radius } if *radius > 0.0 => {
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, None, None, None, Some(blur_filter(radius / 2.0)));
|
||||
layers += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
paint(r, c, fr);
|
||||
for _ in 0..layers {
|
||||
c.pop_layer();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
paint(self, ctx, f);
|
||||
}
|
||||
|
||||
@@ -1760,6 +1760,8 @@ mod tests_fontchange;
|
||||
#[cfg(test)]
|
||||
mod tests_freeform;
|
||||
#[cfg(test)]
|
||||
mod tests_fxzoom;
|
||||
#[cfg(test)]
|
||||
mod tests_isolation;
|
||||
#[cfg(test)]
|
||||
mod tests_knockout;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
//! Raster effects stay where they are, and look the same, at every zoom and pan (#755).
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::text::{CharStyle, TextObject};
|
||||
use vectorcraft_doc::{Appearance, Effect, Node, NodeKind};
|
||||
use vectorcraft_geom::{Point, shapes};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Type blurred by `radius` points (Gaussian Blur, or Feather when `feather`) over a rectangle
|
||||
/// that overlays at half opacity; without the type when `radius` is `None`.
|
||||
fn scene(radius: Option<f64>, feather: bool) -> Document {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let mut back = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 100.0)),
|
||||
Appearance::basic(Paint::solid(Color::rgb(0.2, 0.4, 0.9)), Paint::None, 0.0),
|
||||
);
|
||||
back.blend = BlendMode::Overlay;
|
||||
back.opacity = 0.5;
|
||||
d.insert(Some(l), 0, back).unwrap();
|
||||
if let Some(radius) = radius {
|
||||
let style = CharStyle { size: 36.0, ..CharStyle::default() };
|
||||
let mut t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(30.0, 60.0), "Hello", style))));
|
||||
let id = if feather { "stylize.feather" } else { "blur.gaussian" };
|
||||
t.appearance.effects.push(Effect { id: id.into(), params: json!({"radius": radius}), visible: true });
|
||||
d.insert(Some(l), 1, t).unwrap();
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// `d` with its layer's art in one group: the overlay and the type grouped, as in #755's report.
|
||||
fn grouped(mut d: Document) -> Document {
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let members = d.children_mut(Some(l)).map(std::mem::take).unwrap();
|
||||
d.insert(Some(l), 0, Node::new(id, NodeKind::Group { children: members, clip: false })).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// The canvas's options: art on nothing, no artboards.
|
||||
fn screen() -> RenderOptions {
|
||||
RenderOptions { background: None, artboards: false, ..Default::default() }
|
||||
}
|
||||
|
||||
fn render(d: &Document, threads: u16, view: Affine, (w, h): (u32, u32)) -> Rendered {
|
||||
let mut r = Renderer::new();
|
||||
r.threads = threads;
|
||||
r.render(d, w, h, view, &screen())
|
||||
}
|
||||
|
||||
/// Where the type's ink is on the document (its centroid), rendered `threads`-threaded through
|
||||
/// `view` on a `w` × `h` canvas.
|
||||
fn ink_centre(radius: f64, threads: u16, view: Affine, size: (u32, u32)) -> Point {
|
||||
let with = render(&scene(Some(radius), false), threads, view, size);
|
||||
let without = render(&scene(None, false), threads, view, size);
|
||||
let (mut sx, mut sy, mut sw) = (0.0, 0.0, 0.0);
|
||||
for (i, (p, q)) in with.pixels.chunks(4).zip(without.pixels.chunks(4)).enumerate() {
|
||||
let d: f64 = (0..4).map(|c| f64::from(p[c].abs_diff(q[c]))).sum();
|
||||
let (x, y) = ((i % size.0 as usize) as f64 + 0.5, (i / size.0 as usize) as f64 + 0.5);
|
||||
sx += d * x;
|
||||
sy += d * y;
|
||||
sw += d;
|
||||
}
|
||||
assert!(sw > 0.0, "the type shows");
|
||||
view.inverse() * Point::new(sx / sw, sy / sw)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blurred_type_stays_in_place_at_every_zoom_and_pan() {
|
||||
for (radius, threads) in [(3.0, 0), (3.0, 4), (12.0, 0), (12.0, 4)] {
|
||||
let base = ink_centre(radius, threads, Affine::IDENTITY, (200, 100));
|
||||
// The whole document in view, at other zooms and moved by parts of a pixel.
|
||||
for (zoom, pan) in
|
||||
[(1.0, (13.3, 7.6)), (2.0, (41.7, 20.2)), (3.3, (15.25, 9.5)), (0.5, (10.4, 5.1)), (6.0, (30.6, 25.3)), (1.37, (0.5, 0.25))]
|
||||
{
|
||||
let view = Affine::translate(pan) * Affine::scale(zoom);
|
||||
let size = ((200.0 * zoom + 2.0 * pan.0).ceil() as u32, (100.0 * zoom + 2.0 * pan.1).ceil() as u32);
|
||||
let at = ink_centre(radius, threads, view, size);
|
||||
assert!(
|
||||
(at.x - base.x).abs() < 1.0 && (at.y - base.y).abs() < 1.0,
|
||||
"radius {radius}, threads {threads}, zoom {zoom}, pan {pan:?}: {at:?} vs {base:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrolling moves the blurred type with the page and nothing else: two views a whole number of
|
||||
/// pixels apart draw the same pixels where they overlap, whether the type fits on the canvas or
|
||||
/// runs off it.
|
||||
#[test]
|
||||
fn scrolling_moves_blurred_type_without_changing_it() {
|
||||
for (feather, group) in [(false, false), (true, false), (false, true)] {
|
||||
let d = scene(Some(12.0), feather);
|
||||
let d = if group { grouped(d) } else { d };
|
||||
for threads in [0, 4] {
|
||||
for (zoom, (w, h)) in [(1.0, (240, 140)), (4.0, (500, 300)), (8.0, (400, 300))] {
|
||||
let at = |dx: f64, dy: f64| render(&d, threads, Affine::translate((-dx, -dy)) * Affine::scale(zoom), (w, h));
|
||||
let a = at(20.0, 10.0);
|
||||
for (sx, sy) in [(1u32, 0u32), (3, 2), (37, 23), (101, 57)] {
|
||||
let b = at(20.0 + f64::from(sx), 10.0 + f64::from(sy));
|
||||
let mut worst = 0;
|
||||
for y in sy..h {
|
||||
for x in sx..w {
|
||||
let i = ((y * w + x) * 4) as usize;
|
||||
let j = (((y - sy) * w + x - sx) * 4) as usize;
|
||||
for c in 0..4 {
|
||||
worst = worst.max(a.pixels[i + c].abs_diff(b.pixels[j + c]));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
worst <= 3,
|
||||
"feather {feather}, grouped {group}, threads {threads}, zoom {zoom}, scrolled by ({sx}, {sy}): a pixel changed by {worst}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrolling by parts of a pixel (a trackpad, a zoom about the pointer) moves heavily blurred type
|
||||
/// by just that: its ink stays put on the page within a tenth of a pixel, at any zoom.
|
||||
#[test]
|
||||
fn blurred_type_doesnt_wobble_as_the_view_moves_by_parts_of_a_pixel() {
|
||||
for threads in [0, 4] {
|
||||
for zoom in [1.0, 4.0] {
|
||||
let at = |p: f64| {
|
||||
ink_centre(30.0, threads, Affine::translate((400.0 - 75.0 * zoom + p, 330.0 - 48.0 * zoom + p)) * Affine::scale(zoom), (800, 660))
|
||||
};
|
||||
let base = at(0.0);
|
||||
for k in 1..10 {
|
||||
let c = at(f64::from(k) * 0.1);
|
||||
// How far it moved on the screen, besides the view's own move.
|
||||
let (dx, dy) = ((c.x - base.x) * zoom, (c.y - base.y) * zoom);
|
||||
assert!(dx.abs() < 0.1 && dy.abs() < 0.1, "threads {threads}, zoom {zoom}, moved by {}: off by ({dx}, {dy}) px", f64::from(k) * 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +346,10 @@ impl Tool for ArtboardTool {
|
||||
for h in Handle::ALL {
|
||||
o.push(Overlay::Anchor { p: h.pos(r), color: BLUE, filled: false, size: 7.0 });
|
||||
}
|
||||
o.push(Overlay::Label { p: Point::new(r.x0, r.y0 - cx.tol(14.0)), text: format!("{:02} - {}", self.active + 1, ab.name), color: BLUE });
|
||||
// Where the canvas names the other artboards: on the top edge, flush with the left one
|
||||
// (labels are drawn 8 px right of and 14 px above their point).
|
||||
let at = Point::new(r.x0 - cx.tol(8.0), r.y0 - cx.tol(4.0));
|
||||
o.push(Overlay::Label { p: at, text: format!("{:02} - {}", self.active + 1, ab.name), color: BLUE });
|
||||
if let (Some(Drag::Resize { began: true, .. }), true) = (self.drag, cx.measurement_labels) {
|
||||
o.push(Overlay::Measure {
|
||||
p: Point::new(r.x1, r.y1) + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
|
||||
|
||||
@@ -349,8 +349,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if app.ui.view.pixel_preview && v.zoom >= PIXEL_GRID_ZOOM && app.session.prefs.show_pixel_grid {
|
||||
grid(&painter, &xf, 1.0, 1, LineLook { color: PIXEL_GRID, dots: false });
|
||||
}
|
||||
// Artboard edges and names.
|
||||
// Artboard edges and names. The Artboard tool labels its active artboard itself (following
|
||||
// a move or resize): that one isn't named twice.
|
||||
let active_ab = 0;
|
||||
let artboard_tool = app.session.tool_id() == "artboard";
|
||||
let tool_labelled = artboard_tool.then(|| app.session.tool_options()["active"].as_u64()).flatten();
|
||||
for (i, ab) in doc.artboards.iter().enumerate() {
|
||||
let r = xf.rect_to_screen(ab.rect);
|
||||
let c = if i == active_ab { Color32::from_gray(0) } else { Color32::from_gray(120) };
|
||||
@@ -359,7 +362,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if doc.setup.has_bleed() {
|
||||
painter.add(Shape::closed_line(xf.quad(doc.setup.bleed_rect(ab.rect)), Stroke::new(1.0, t.bleed)));
|
||||
}
|
||||
if app.session.tool_id() == "artboard" || doc.artboards.len() > 1 {
|
||||
if (artboard_tool || doc.artboards.len() > 1) && tool_labelled != u64::try_from(i).ok() {
|
||||
painter.text(
|
||||
r.left_top() - vec2(0.0, 4.0),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
|
||||
@@ -335,10 +335,9 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
// No save dialog for an agent: the bytes come back, as in headless mode.
|
||||
None => wrap(app.run("document.export", p.clone())),
|
||||
},
|
||||
"app.quit" => {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
ok(Value::Null)
|
||||
}
|
||||
// As the menu's Quit: `{"pending": "saveChanges"}` while a modified document is asked
|
||||
// about; otherwise null, and the host closes the window (#830).
|
||||
"app.quit" => wrap(app.run("app.quit", json!({}))),
|
||||
other => err(format!("unknown method `{other}`")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
||||
("blur.radial", "method") => Some(&[("Spin", "spin"), ("Zoom", "zoom")]),
|
||||
("blur.radial", "quality") => Some(&[("Draft", "draft"), ("Good", "good"), ("Best", "best")]),
|
||||
("blur.smart", "quality") => Some(&[("Low", "low"), ("Medium", "medium"), ("High", "high")]),
|
||||
("path.offsetPath", "joins") => Some(&[("Miter", "miter"), ("Round", "round"), ("Bevel", "bevel")]),
|
||||
("distort.roughen" | "distort.zigZag", "points") => Some(&[("Smooth", "smooth"), ("Corner", "corner")]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -371,3 +373,19 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
app.ui.dialog = None;
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fields_that_pick_a_value_are_dropdowns_of_its_values() {
|
||||
// The values come from the effect's parameter documentation (`joins: "miter"|"round"|…`).
|
||||
for (effect, key) in [("path.offsetPath", "joins"), ("distort.zigZag", "points"), ("distort.roughen", "points")] {
|
||||
let doc = vectorcraft_effects::effect_info(effect).unwrap().params;
|
||||
let listed = doc.split(&format!("{key}: ")).nth(1).unwrap().split([',', '}', ' ']).next().unwrap().to_string();
|
||||
let values: Vec<String> = choices(effect, key).unwrap().iter().map(|(_, v)| format!("\"{v}\"")).collect();
|
||||
assert_eq!(values.join("|"), listed, "{effect} {key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,9 @@ pub(super) fn param_fields(
|
||||
d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
keys.sort_by_key(|(k, _)| rank(k));
|
||||
for (k, v) in keys {
|
||||
ui.label(egui::RichText::new(humanized(&k)).color(t.text));
|
||||
// A size's Relative / Absolute pair (Roughen, Zig Zag, Tweak) is its own label.
|
||||
let relative = k == "relative" && v.is_boolean();
|
||||
ui.label(egui::RichText::new(if relative { String::new() } else { humanized(&k) }).color(t.text));
|
||||
if let Some(cur) = crate::widgets::blend_param(&k, &v) {
|
||||
if let Some(m) = crate::widgets::blend_param_dropdown(ui, ("fx-blend", &k), cur) {
|
||||
d.fields.insert(k, m);
|
||||
@@ -247,9 +249,19 @@ pub(super) fn param_fields(
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
Value::Bool(mut b) => {
|
||||
if ui.checkbox(&mut b, "").changed() {
|
||||
d.fields.insert(k, json!(b));
|
||||
Value::Bool(b) if relative => {
|
||||
ui.horizontal(|ui| {
|
||||
for (label, on) in [(tl!("Relative"), true), (tl!("Absolute"), false)] {
|
||||
if crate::widgets::radio(ui, label, b == on, true) && b != on {
|
||||
d.fields.insert(k.clone(), json!(on));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Value::Bool(b) => {
|
||||
if crate::widgets::check(ui, "", b, true) {
|
||||
d.fields.insert(k, json!(!b));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2564,6 +2564,9 @@
|
||||
Smart Guides (⌘U) Guías inteligentes (⌘U)
|
||||
Smart Punctuation… Puntuación inteligente…
|
||||
Smooth Suavizar
|
||||
Corner Vértice
|
||||
Relative Relativo
|
||||
Absolute Absoluto
|
||||
Smooth Color Color suavizado
|
||||
Smooth Tool Herramienta Suavizar
|
||||
Snap Curves To Lines Ajustar curvas a líneas
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2561,6 +2561,9 @@
|
||||
Smart Guides (⌘U) Repères commandés (⌘U)
|
||||
Smart Punctuation… Ponctuation typographique…
|
||||
Smooth Lisser
|
||||
Corner Angle
|
||||
Relative Relatif
|
||||
Absolute Absolu
|
||||
Smooth Color Couleur lissée
|
||||
Smooth Tool Outil Arrondi
|
||||
Snap Curves To Lines Aimanter les courbes aux lignes
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2564,6 +2564,9 @@
|
||||
Smart Guides (⌘U) Guide sensibili (⌘U)
|
||||
Smart Punctuation… Punteggiatura intelligente…
|
||||
Smooth Smussa
|
||||
Corner Angolo
|
||||
Relative Relativo
|
||||
Absolute Assoluto
|
||||
Smooth Color Colore graduale
|
||||
Smooth Tool Strumento Smussa
|
||||
Snap Curves To Lines Aggancia curve a linee
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2379,6 +2379,9 @@
|
||||
Smart Guides (View → Smart Guides) スマートガイド (表示 → スマートガイド)
|
||||
Smart Guides (⌘U) スマートガイド (⌘U)
|
||||
Smooth スムーズ
|
||||
Corner コーナー
|
||||
Relative 相対
|
||||
Absolute 絶対
|
||||
Smooth Tool スムーズツール
|
||||
Snap Curves To Lines 曲線を直線にスナップ
|
||||
View: 表示:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2560,6 +2560,9 @@
|
||||
Smart Guides (⌘U) Умные направляющие (⌘U)
|
||||
Smart Punctuation… Умная пунктуация…
|
||||
Smooth Сгладить
|
||||
Corner Угол
|
||||
Relative Относительно
|
||||
Absolute Абсолютно
|
||||
Smooth Color Сглаженный цвет
|
||||
Smooth Tool Инструмент «Сглаживание»
|
||||
Snap Curves To Lines Привязывать кривые к линиям
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2556,6 +2556,9 @@
|
||||
Smart Guides (⌘U) Розумні напрямні (⌘U)
|
||||
Smart Punctuation… Розумна пунктуація…
|
||||
Smooth Згладити
|
||||
Corner Кут
|
||||
Relative Відносно
|
||||
Absolute Абсолютно
|
||||
Smooth Color Згладжений колір
|
||||
Smooth Tool Інструмент «Згладжування»
|
||||
Snap Curves To Lines Прив’язувати криві до ліній
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 632 and column 40.
|
@@ -2481,6 +2481,9 @@
|
||||
Smart Guides (⌘U) 智能参考线 (⌘U)
|
||||
Smart Punctuation… 智能标点…
|
||||
Smooth 平滑
|
||||
Corner 尖角
|
||||
Relative 相对
|
||||
Absolute 绝对
|
||||
Smooth Color 平滑颜色
|
||||
Smooth Tool 平滑工具
|
||||
Snap Curves To Lines 将曲线对齐到直线
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2567,6 +2567,9 @@
|
||||
Smart Guides (⌘U) 智慧型參考線 (⌘U)
|
||||
Smart Punctuation… 智慧型標點…
|
||||
Smooth 平滑
|
||||
Corner 角點
|
||||
Relative 相對
|
||||
Absolute 絕對
|
||||
Smooth Color 平滑顏色
|
||||
Smooth Tool 平滑工具
|
||||
Snap Curves To Lines 將曲線靠齊線條
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -206,4 +206,23 @@ mod tests {
|
||||
app.run("app.quit", json!({})).unwrap();
|
||||
assert_eq!(app.ui.status, "quit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_control_channels_quit_says_when_it_asks_first() {
|
||||
// #830: it answered null at once, before the question.
|
||||
let (mut app, _) = app();
|
||||
let ctx = egui::Context::default();
|
||||
let quit = |app: &mut VectorcraftApp| {
|
||||
let (req, _rx) = crate::control::ControlRequest::new("app.quit", json!({}));
|
||||
let crate::control::Outcome::Done(r) = crate::control::handle(app, &ctx, &req) else { panic!("not done") };
|
||||
r
|
||||
};
|
||||
new_doc(&mut app, true);
|
||||
assert_eq!(quit(&mut app), json!({"ok": true, "result": {"pending": KIND}}));
|
||||
assert_eq!((pending(&app), app.ui.status.as_str()), (Some(0), ""));
|
||||
app.ui.dialog = None;
|
||||
app.run("file.save", json!({"path": "/docs/saved.vectorcraft"})).unwrap();
|
||||
assert_eq!(quit(&mut app), json!({"ok": true, "result": null}));
|
||||
assert_eq!(app.ui.status, "quit");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -45,7 +45,8 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop:
|
||||
|
||||
The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU (wgpu, through eframe) only composites the canvas texture and draws the UI, so any GPU that can show the window will do. The desktop app picks the window's adapter itself (`apps/vectorcraft/src/gpu.rs`, eframe's `native_adapter_selector`) and logs every adapter it found and the one it uses.
|
||||
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; with Automatic, the GPU that drives a display, and among those the one that drives the primary display; then the power preference; then, on Windows, where each GPU is listed under both, a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals. `WGPU_BACKEND=vulkan` still picks Vulkan.
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; with Automatic, the GPU that drives a display, and among those the one that drives the primary display; then the power preference; then a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals.
|
||||
- **Windows backends:** Direct3D 12, falling back to OpenGL. Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared.
|
||||
- **The GPU that drives the display.** A GPU without a monitor is the wrong one even when it says it can show the window: on a desktop with a Ryzen's integrated GPU (no monitor) and an NVIDIA card driving both monitors, power saving picked the integrated GPU, Windows had to present every frame across adapters, and the integrated GPU's driver reset under that: the graphics device was lost, or the desktop's compositor went down and every monitor stayed black for minutes (pdfcraft#378). So Automatic asks the system which GPU drives which display and puts that GPU first; the power preference only orders the rest, and decides alone where the system can't say.
|
||||
- **Windows:** `EnumDisplayDevices` (the display devices attached to the desktop, and the primary one), through the `winsafe` crate; the PCI ids in each device id are matched against wgpu's adapters. The registry doesn't tell a user which adapter drives a display, and WMI reports a resolution for a GPU without a monitor, so neither is used.
|
||||
- **Linux:** the connected connectors in `/sys/class/drm` and their cards' PCI ids; a built-in panel (`eDP`, `LVDS`, `DSI`) counts as the primary display.
|
||||
|
||||
Reference in new issue
Block a user